diff options
| author | Jorge Niedbalski <jorge.niedbalski@canonical.com> | 2017-02-10 17:35:18 -0300 |
|---|---|---|
| committer | Jorge Niedbalski <jorge.niedbalski@canonical.com> | 2017-02-10 17:35:18 -0300 |
| commit | 1de20087e562f7ddbe1811dd586f324fa6f83d2f (patch) | |
| tree | b10a69cd7e9258a7e3d9ed2ded84a4e8fb9e4864 | |
| parent | 0b7b328a92c521d6115019b1ea02108cd7ca4ecb (diff) | |
| parent | 21a40c7ab606fd254183a9a70ca0ecc099076ea9 (diff) | |
[mario-splivalo, r=niedbalski, billy-olsen] Fixes LP: #1575534 and LP:#1513094
67 files changed, 1881 insertions, 1362 deletions
diff --git a/actions/backup_test.py b/actions/backup_test.py index 76f9ac0..d6c2647 100644 --- a/actions/backup_test.py +++ b/actions/backup_test.py @@ -48,5 +48,6 @@ class TestBackups(unittest.TestCase): "working-dir": "this/dir"}), call({"output": "output"})]) + if __name__ == '__main__': unittest.main() diff --git a/actions/perf b/actions/perf index ebf15db..d3ff8a6 100755 --- a/actions/perf +++ b/actions/perf @@ -122,5 +122,6 @@ def main(): Benchmark.finish() + if __name__ == "__main__": main() diff --git a/charm-helpers-sync.yaml b/charm-helpers-sync.yaml index caee27c..ed5ea5f 100644 --- a/charm-helpers-sync.yaml +++ b/charm-helpers-sync.yaml @@ -7,3 +7,4 @@ include: - contrib.python.packages - payload.execd - contrib.charmsupport + - osplatform diff --git a/charmhelpers/__init__.py b/charmhelpers/__init__.py index f72e7f8..4886788 100644 --- a/charmhelpers/__init__.py +++ b/charmhelpers/__init__.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # Bootstrap charm-helpers, installing its dependencies if necessary using # only standard libraries. diff --git a/charmhelpers/contrib/__init__.py b/charmhelpers/contrib/__init__.py index d1400a0..d7567b8 100644 --- a/charmhelpers/contrib/__init__.py +++ b/charmhelpers/contrib/__init__.py @@ -1,15 +1,13 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/charmhelpers/contrib/charmsupport/__init__.py b/charmhelpers/contrib/charmsupport/__init__.py index d1400a0..d7567b8 100644 --- a/charmhelpers/contrib/charmsupport/__init__.py +++ b/charmhelpers/contrib/charmsupport/__init__.py @@ -1,15 +1,13 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/charmhelpers/contrib/charmsupport/nrpe.py b/charmhelpers/contrib/charmsupport/nrpe.py index 2f24642..1410512 100644 --- a/charmhelpers/contrib/charmsupport/nrpe.py +++ b/charmhelpers/contrib/charmsupport/nrpe.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """Compatibility with the nrpe-external-master charm""" # Copyright 2012 Canonical Ltd. @@ -40,6 +38,7 @@ from charmhelpers.core.hookenv import ( ) from charmhelpers.core.host import service +from charmhelpers.core import host # This module adds compatibility with the nrpe-external-master and plain nrpe # subordinate charms. To use it in your charm: @@ -110,6 +109,13 @@ from charmhelpers.core.host import service # def local_monitors_relation_changed(): # update_nrpe_config() # +# 4.a If your charm is a subordinate charm set primary=False +# +# from charmsupport.nrpe import NRPE +# (...) +# def update_nrpe_config(): +# nrpe_compat = NRPE(primary=False) +# # 5. ln -s hooks.py nrpe-external-master-relation-changed # ln -s hooks.py local-monitors-relation-changed @@ -222,9 +228,10 @@ class NRPE(object): nagios_exportdir = '/var/lib/nagios/export' nrpe_confdir = '/etc/nagios/nrpe.d' - def __init__(self, hostname=None): + def __init__(self, hostname=None, primary=True): super(NRPE, self).__init__() self.config = config() + self.primary = primary self.nagios_context = self.config['nagios_context'] if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']: self.nagios_servicegroups = self.config['nagios_servicegroups'] @@ -240,6 +247,12 @@ class NRPE(object): else: self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) self.checks = [] + # Iff in an nrpe-external-master relation hook, set primary status + relation = relation_ids('nrpe-external-master') + if relation: + log("Setting charm primary status {}".format(primary)) + for rid in relation_ids('nrpe-external-master'): + relation_set(relation_id=rid, relation_settings={'primary': self.primary}) def add_check(self, *args, **kwargs): self.checks.append(Check(*args, **kwargs)) @@ -334,16 +347,25 @@ def add_init_service_checks(nrpe, services, unit_name): :param str unit_name: Unit name to use in check description """ for svc in services: + # Don't add a check for these services from neutron-gateway + if svc in ['ext-port', 'os-charm-phy-nic-mtu']: + next + upstart_init = '/etc/init/%s.conf' % svc sysv_init = '/etc/init.d/%s' % svc - if os.path.exists(upstart_init): - # Don't add a check for these services from neutron-gateway - if svc not in ['ext-port', 'os-charm-phy-nic-mtu']: - nrpe.add_check( - shortname=svc, - description='process check {%s}' % unit_name, - check_cmd='check_upstart_job %s' % svc - ) + + if host.init_is_systemd(): + nrpe.add_check( + shortname=svc, + description='process check {%s}' % unit_name, + check_cmd='check_systemd.py %s' % svc + ) + elif os.path.exists(upstart_init): + nrpe.add_check( + shortname=svc, + description='process check {%s}' % unit_name, + check_cmd='check_upstart_job %s' % svc + ) elif os.path.exists(sysv_init): cronpath = '/etc/cron.d/nagios-service-check-%s' % svc cron_file = ('*/5 * * * * root ' diff --git a/charmhelpers/contrib/charmsupport/volumes.py b/charmhelpers/contrib/charmsupport/volumes.py index 320961b..7ea43f0 100644 --- a/charmhelpers/contrib/charmsupport/volumes.py +++ b/charmhelpers/contrib/charmsupport/volumes.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. ''' Functions for managing volumes in juju units. One volume is supported per unit. diff --git a/charmhelpers/contrib/hahelpers/__init__.py b/charmhelpers/contrib/hahelpers/__init__.py index d1400a0..d7567b8 100644 --- a/charmhelpers/contrib/hahelpers/__init__.py +++ b/charmhelpers/contrib/hahelpers/__init__.py @@ -1,15 +1,13 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/charmhelpers/contrib/hahelpers/cluster.py b/charmhelpers/contrib/hahelpers/cluster.py index aa0b515..e02350e 100644 --- a/charmhelpers/contrib/hahelpers/cluster.py +++ b/charmhelpers/contrib/hahelpers/cluster.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # # Copyright 2012 Canonical Ltd. @@ -41,10 +39,11 @@ from charmhelpers.core.hookenv import ( relation_get, config as config_get, INFO, - ERROR, + DEBUG, WARNING, unit_get, - is_leader as juju_is_leader + is_leader as juju_is_leader, + status_set, ) from charmhelpers.core.decorators import ( retry_on_exception, @@ -60,6 +59,10 @@ class HAIncompleteConfig(Exception): pass +class HAIncorrectConfig(Exception): + pass + + class CRMResourceNotFound(Exception): pass @@ -274,27 +277,71 @@ def get_hacluster_config(exclude_keys=None): Obtains all relevant configuration from charm configuration required for initiating a relation to hacluster: - ha-bindiface, ha-mcastport, vip + ha-bindiface, ha-mcastport, vip, os-internal-hostname, + os-admin-hostname, os-public-hostname, os-access-hostname param: exclude_keys: list of setting key(s) to be excluded. returns: dict: A dict containing settings keyed by setting name. - raises: HAIncompleteConfig if settings are missing. + raises: HAIncompleteConfig if settings are missing or incorrect. ''' - settings = ['ha-bindiface', 'ha-mcastport', 'vip'] + settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'os-internal-hostname', + 'os-admin-hostname', 'os-public-hostname', 'os-access-hostname'] conf = {} for setting in settings: if exclude_keys and setting in exclude_keys: continue conf[setting] = config_get(setting) - missing = [] - [missing.append(s) for s, v in six.iteritems(conf) if v is None] - if missing: - log('Insufficient config data to configure hacluster.', level=ERROR) - raise HAIncompleteConfig + + if not valid_hacluster_config(): + raise HAIncorrectConfig('Insufficient or incorrect config data to ' + 'configure hacluster.') return conf +def valid_hacluster_config(): + ''' + Check that either vip or dns-ha is set. If dns-ha then one of os-*-hostname + must be set. + + Note: ha-bindiface and ha-macastport both have defaults and will always + be set. We only care that either vip or dns-ha is set. + + :returns: boolean: valid config returns true. + raises: HAIncompatibileConfig if settings conflict. + raises: HAIncompleteConfig if settings are missing. + ''' + vip = config_get('vip') + dns = config_get('dns-ha') + if not(bool(vip) ^ bool(dns)): + msg = ('HA: Either vip or dns-ha must be set but not both in order to ' + 'use high availability') + status_set('blocked', msg) + raise HAIncorrectConfig(msg) + + # If dns-ha then one of os-*-hostname must be set + if dns: + dns_settings = ['os-internal-hostname', 'os-admin-hostname', + 'os-public-hostname', 'os-access-hostname'] + # At this point it is unknown if one or all of the possible + # network spaces are in HA. Validate at least one is set which is + # the minimum required. + for setting in dns_settings: + if config_get(setting): + log('DNS HA: At least one hostname is set {}: {}' + ''.format(setting, config_get(setting)), + level=DEBUG) + return True + + msg = ('DNS HA: At least one os-*-hostname(s) must be set to use ' + 'DNS HA') + status_set('blocked', msg) + raise HAIncompleteConfig(msg) + + log('VIP HA: VIP is set {}'.format(vip), level=DEBUG) + return True + + def canonical_url(configs, vip_setting='vip'): ''' Returns the correct HTTP URL to this host given the state of HTTPS diff --git a/charmhelpers/contrib/python/__init__.py b/charmhelpers/contrib/python/__init__.py index d1400a0..d7567b8 100644 --- a/charmhelpers/contrib/python/__init__.py +++ b/charmhelpers/contrib/python/__init__.py @@ -1,15 +1,13 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/charmhelpers/contrib/python/packages.py b/charmhelpers/contrib/python/packages.py index a2411c3..e29bd1b 100644 --- a/charmhelpers/contrib/python/packages.py +++ b/charmhelpers/contrib/python/packages.py @@ -3,19 +3,17 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os import subprocess @@ -80,7 +78,8 @@ def pip_install_requirements(requirements, constraints=None, **options): pip_execute(command) -def pip_install(package, fatal=False, upgrade=False, venv=None, **options): +def pip_install(package, fatal=False, upgrade=False, venv=None, + constraints=None, **options): """Install a python package""" if venv: venv_python = os.path.join(venv, 'bin/pip') @@ -95,6 +94,9 @@ def pip_install(package, fatal=False, upgrade=False, venv=None, **options): if upgrade: command.append('--upgrade') + if constraints: + command.extend(['-c', constraints]) + if isinstance(package, list): command.extend(package) else: diff --git a/charmhelpers/core/__init__.py b/charmhelpers/core/__init__.py index d1400a0..d7567b8 100644 --- a/charmhelpers/core/__init__.py +++ b/charmhelpers/core/__init__.py @@ -1,15 +1,13 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/charmhelpers/core/decorators.py b/charmhelpers/core/decorators.py index bb05620..6ad41ee 100644 --- a/charmhelpers/core/decorators.py +++ b/charmhelpers/core/decorators.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # # Copyright 2014 Canonical Ltd. diff --git a/charmhelpers/core/files.py b/charmhelpers/core/files.py index 0f12d32..fdd82b7 100644 --- a/charmhelpers/core/files.py +++ b/charmhelpers/core/files.py @@ -3,19 +3,17 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. __author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>' diff --git a/charmhelpers/core/fstab.py b/charmhelpers/core/fstab.py index 3056fba..d9fa915 100644 --- a/charmhelpers/core/fstab.py +++ b/charmhelpers/core/fstab.py @@ -3,19 +3,17 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import io import os diff --git a/charmhelpers/core/hookenv.py b/charmhelpers/core/hookenv.py index 0132129..d1cb68d 100644 --- a/charmhelpers/core/hookenv.py +++ b/charmhelpers/core/hookenv.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. "Interactions with the Juju environment" # Copyright 2013 Canonical Ltd. @@ -334,6 +332,8 @@ def config(scope=None): config_cmd_line = ['config-get'] if scope is not None: config_cmd_line.append(scope) + else: + config_cmd_line.append('--all') config_cmd_line.append('--format=json') try: config_data = json.loads( @@ -616,6 +616,20 @@ def close_port(port, protocol="TCP"): subprocess.check_call(_args) +def open_ports(start, end, protocol="TCP"): + """Opens a range of service network ports""" + _args = ['open-port'] + _args.append('{}-{}/{}'.format(start, end, protocol)) + subprocess.check_call(_args) + + +def close_ports(start, end, protocol="TCP"): + """Close a range of service network ports""" + _args = ['close-port'] + _args.append('{}-{}/{}'.format(start, end, protocol)) + subprocess.check_call(_args) + + @cached def unit_get(attribute): """Get the unit ID for the remote unit""" @@ -845,6 +859,20 @@ def translate_exc(from_exc, to_exc): return inner_translate_exc1 +def application_version_set(version): + """Charm authors may trigger this command from any hook to output what + version of the application is running. This could be a package version, + for instance postgres version 9.5. It could also be a build number or + version control revision identifier, for instance git sha 6fb7ba68. """ + + cmd = ['application-version-set'] + cmd.append(version) + try: + subprocess.check_call(cmd) + except OSError: + log("Application Version: {}".format(version)) + + @translate_exc(from_exc=OSError, to_exc=NotImplementedError) def is_leader(): """Does the current unit hold the juju leadership @@ -1006,4 +1034,4 @@ def network_get_primary_address(binding): :raise: NotImplementedError if run on Juju < 2.0 ''' cmd = ['network-get', '--primary-address', binding] - return subprocess.check_output(cmd).strip() + return subprocess.check_output(cmd).decode('UTF-8').strip() diff --git a/charmhelpers/core/host.py b/charmhelpers/core/host.py index e367e45..14ffd15 100644 --- a/charmhelpers/core/host.py +++ b/charmhelpers/core/host.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. """Tools for working with the host system""" # Copyright 2012 Canonical Ltd. @@ -32,14 +30,31 @@ import subprocess import hashlib import functools import itertools -from contextlib import contextmanager -from collections import OrderedDict - import six +from contextlib import contextmanager +from collections import OrderedDict from .hookenv import log from .fstab import Fstab - +from charmhelpers.osplatform import get_platform + +__platform__ = get_platform() +if __platform__ == "ubuntu": + from charmhelpers.core.host_factory.ubuntu import ( + service_available, + add_new_group, + lsb_release, + cmp_pkgrevno, + ) # flake8: noqa -- ignore F401 for this import +elif __platform__ == "centos": + from charmhelpers.core.host_factory.centos import ( + service_available, + add_new_group, + lsb_release, + cmp_pkgrevno, + ) # flake8: noqa -- ignore F401 for this import + +UPDATEDB_PATH = '/etc/updatedb.conf' def service_start(service_name): """Start a system service""" @@ -146,8 +161,11 @@ def service_running(service_name): return False else: # This works for upstart scripts where the 'service' command - # returns a consistent string to represent running 'start/running' - if "start/running" in output: + # returns a consistent string to represent running + # 'start/running' + if ("start/running" in output or + "is running" in output or + "up and running" in output): return True elif os.path.exists(_INIT_D_CONF.format(service_name)): # Check System V scripts init script return codes @@ -155,18 +173,6 @@ def service_running(service_name): return False -def service_available(service_name): - """Determine whether a system service is available""" - try: - subprocess.check_output( - ['service', service_name, 'status'], - stderr=subprocess.STDOUT).decode('UTF-8') - except subprocess.CalledProcessError as e: - return b'unrecognized service' not in e.output - else: - return True - - SYSTEMD_SYSTEM = '/run/systemd/system' @@ -175,8 +181,9 @@ def init_is_systemd(): return os.path.isdir(SYSTEMD_SYSTEM) -def adduser(username, password=None, shell='/bin/bash', system_user=False, - primary_group=None, secondary_groups=None, uid=None): +def adduser(username, password=None, shell='/bin/bash', + system_user=False, primary_group=None, + secondary_groups=None, uid=None, home_dir=None): """Add a user to the system. Will log but otherwise succeed if the user already exists. @@ -188,6 +195,7 @@ def adduser(username, password=None, shell='/bin/bash', system_user=False, :param str primary_group: Primary group for user; defaults to username :param list secondary_groups: Optional list of additional groups :param int uid: UID for user being created + :param str home_dir: Home directory for user :returns: The password database entry struct, as returned by `pwd.getpwnam` """ @@ -202,6 +210,8 @@ def adduser(username, password=None, shell='/bin/bash', system_user=False, cmd = ['useradd'] if uid: cmd.extend(['--uid', str(uid)]) + if home_dir: + cmd.extend(['--home', str(home_dir)]) if system_user or password is None: cmd.append('--system') else: @@ -285,17 +295,7 @@ def add_group(group_name, system_group=False, gid=None): log('group with gid {0} already exists!'.format(gid)) except KeyError: log('creating group {0}'.format(group_name)) - cmd = ['addgroup'] - if gid: - cmd.extend(['--gid', str(gid)]) - if system_group: - cmd.append('--system') - else: - cmd.extend([ - '--group', - ]) - cmd.append(group_name) - subprocess.check_call(cmd) + add_new_group(group_name, system_group, gid) group_info = grp.getgrnam(group_name) return group_info @@ -307,15 +307,17 @@ def add_user_to_group(username, group): subprocess.check_call(cmd) -def rsync(from_path, to_path, flags='-r', options=None): +def rsync(from_path, to_path, flags='-r', options=None, timeout=None): """Replicate the contents of a path""" options = options or ['--delete', '--executability'] cmd = ['/usr/bin/rsync', flags] + if timeout: + cmd = ['timeout', str(timeout)] + cmd cmd.extend(options) cmd.append(from_path) cmd.append(to_path) log(" ".join(cmd)) - return subprocess.check_output(cmd).decode('UTF-8').strip() + return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip() def symlink(source, destination): @@ -540,16 +542,6 @@ def restart_on_change_helper(lambda_f, restart_map, stopstart=False, return r -def lsb_release(): - """Return /etc/lsb-release in a dict""" - d = {} - with open('/etc/lsb-release', 'r') as lsb: - for l in lsb: - k, v = l.split('=') - d[k.strip()] = v.strip() - return d - - def pwgen(length=None): """Generate a random pasword.""" if length is None: @@ -673,25 +665,6 @@ def get_nic_hwaddr(nic): return hwaddr -def cmp_pkgrevno(package, revno, pkgcache=None): - """Compare supplied revno with the revno of the installed package - - * 1 => Installed revno is greater than supplied arg - * 0 => Installed revno is the same as supplied arg - * -1 => Installed revno is less than supplied arg - - This function imports apt_cache function from charmhelpers.fetch if - the pkgcache argument is None. Be sure to add charmhelpers.fetch if - you call this function, or pass an apt_pkg.Cache() instance. - """ - import apt_pkg - if not pkgcache: - from charmhelpers.fetch import apt_cache - pkgcache = apt_cache() - pkg = pkgcache[package] - return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) - - @contextmanager def chdir(directory): """Change the current working directory to a different directory for a code @@ -714,7 +687,7 @@ def chownr(path, owner, group, follow_links=True, chowntopdir=False): :param str path: The string path to start changing ownership. :param str owner: The owner string to use when looking up the uid. :param str group: The group string to use when looking up the gid. - :param bool follow_links: Also Chown links if True + :param bool follow_links: Also follow and chown links if True :param bool chowntopdir: Also chown path itself if True """ uid = pwd.getpwnam(owner).pw_uid @@ -728,7 +701,7 @@ def chownr(path, owner, group, follow_links=True, chowntopdir=False): broken_symlink = os.path.lexists(path) and not os.path.exists(path) if not broken_symlink: chown(path, uid, gid) - for root, dirs, files in os.walk(path): + for root, dirs, files in os.walk(path, followlinks=follow_links): for name in dirs + files: full = os.path.join(root, name) broken_symlink = os.path.lexists(full) and not os.path.exists(full) @@ -762,3 +735,42 @@ def get_total_ram(): assert unit == 'kB', 'Unknown unit' return int(value) * 1024 # Classic, not KiB. raise NotImplementedError() + + +UPSTART_CONTAINER_TYPE = '/run/container_type' + + +def is_container(): + """Determine whether unit is running in a container + + @return: boolean indicating if unit is in a container + """ + if init_is_systemd(): + # Detect using systemd-detect-virt + return subprocess.call(['systemd-detect-virt', + '--container']) == 0 + else: + # Detect using upstart container file marker + return os.path.exists(UPSTART_CONTAINER_TYPE) + + +def add_to_updatedb_prunepath(path): + with open(UPDATEDB_PATH, 'r+') as f_id: + updatedb_text = f_id.read() + output = updatedb(updatedb_text, path) + f_id.seek(0) + f_ids.write(output) + f_id.truncate() + + +def updatedb(updatedb_text, new_path): + lines = [line for line in updatedb_text.split("\n")] + for i, line in enumerate(lines): + if line.startswith("PRUNEPATHS="): + paths_line = line.split("=")[1].replace('"', '') + paths = paths_line.split(" ") + if new_path not in paths: + paths.append(new_path) + lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths)) + output = "\n".join(lines) + return output diff --git a/charmhelpers/core/host_factory/__init__.py b/charmhelpers/core/host_factory/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/charmhelpers/core/host_factory/__init__.py diff --git a/charmhelpers/core/host_factory/centos.py b/charmhelpers/core/host_factory/centos.py new file mode 100644 index 0000000..902d469 --- /dev/null +++ b/charmhelpers/core/host_factory/centos.py @@ -0,0 +1,56 @@ +import subprocess +import yum +import os + + +def service_available(service_name): + # """Determine whether a system service is available.""" + if os.path.isdir('/run/systemd/system'): + cmd = ['systemctl', 'is-enabled', service_name] + else: + cmd = ['service', service_name, 'is-enabled'] + return subprocess.call(cmd) == 0 + + +def add_new_group(group_name, system_group=False, gid=None): + cmd = ['groupadd'] + if gid: + cmd.extend(['--gid', str(gid)]) + if system_group: + cmd.append('-r') + cmd.append(group_name) + subprocess.check_call(cmd) + + +def lsb_release(): + """Return /etc/os-release in a dict.""" + d = {} + with open('/etc/os-release', 'r') as lsb: + for l in lsb: + s = l.split('=') + if len(s) != 2: + continue + d[s[0].strip()] = s[1].strip() + return d + + +def cmp_pkgrevno(package, revno, pkgcache=None): + """Compare supplied revno with the revno of the installed package. + + * 1 => Installed revno is greater than supplied arg + * 0 => Installed revno is the same as supplied arg + * -1 => Installed revno is less than supplied arg + + This function imports YumBase function if the pkgcache argument + is None. + """ + if not pkgcache: + y = yum.YumBase() + packages = y.doPackageLists() + pkgcache = {i.Name: i.version for i in packages['installed']} + pkg = pkgcache[package] + if pkg > revno: + return 1 + if pkg < revno: + return -1 + return 0 diff --git a/charmhelpers/core/host_factory/ubuntu.py b/charmhelpers/core/host_factory/ubuntu.py new file mode 100644 index 0000000..8c66af5 --- /dev/null +++ b/charmhelpers/core/host_factory/ubuntu.py @@ -0,0 +1,56 @@ +import subprocess + + +def service_available(service_name): + """Determine whether a system service is available""" + try: + subprocess.check_output( + ['service', service_name, 'status'], + stderr=subprocess.STDOUT).decode('UTF-8') + except subprocess.CalledProcessError as e: + return b'unrecognized service' not in e.output + else: + return True + + +def add_new_group(group_name, system_group=False, gid=None): + cmd = ['addgroup'] + if gid: + cmd.extend(['--gid', str(gid)]) + if system_group: + cmd.append('--system') + else: + cmd.extend([ + '--group', + ]) + cmd.append(group_name) + subprocess.check_call(cmd) + + +def lsb_release(): + """Return /etc/lsb-release in a dict""" + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + return d + + +def cmp_pkgrevno(package, revno, pkgcache=None): + """Compare supplied revno with the revno of the installed package. + + * 1 => Installed revno is greater than supplied arg + * 0 => Installed revno is the same as supplied arg + * -1 => Installed revno is less than supplied arg + + This function imports apt_cache function from charmhelpers.fetch if + the pkgcache argument is None. Be sure to add charmhelpers.fetch if + you call this function, or pass an apt_pkg.Cache() instance. + """ + import apt_pkg + if not pkgcache: + from charmhelpers.fetch import apt_cache + pkgcache = apt_cache() + pkg = pkgcache[package] + return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) diff --git a/charmhelpers/core/hugepage.py b/charmhelpers/core/hugepage.py index a783ad9..54b5b5e 100644 --- a/charmhelpers/core/hugepage.py +++ b/charmhelpers/core/hugepage.py @@ -2,19 +2,17 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import yaml from charmhelpers.core import fstab diff --git a/charmhelpers/core/kernel.py b/charmhelpers/core/kernel.py index 5dc6495..2d40452 100644 --- a/charmhelpers/core/kernel.py +++ b/charmhelpers/core/kernel.py @@ -3,29 +3,40 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" +import re +import subprocess +from charmhelpers.osplatform import get_platform from charmhelpers.core.hookenv import ( log, INFO ) -from subprocess import check_call, check_output -import re +__platform__ = get_platform() +if __platform__ == "ubuntu": + from charmhelpers.core.kernel_factory.ubuntu import ( + persistent_modprobe, + update_initramfs, + ) # flake8: noqa -- ignore F401 for this import +elif __platform__ == "centos": + from charmhelpers.core.kernel_factory.centos import ( + persistent_modprobe, + update_initramfs, + ) # flake8: noqa -- ignore F401 for this import + +__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" def modprobe(module, persist=True): @@ -34,11 +45,9 @@ def modprobe(module, persist=True): log('Loading kernel module %s' % module, level=INFO) - check_call(cmd) + subprocess.check_call(cmd) if persist: - with open('/etc/modules', 'r+') as modules: - if module not in modules.read(): - modules.write(module) + persistent_modprobe(module) def rmmod(module, force=False): @@ -48,21 +57,16 @@ def rmmod(module, force=False): cmd.append('-f') cmd.append(module) log('Removing kernel module %s' % module, level=INFO) - return check_call(cmd) + return subprocess.check_call(cmd) def lsmod(): """Shows what kernel modules are currently loaded""" - return check_output(['lsmod'], - universal_newlines=True) + return subprocess.check_output(['lsmod'], + universal_newlines=True) def is_module_loaded(module): """Checks if a kernel module is already loaded""" matches = re.findall('^%s[ ]+' % module, lsmod(), re.M) return len(matches) > 0 - - -def update_initramfs(version='all'): - """Updates an initramfs image""" - return check_call(["update-initramfs", "-k", version, "-u"]) diff --git a/charmhelpers/core/kernel_factory/__init__.py b/charmhelpers/core/kernel_factory/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/charmhelpers/core/kernel_factory/__init__.py diff --git a/charmhelpers/core/kernel_factory/centos.py b/charmhelpers/core/kernel_factory/centos.py new file mode 100644 index 0000000..1c402c1 --- /dev/null +++ b/charmhelpers/core/kernel_factory/centos.py @@ -0,0 +1,17 @@ +import subprocess +import os + + +def persistent_modprobe(module): + """Load a kernel module and configure for auto-load on reboot.""" + if not os.path.exists('/etc/rc.modules'): + open('/etc/rc.modules', 'a') + os.chmod('/etc/rc.modules', 111) + with open('/etc/rc.modules', 'r+') as modules: + if module not in modules.read(): + modules.write('modprobe %s\n' % module) + + +def update_initramfs(version='all'): + """Updates an initramfs image.""" + return subprocess.check_call(["dracut", "-f", version]) diff --git a/charmhelpers/core/kernel_factory/ubuntu.py b/charmhelpers/core/kernel_factory/ubuntu.py new file mode 100644 index 0000000..3de372f --- /dev/null +++ b/charmhelpers/core/kernel_factory/ubuntu.py @@ -0,0 +1,13 @@ +import subprocess + + +def persistent_modprobe(module): + """Load a kernel module and configure for auto-load on reboot.""" + with open('/etc/modules', 'r+') as modules: + if module not in modules.read(): + modules.write(module + "\n") + + +def update_initramfs(version='all'): + """Updates an initramfs image.""" + return subprocess.check_call(["update-initramfs", "-k", version, "-u"]) diff --git a/charmhelpers/core/services/__init__.py b/charmhelpers/core/services/__init__.py index 0928158..61fd074 100644 --- a/charmhelpers/core/services/__init__.py +++ b/charmhelpers/core/services/__init__.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from .base import * # NOQA from .helpers import * # NOQA diff --git a/charmhelpers/core/services/base.py b/charmhelpers/core/services/base.py index a42660c..ca9dc99 100644 --- a/charmhelpers/core/services/base.py +++ b/charmhelpers/core/services/base.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os import json diff --git a/charmhelpers/core/services/helpers.py b/charmhelpers/core/services/helpers.py index 2423704..3e6e30d 100644 --- a/charmhelpers/core/services/helpers.py +++ b/charmhelpers/core/services/helpers.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os import yaml diff --git a/charmhelpers/core/strutils.py b/charmhelpers/core/strutils.py index 7e3f969..dd9b971 100644 --- a/charmhelpers/core/strutils.py +++ b/charmhelpers/core/strutils.py @@ -3,19 +3,17 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import six import re diff --git a/charmhelpers/core/sysctl.py b/charmhelpers/core/sysctl.py index 21cc8ab..6e413e3 100644 --- a/charmhelpers/core/sysctl.py +++ b/charmhelpers/core/sysctl.py @@ -3,19 +3,17 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import yaml diff --git a/charmhelpers/core/templating.py b/charmhelpers/core/templating.py index d2d8eaf..7b801a3 100644 --- a/charmhelpers/core/templating.py +++ b/charmhelpers/core/templating.py @@ -1,20 +1,19 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os +import sys from charmhelpers.core import host from charmhelpers.core import hookenv @@ -40,8 +39,9 @@ def render(source, target, context, owner='root', group='root', The rendered template will be written to the file as well as being returned as a string. - Note: Using this requires python-jinja2; if it is not installed, calling - this will attempt to use charmhelpers.fetch.apt_install to install it. + Note: Using this requires python-jinja2 or python3-jinja2; if it is not + installed, calling this will attempt to use charmhelpers.fetch.apt_install + to install it. """ try: from jinja2 import FileSystemLoader, Environment, exceptions @@ -53,7 +53,10 @@ def render(source, target, context, owner='root', group='root', 'charmhelpers.fetch to install it', level=hookenv.ERROR) raise - apt_install('python-jinja2', fatal=True) + if sys.version_info.major == 2: + apt_install('python-jinja2', fatal=True) + else: + apt_install('python3-jinja2', fatal=True) from jinja2 import FileSystemLoader, Environment, exceptions if template_loader: diff --git a/charmhelpers/core/unitdata.py b/charmhelpers/core/unitdata.py index 338104e..54ec969 100644 --- a/charmhelpers/core/unitdata.py +++ b/charmhelpers/core/unitdata.py @@ -3,20 +3,17 @@ # # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. -# -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# http://www.apache.org/licenses/LICENSE-2.0 # +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # # Authors: # Kapil Thangavelu <kapil.foss@gmail.com> diff --git a/charmhelpers/fetch/__init__.py b/charmhelpers/fetch/__init__.py index ad485ec..ec5e0fe 100644 --- a/charmhelpers/fetch/__init__.py +++ b/charmhelpers/fetch/__init__.py @@ -1,32 +1,24 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import importlib -from tempfile import NamedTemporaryFile -import time +from charmhelpers.osplatform import get_platform from yaml import safe_load -from charmhelpers.core.host import ( - lsb_release -) -import subprocess from charmhelpers.core.hookenv import ( config, log, ) -import os import six if six.PY3: @@ -35,87 +27,6 @@ else: from urlparse import urlparse, urlunparse -CLOUD_ARCHIVE = """# Ubuntu Cloud Archive -deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main -""" -PROPOSED_POCKET = """# Proposed -deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted -""" -CLOUD_ARCHIVE_POCKETS = { - # Folsom - 'folsom': 'precise-updates/folsom', - 'precise-folsom': 'precise-updates/folsom', - 'precise-folsom/updates': 'precise-updates/folsom', - 'precise-updates/folsom': 'precise-updates/folsom', - 'folsom/proposed': 'precise-proposed/folsom', - 'precise-folsom/proposed': 'precise-proposed/folsom', - 'precise-proposed/folsom': 'precise-proposed/folsom', - # Grizzly - 'grizzly': 'precise-updates/grizzly', - 'precise-grizzly': 'precise-updates/grizzly', - 'precise-grizzly/updates': 'precise-updates/grizzly', - 'precise-updates/grizzly': 'precise-updates/grizzly', - 'grizzly/proposed': 'precise-proposed/grizzly', - 'precise-grizzly/proposed': 'precise-proposed/grizzly', - 'precise-proposed/grizzly': 'precise-proposed/grizzly', - # Havana - 'havana': 'precise-updates/havana', - 'precise-havana': 'precise-updates/havana', - 'precise-havana/updates': 'precise-updates/havana', - 'precise-updates/havana': 'precise-updates/havana', - 'havana/proposed': 'precise-proposed/havana', - 'precise-havana/proposed': 'precise-proposed/havana', - 'precise-proposed/havana': 'precise-proposed/havana', - # Icehouse - 'icehouse': 'precise-updates/icehouse', - 'precise-icehouse': 'precise-updates/icehouse', - 'precise-icehouse/updates': 'precise-updates/icehouse', - 'precise-updates/icehouse': 'precise-updates/icehouse', - 'icehouse/proposed': 'precise-proposed/icehouse', - 'precise-icehouse/proposed': 'precise-proposed/icehouse', - 'precise-proposed/icehouse': 'precise-proposed/icehouse', - # Juno - 'juno': 'trusty-updates/juno', - 'trusty-juno': 'trusty-updates/juno', - 'trusty-juno/updates': 'trusty-updates/juno', - 'trusty-updates/juno': 'trusty-updates/juno', - 'juno/proposed': 'trusty-proposed/juno', - 'trusty-juno/proposed': 'trusty-proposed/juno', - 'trusty-proposed/juno': 'trusty-proposed/juno', - # Kilo - 'kilo': 'trusty-updates/kilo', - 'trusty-kilo': 'trusty-updates/kilo', - 'trusty-kilo/updates': 'trusty-updates/kilo', - 'trusty-updates/kilo': 'trusty-updates/kilo', - 'kilo/proposed': 'trusty-proposed/kilo', - 'trusty-kilo/proposed': 'trusty-proposed/kilo', - 'trusty-proposed/kilo': 'trusty-proposed/kilo', - # Liberty - 'liberty': 'trusty-updates/liberty', - 'trusty-liberty': 'trusty-updates/liberty', - 'trusty-liberty/updates': 'trusty-updates/liberty', - 'trusty-updates/liberty': 'trusty-updates/liberty', - 'liberty/proposed': 'trusty-proposed/liberty', - 'trusty-liberty/proposed': 'trusty-proposed/liberty', - 'trusty-proposed/liberty': 'trusty-proposed/liberty', - # Mitaka - 'mitaka': 'trusty-updates/mitaka', - 'trusty-mitaka': 'trusty-updates/mitaka', - 'trusty-mitaka/updates': 'trusty-updates/mitaka', - 'trusty-updates/mitaka': 'trusty-updates/mitaka', - 'mitaka/proposed': 'trusty-proposed/mitaka', - 'trusty-mitaka/proposed': 'trusty-proposed/mitaka', - 'trusty-proposed/mitaka': 'trusty-proposed/mitaka', - # Newton - 'newton': 'xenial-updates/newton', - 'xenial-newton': 'xenial-updates/newton', - 'xenial-newton/updates': 'xenial-updates/newton', - 'xenial-updates/newton': 'xenial-updates/newton', - 'newton/proposed': 'xenial-proposed/newton', - 'xenial-newton/proposed': 'xenial-proposed/newton', - 'xenial-proposed/newton': 'xenial-proposed/newton', -} - # The order of this list is very important. Handlers should be listed in from # least- to most-specific URL matching. FETCH_HANDLERS = ( @@ -124,10 +35,6 @@ FETCH_HANDLERS = ( 'charmhelpers.fetch.giturl.GitUrlFetchHandler', ) -APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. -APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. -APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. - class SourceConfigError(Exception): pass @@ -165,180 +72,38 @@ class BaseFetchHandler(object): return urlunparse(parts) -def filter_installed_packages(packages): - """Returns a list of packages that require installation""" - cache = apt_cache() - _pkgs = [] - for package in packages: - try: - p = cache[package] - p.current_ver or _pkgs.append(package) - except KeyError: - log('Package {} has no installation candidate.'.format(package), - level='WARNING') - _pkgs.append(package) - return _pkgs - - -def apt_cache(in_memory=True): - """Build and return an apt cache""" - from apt import apt_pkg - apt_pkg.init() - if in_memory: - apt_pkg.config.set("Dir::Cache::pkgcache", "") - apt_pkg.config.set("Dir::Cache::srcpkgcache", "") - return apt_pkg.Cache() - - -def apt_install(packages, options=None, fatal=False): - """Install one or more packages""" - if options is None: - options = ['--option=Dpkg::Options::=--force-confold'] - - cmd = ['apt-get', '--assume-yes'] - cmd.extend(options) - cmd.append('install') - if isinstance(packages, six.string_types): - cmd.append(packages) - else: - cmd.extend(packages) - log("Installing {} with options: {}".format(packages, - options)) - _run_apt_command(cmd, fatal) - - -def apt_upgrade(options=None, fatal=False, dist=False): - """Upgrade all packages""" - if options is None: - options = ['--option=Dpkg::Options::=--force-confold'] - - cmd = ['apt-get', '--assume-yes'] - cmd.extend(options) - if dist: - cmd.append('dist-upgrade') - else: - cmd.append('upgrade') - log("Upgrading with options: {}".format(options)) - _run_apt_command(cmd, fatal) - - -def apt_update(fatal=False): - """Update local apt cache""" - cmd = ['apt-get', 'update'] - _run_apt_command(cmd, fatal) - - -def apt_purge(packages, fatal=False): - """Purge one or more packages""" - cmd = ['apt-get', '--assume-yes', 'purge'] - if isinstance(packages, six.string_types): - cmd.append(packages) - else: - cmd.extend(packages) - log("Purging {}".format(packages)) - _run_apt_command(cmd, fatal) - - -def apt_mark(packages, mark, fatal=False): - """Flag one or more packages using apt-mark""" - log("Marking {} as {}".format(packages, mark)) - cmd = ['apt-mark', mark] - if isinstance(packages, six.string_types): - cmd.append(packages) - else: - cmd.extend(packages) - - if fatal: - subprocess.check_call(cmd, universal_newlines=True) - else: - subprocess.call(cmd, universal_newlines=True) +__platform__ = get_platform() +module = "charmhelpers.fetch.%s" % __platform__ +fetch = importlib.import_module(module) +filter_installed_packages = fetch.filter_installed_packages +install = fetch.install +upgrade = fetch.upgrade +update = fetch.update +purge = fetch.purge +add_source = fetch.add_source -def apt_hold(packages, fatal=False): - return apt_mark(packages, 'hold', fatal=fatal) - - -def apt_unhold(packages, fatal=False): - return apt_mark(packages, 'unhold', fatal=fatal) - - -def add_source(source, key=None): - """Add a package source to this system. - - @param source: a URL or sources.list entry, as supported by - add-apt-repository(1). Examples:: - - ppa:charmers/example - deb https://stub:key@private.example.com/ubuntu trusty main - - In addition: - 'proposed:' may be used to enable the standard 'proposed' - pocket for the release. - 'cloud:' may be used to activate official cloud archive pockets, - such as 'cloud:icehouse' - 'distro' may be used as a noop - - @param key: A key to be added to the system's APT keyring and used - to verify the signatures on packages. Ideally, this should be an - ASCII format GPG public key including the block headers. A GPG key - id may also be used, but be aware that only insecure protocols are - available to retrieve the actual public key from a public keyserver - placing your Juju environment at risk. ppa and cloud archive keys - are securely added automtically, so sould not be provided. - """ - if source is None: - log('Source is not present. Skipping') - return - - if (source.startswith('ppa:') or - source.startswith('http') or - source.startswith('deb ') or - source.startswith('cloud-archive:')): - subprocess.check_call(['add-apt-repository', '--yes', source]) - elif source.startswith('cloud:'): - apt_install(filter_installed_packages(['ubuntu-cloud-keyring']), - fatal=True) - pocket = source.split(':')[-1] - if pocket not in CLOUD_ARCHIVE_POCKETS: - raise SourceConfigError( - 'Unsupported cloud: source option %s' % - pocket) - actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket] - with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt: - apt.write(CLOUD_ARCHIVE.format(actual_pocket)) - elif source == 'proposed': - release = lsb_release()['DISTRIB_CODENAME'] - with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: - apt.write(PROPOSED_POCKET.format(release)) - elif source == 'distro': - pass - else: - log("Unknown source: {!r}".format(source)) - - if key: - if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: - with NamedTemporaryFile('w+') as key_file: - key_file.write(key) - key_file.flush() - key_file.seek(0) - subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file) - else: - # Note that hkp: is in no way a secure protocol. Using a - # GPG key id is pointless from a security POV unless you - # absolutely trust your network and DNS. - subprocess.check_call(['apt-key', 'adv', '--keyserver', - 'hkp://keyserver.ubuntu.com:80', '--recv', - key]) +if __platform__ == "ubuntu": + apt_cache = fetch.apt_cache + apt_install = fetch.install + apt_update = fetch.update + apt_upgrade = fetch.upgrade + apt_purge = fetch.purge + apt_mark = fetch.apt_mark + apt_hold = fetch.apt_hold + apt_unhold = fetch.apt_unhold + get_upstream_version = fetch.get_upstream_version +elif __platform__ == "centos": + yum_search = fetch.yum_search def configure_sources(update=False, sources_var='install_sources', keys_var='install_keys'): - """ - Configure multiple sources from charm configuration. + """Configure multiple sources from charm configuration. The lists are encoded as yaml fragments in the configuration. - The frament needs to be included as a string. Sources and their + The fragment needs to be included as a string. Sources and their corresponding keys are of the types supported by add_source(). Example config: @@ -370,12 +135,11 @@ def configure_sources(update=False, for source, key in zip(sources, keys): add_source(source, key) if update: - apt_update(fatal=True) + fetch.update(fatal=True) def install_remote(source, *args, **kwargs): - """ - Install a file tree from a remote source + """Install a file tree from a remote source. The specified source should be a url of the form: scheme://[host]/path[#[option=value][&...]] @@ -398,19 +162,17 @@ def install_remote(source, *args, **kwargs): # We ONLY check for True here because can_handle may return a string # explaining why it can't handle a given source. handlers = [h for h in plugins() if h.can_handle(source) is True] - installed_to = None for handler in handlers: try: - installed_to = handler.install(source, *args, **kwargs) + return handler.install(source, *args, **kwargs) except UnhandledSource as e: log('Install source attempt unsuccessful: {}'.format(e), level='WARNING') - if not installed_to: - raise UnhandledSource("No handler found for source {}".format(source)) - return installed_to + raise UnhandledSource("No handler found for source {}".format(source)) def install_from_config(config_var_name): + """Install a file from config.""" charm_config = config() source = charm_config[config_var_name] return install_remote(source) @@ -433,40 +195,3 @@ def plugins(fetch_handlers=None): log("FetchHandler {} not found, skipping plugin".format( handler_name)) return plugin_list - - -def _run_apt_command(cmd, fatal=False): - """ - Run an APT command, checking output and retrying if the fatal flag is set - to True. - - :param: cmd: str: The apt command to run. - :param: fatal: bool: Whether the command's output should be checked and - retried. - """ - env = os.environ.copy() - - if 'DEBIAN_FRONTEND' not in env: - env['DEBIAN_FRONTEND'] = 'noninteractive' - - if fatal: - retry_count = 0 - result = None - - # If the command is considered "fatal", we need to retry if the apt - # lock was not acquired. - - while result is None or result == APT_NO_LOCK: - try: - result = subprocess.check_call(cmd, env=env) - except subprocess.CalledProcessError as e: - retry_count = retry_count + 1 - if retry_count > APT_NO_LOCK_RETRY_COUNT: - raise - result = e.returncode - log("Couldn't acquire DPKG lock. Will retry in {} seconds." - "".format(APT_NO_LOCK_RETRY_DELAY)) - time.sleep(APT_NO_LOCK_RETRY_DELAY) - - else: - subprocess.call(cmd, env=env) diff --git a/charmhelpers/fetch/archiveurl.py b/charmhelpers/fetch/archiveurl.py index b8e0943..dd24f9e 100644 --- a/charmhelpers/fetch/archiveurl.py +++ b/charmhelpers/fetch/archiveurl.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os import hashlib diff --git a/charmhelpers/fetch/bzrurl.py b/charmhelpers/fetch/bzrurl.py index cafd27f..07cd029 100644 --- a/charmhelpers/fetch/bzrurl.py +++ b/charmhelpers/fetch/bzrurl.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os from subprocess import check_call @@ -20,19 +18,20 @@ from charmhelpers.fetch import ( BaseFetchHandler, UnhandledSource, filter_installed_packages, - apt_install, + install, ) from charmhelpers.core.host import mkdir if filter_installed_packages(['bzr']) != []: - apt_install(['bzr']) + install(['bzr']) if filter_installed_packages(['bzr']) != []: raise NotImplementedError('Unable to install bzr') class BzrUrlFetchHandler(BaseFetchHandler): - """Handler for bazaar branches via generic and lp URLs""" + """Handler for bazaar branches via generic and lp URLs.""" + def can_handle(self, source): url_parts = self.parse_url(source) if url_parts.scheme not in ('bzr+ssh', 'lp', ''): @@ -42,15 +41,23 @@ class BzrUrlFetchHandler(BaseFetchHandler): else: return True - def branch(self, source, dest): + def branch(self, source, dest, revno=None): if not self.can_handle(source): raise UnhandledSource("Cannot handle {}".format(source)) + cmd_opts = [] + if revno: + cmd_opts += ['-r', str(revno)] if os.path.exists(dest): - check_call(['bzr', 'pull', '--overwrite', '-d', dest, source]) + cmd = ['bzr', 'pull'] + cmd += cmd_opts + cmd += ['--overwrite', '-d', dest, source] else: - check_call(['bzr', 'branch', source, dest]) + cmd = ['bzr', 'branch'] + cmd += cmd_opts + cmd += [source, dest] + check_call(cmd) - def install(self, source, dest=None): + def install(self, source, dest=None, revno=None): url_parts = self.parse_url(source) branch_name = url_parts.path.strip("/").split("/")[-1] if dest: @@ -59,10 +66,11 @@ class BzrUrlFetchHandler(BaseFetchHandler): dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", branch_name) - if not os.path.exists(dest_dir): - mkdir(dest_dir, perms=0o755) + if dest and not os.path.exists(dest): + mkdir(dest, perms=0o755) + try: - self.branch(source, dest_dir) + self.branch(source, dest_dir, revno) except OSError as e: raise UnhandledSource(e.strerror) return dest_dir diff --git a/charmhelpers/fetch/centos.py b/charmhelpers/fetch/centos.py new file mode 100644 index 0000000..604bbfb --- /dev/null +++ b/charmhelpers/fetch/centos.py @@ -0,0 +1,171 @@ +# Copyright 2014-2015 Canonical Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import subprocess +import os +import time +import six +import yum + +from tempfile import NamedTemporaryFile +from charmhelpers.core.hookenv import log + +YUM_NO_LOCK = 1 # The return code for "couldn't acquire lock" in YUM. +YUM_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. +YUM_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. + + +def filter_installed_packages(packages): + """Return a list of packages that require installation.""" + yb = yum.YumBase() + package_list = yb.doPackageLists() + temp_cache = {p.base_package_name: 1 for p in package_list['installed']} + + _pkgs = [p for p in packages if not temp_cache.get(p, False)] + return _pkgs + + +def install(packages, options=None, fatal=False): + """Install one or more packages.""" + cmd = ['yum', '--assumeyes'] + if options is not None: + cmd.extend(options) + cmd.append('install') + if isinstance(packages, six.string_types): + cmd.append(packages) + else: + cmd.extend(packages) + log("Installing {} with options: {}".format(packages, + options)) + _run_yum_command(cmd, fatal) + + +def upgrade(options=None, fatal=False, dist=False): + """Upgrade all packages.""" + cmd = ['yum', '--assumeyes'] + if options is not None: + cmd.extend(options) + cmd.append('upgrade') + log("Upgrading with options: {}".format(options)) + _run_yum_command(cmd, fatal) + + +def update(fatal=False): + """Update local yum cache.""" + cmd = ['yum', '--assumeyes', 'update'] + log("Update with fatal: {}".format(fatal)) + _run_yum_command(cmd, fatal) + + +def purge(packages, fatal=False): + """Purge one or more packages.""" + cmd = ['yum', '--assumeyes', 'remove'] + if isinstance(packages, six.string_types): + cmd.append(packages) + else: + cmd.extend(packages) + log("Purging {}".format(packages)) + _run_yum_command(cmd, fatal) + + +def yum_search(packages): + """Search for a package.""" + output = {} + cmd = ['yum', 'search'] + if isinstance(packages, six.string_types): + cmd.append(packages) + else: + cmd.extend(packages) + log("Searching for {}".format(packages)) + result = subprocess.check_output(cmd) + for package in list(packages): + output[package] = package in result + return output + + +def add_source(source, key=None): + """Add a package source to this system. + + @param source: a URL with a rpm package + + @param key: A key to be added to the system's keyring and used + to verify the signatures on packages. Ideally, this should be an + ASCII format GPG public key including the block headers. A GPG key + id may also be used, but be aware that only insecure protocols are + available to retrieve the actual public key from a public keyserver + placing your Juju environment at risk. + """ + if source is None: + log('Source is not present. Skipping') + return + + if source.startswith('http'): + directory = '/etc/yum.repos.d/' + for filename in os.listdir(directory): + with open(directory + filename, 'r') as rpm_file: + if source in rpm_file.read(): + break + else: + log("Add source: {!r}".format(source)) + # write in the charms.repo + with open(directory + 'Charms.repo', 'a') as rpm_file: + rpm_file.write('[%s]\n' % source[7:].replace('/', '_')) + rpm_file.write('name=%s\n' % source[7:]) + rpm_file.write('baseurl=%s\n\n' % source) + else: + log("Unknown source: {!r}".format(source)) + + if key: + if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: + with NamedTemporaryFile('w+') as key_file: + key_file.write(key) + key_file.flush() + key_file.seek(0) + subprocess.check_call(['rpm', '--import', key_file]) + else: + subprocess.check_call(['rpm', '--import', key]) + + +def _run_yum_command(cmd, fatal=False): + """Run an YUM command. + + Checks the output and retry if the fatal flag is set to True. + + :param: cmd: str: The yum command to run. + :param: fatal: bool: Whether the command's output should be checked and + retried. + """ + env = os.environ.copy() + + if fatal: + retry_count = 0 + result = None + + # If the command is considered "fatal", we need to retry if the yum + # lock was not acquired. + + while result is None or result == YUM_NO_LOCK: + try: + result = subprocess.check_call(cmd, env=env) + except subprocess.CalledProcessError as e: + retry_count = retry_count + 1 + if retry_count > YUM_NO_LOCK_RETRY_COUNT: + raise + result = e.returncode + log("Couldn't acquire YUM lock. Will retry in {} seconds." + "".format(YUM_NO_LOCK_RETRY_DELAY)) + time.sleep(YUM_NO_LOCK_RETRY_DELAY) + + else: + subprocess.call(cmd, env=env) diff --git a/charmhelpers/fetch/giturl.py b/charmhelpers/fetch/giturl.py index 65ed531..4cf21bc 100644 --- a/charmhelpers/fetch/giturl.py +++ b/charmhelpers/fetch/giturl.py @@ -1,18 +1,16 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os from subprocess import check_call, CalledProcessError @@ -20,17 +18,18 @@ from charmhelpers.fetch import ( BaseFetchHandler, UnhandledSource, filter_installed_packages, - apt_install, + install, ) if filter_installed_packages(['git']) != []: - apt_install(['git']) + install(['git']) if filter_installed_packages(['git']) != []: raise NotImplementedError('Unable to install git') class GitUrlFetchHandler(BaseFetchHandler): - """Handler for git branches via generic and github URLs""" + """Handler for git branches via generic and github URLs.""" + def can_handle(self, source): url_parts = self.parse_url(source) # TODO (mattyw) no support for ssh git@ yet diff --git a/charmhelpers/fetch/ubuntu.py b/charmhelpers/fetch/ubuntu.py new file mode 100644 index 0000000..39b9b80 --- /dev/null +++ b/charmhelpers/fetch/ubuntu.py @@ -0,0 +1,344 @@ +# Copyright 2014-2015 Canonical Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import six +import time +import subprocess + +from tempfile import NamedTemporaryFile +from charmhelpers.core.host import ( + lsb_release +) +from charmhelpers.core.hookenv import log +from charmhelpers.fetch import SourceConfigError + +CLOUD_ARCHIVE = """# Ubuntu Cloud Archive +deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main +""" + +PROPOSED_POCKET = """# Proposed +deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted +""" + +CLOUD_ARCHIVE_POCKETS = { + # Folsom + 'folsom': 'precise-updates/folsom', + 'precise-folsom': 'precise-updates/folsom', + 'precise-folsom/updates': 'precise-updates/folsom', + 'precise-updates/folsom': 'precise-updates/folsom', + 'folsom/proposed': 'precise-proposed/folsom', + 'precise-folsom/proposed': 'precise-proposed/folsom', + 'precise-proposed/folsom': 'precise-proposed/folsom', + # Grizzly + 'grizzly': 'precise-updates/grizzly', + 'precise-grizzly': 'precise-updates/grizzly', + 'precise-grizzly/updates': 'precise-updates/grizzly', + 'precise-updates/grizzly': 'precise-updates/grizzly', + 'grizzly/proposed': 'precise-proposed/grizzly', + 'precise-grizzly/proposed': 'precise-proposed/grizzly', + 'precise-proposed/grizzly': 'precise-proposed/grizzly', + # Havana + 'havana': 'precise-updates/havana', + 'precise-havana': 'precise-updates/havana', + 'precise-havana/updates': 'precise-updates/havana', + 'precise-updates/havana': 'precise-updates/havana', + 'havana/proposed': 'precise-proposed/havana', + 'precise-havana/proposed': 'precise-proposed/havana', + 'precise-proposed/havana': 'precise-proposed/havana', + # Icehouse + 'icehouse': 'precise-updates/icehouse', + 'precise-icehouse': 'precise-updates/icehouse', + 'precise-icehouse/updates': 'precise-updates/icehouse', + 'precise-updates/icehouse': 'precise-updates/icehouse', + 'icehouse/proposed': 'precise-proposed/icehouse', + 'precise-icehouse/proposed': 'precise-proposed/icehouse', + 'precise-proposed/icehouse': 'precise-proposed/icehouse', + # Juno + 'juno': 'trusty-updates/juno', + 'trusty-juno': 'trusty-updates/juno', + 'trusty-juno/updates': 'trusty-updates/juno', + 'trusty-updates/juno': 'trusty-updates/juno', + 'juno/proposed': 'trusty-proposed/juno', + 'trusty-juno/proposed': 'trusty-proposed/juno', + 'trusty-proposed/juno': 'trusty-proposed/juno', + # Kilo + 'kilo': 'trusty-updates/kilo', + 'trusty-kilo': 'trusty-updates/kilo', + 'trusty-kilo/updates': 'trusty-updates/kilo', + 'trusty-updates/kilo': 'trusty-updates/kilo', + 'kilo/proposed': 'trusty-proposed/kilo', + 'trusty-kilo/proposed': 'trusty-proposed/kilo', + 'trusty-proposed/kilo': 'trusty-proposed/kilo', + # Liberty + 'liberty': 'trusty-updates/liberty', + 'trusty-liberty': 'trusty-updates/liberty', + 'trusty-liberty/updates': 'trusty-updates/liberty', + 'trusty-updates/liberty': 'trusty-updates/liberty', + 'liberty/proposed': 'trusty-proposed/liberty', + 'trusty-liberty/proposed': 'trusty-proposed/liberty', + 'trusty-proposed/liberty': 'trusty-proposed/liberty', + # Mitaka + 'mitaka': 'trusty-updates/mitaka', + 'trusty-mitaka': 'trusty-updates/mitaka', + 'trusty-mitaka/updates': 'trusty-updates/mitaka', + 'trusty-updates/mitaka': 'trusty-updates/mitaka', + 'mitaka/proposed': 'trusty-proposed/mitaka', + 'trusty-mitaka/proposed': 'trusty-proposed/mitaka', + 'trusty-proposed/mitaka': 'trusty-proposed/mitaka', + # Newton + 'newton': 'xenial-updates/newton', + 'xenial-newton': 'xenial-updates/newton', + 'xenial-newton/updates': 'xenial-updates/newton', + 'xenial-updates/newton': 'xenial-updates/newton', + 'newton/proposed': 'xenial-proposed/newton', + 'xenial-newton/proposed': 'xenial-proposed/newton', + 'xenial-proposed/newton': 'xenial-proposed/newton', + # Ocata + 'ocata': 'xenial-updates/ocata', + 'xenial-ocata': 'xenial-updates/ocata', + 'xenial-ocata/updates': 'xenial-updates/ocata', + 'xenial-updates/ocata': 'xenial-updates/ocata', + 'ocata/proposed': 'xenial-proposed/ocata', + 'xenial-ocata/proposed': 'xenial-proposed/ocata', + 'xenial-ocata/newton': 'xenial-proposed/ocata', +} + +APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. +APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. +APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. + + +def filter_installed_packages(packages): + """Return a list of packages that require installation.""" + cache = apt_cache() + _pkgs = [] + for package in packages: + try: + p = cache[package] + p.current_ver or _pkgs.append(package) + except KeyError: + log('Package {} has no installation candidate.'.format(package), + level='WARNING') + _pkgs.append(package) + return _pkgs + + +def apt_cache(in_memory=True, progress=None): + """Build and return an apt cache.""" + from apt import apt_pkg + apt_pkg.init() + if in_memory: + apt_pkg.config.set("Dir::Cache::pkgcache", "") + apt_pkg.config.set("Dir::Cache::srcpkgcache", "") + return apt_pkg.Cache(progress) + + +def install(packages, options=None, fatal=False): + """Install one or more packages.""" + if options is None: + options = ['--option=Dpkg::Options::=--force-confold'] + + cmd = ['apt-get', '--assume-yes'] + cmd.extend(options) + cmd.append('install') + if isinstance(packages, six.string_types): + cmd.append(packages) + else: + cmd.extend(packages) + log("Installing {} with options: {}".format(packages, + options)) + _run_apt_command(cmd, fatal) + + +def upgrade(options=None, fatal=False, dist=False): + """Upgrade all packages.""" + if options is None: + options = ['--option=Dpkg::Options::=--force-confold'] + + cmd = ['apt-get', '--assume-yes'] + cmd.extend(options) + if dist: + cmd.append('dist-upgrade') + else: + cmd.append('upgrade') + log("Upgrading with options: {}".format(options)) + _run_apt_command(cmd, fatal) + + +def update(fatal=False): + """Update local apt cache.""" + cmd = ['apt-get', 'update'] + _run_apt_command(cmd, fatal) + + +def purge(packages, fatal=False): + """Purge one or more packages.""" + cmd = ['apt-get', '--assume-yes', 'purge'] + if isinstance(packages, six.string_types): + cmd.append(packages) + else: + cmd.extend(packages) + log("Purging {}".format(packages)) + _run_apt_command(cmd, fatal) + + +def apt_mark(packages, mark, fatal=False): + """Flag one or more packages using apt-mark.""" + log("Marking {} as {}".format(packages, mark)) + cmd = ['apt-mark', mark] + if isinstance(packages, six.string_types): + cmd.append(packages) + else: + cmd.extend(packages) + + if fatal: + subprocess.check_call(cmd, universal_newlines=True) + else: + subprocess.call(cmd, universal_newlines=True) + + +def apt_hold(packages, fatal=False): + return apt_mark(packages, 'hold', fatal=fatal) + + +def apt_unhold(packages, fatal=False): + return apt_mark(packages, 'unhold', fatal=fatal) + + +def add_source(source, key=None): + """Add a package source to this system. + + @param source: a URL or sources.list entry, as supported by + add-apt-repository(1). Examples:: + + ppa:charmers/example + deb https://stub:key@private.example.com/ubuntu trusty main + + In addition: + 'proposed:' may be used to enable the standard 'proposed' + pocket for the release. + 'cloud:' may be used to activate official cloud archive pockets, + such as 'cloud:icehouse' + 'distro' may be used as a noop + + @param key: A key to be added to the system's APT keyring and used + to verify the signatures on packages. Ideally, this should be an + ASCII format GPG public key including the block headers. A GPG key + id may also be used, but be aware that only insecure protocols are + available to retrieve the actual public key from a public keyserver + placing your Juju environment at risk. ppa and cloud archive keys + are securely added automtically, so sould not be provided. + """ + if source is None: + log('Source is not present. Skipping') + return + + if (source.startswith('ppa:') or + source.startswith('http') or + source.startswith('deb ') or + source.startswith('cloud-archive:')): + subprocess.check_call(['add-apt-repository', '--yes', source]) + elif source.startswith('cloud:'): + install(filter_installed_packages(['ubuntu-cloud-keyring']), + fatal=True) + pocket = source.split(':')[-1] + if pocket not in CLOUD_ARCHIVE_POCKETS: + raise SourceConfigError( + 'Unsupported cloud: source option %s' % + pocket) + actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket] + with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt: + apt.write(CLOUD_ARCHIVE.format(actual_pocket)) + elif source == 'proposed': + release = lsb_release()['DISTRIB_CODENAME'] + with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: + apt.write(PROPOSED_POCKET.format(release)) + elif source == 'distro': + pass + else: + log("Unknown source: {!r}".format(source)) + + if key: + if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: + with NamedTemporaryFile('w+') as key_file: + key_file.write(key) + key_file.flush() + key_file.seek(0) + subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file) + else: + # Note that hkp: is in no way a secure protocol. Using a + # GPG key id is pointless from a security POV unless you + # absolutely trust your network and DNS. + subprocess.check_call(['apt-key', 'adv', '--keyserver', + 'hkp://keyserver.ubuntu.com:80', '--recv', + key]) + + +def _run_apt_command(cmd, fatal=False): + """Run an APT command. + + Checks the output and retries if the fatal flag is set + to True. + + :param: cmd: str: The apt command to run. + :param: fatal: bool: Whether the command's output should be checked and + retried. + """ + env = os.environ.copy() + + if 'DEBIAN_FRONTEND' not in env: + env['DEBIAN_FRONTEND'] = 'noninteractive' + + if fatal: + retry_count = 0 + result = None + + # If the command is considered "fatal", we need to retry if the apt + # lock was not acquired. + + while result is None or result == APT_NO_LOCK: + try: + result = subprocess.check_call(cmd, env=env) + except subprocess.CalledProcessError as e: + retry_count = retry_count + 1 + if retry_count > APT_NO_LOCK_RETRY_COUNT: + raise + result = e.returncode + log("Couldn't acquire DPKG lock. Will retry in {} seconds." + "".format(APT_NO_LOCK_RETRY_DELAY)) + time.sleep(APT_NO_LOCK_RETRY_DELAY) + + else: + subprocess.call(cmd, env=env) + + +def get_upstream_version(package): + """Determine upstream version based on installed package + + @returns None (if not installed) or the upstream version + """ + import apt_pkg + cache = apt_cache() + try: + pkg = cache[package] + except: + # the package is unknown to the current apt cache. + return None + + if not pkg.current_ver: + # package is known, but no version is currently installed. + return None + + return apt_pkg.upstream_version(pkg.current_ver.ver_str) diff --git a/charmhelpers/osplatform.py b/charmhelpers/osplatform.py new file mode 100644 index 0000000..ea490bb --- /dev/null +++ b/charmhelpers/osplatform.py @@ -0,0 +1,19 @@ +import platform + + +def get_platform(): + """Return the current OS platform. + + For example: if current os platform is Ubuntu then a string "ubuntu" + will be returned (which is the name of the module). + This string is used to decide which platform module should be imported. + """ + tuple_platform = platform.linux_distribution() + current_platform = tuple_platform[0] + if "Ubuntu" in current_platform: + return "ubuntu" + elif "CentOS" in current_platform: + return "centos" + else: + raise RuntimeError("This module is not supported on {}." + .format(current_platform)) diff --git a/charmhelpers/payload/__init__.py b/charmhelpers/payload/__init__.py index e6f4249..ee55cb3 100644 --- a/charmhelpers/payload/__init__.py +++ b/charmhelpers/payload/__init__.py @@ -1,17 +1,15 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. "Tools for working with files injected into a charm just before deployment." diff --git a/charmhelpers/payload/execd.py b/charmhelpers/payload/execd.py index 4d4d81a..1502aa0 100644 --- a/charmhelpers/payload/execd.py +++ b/charmhelpers/payload/execd.py @@ -2,19 +2,17 @@ # Copyright 2014-2015 Canonical Limited. # -# This file is part of charm-helpers. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. +# http://www.apache.org/licenses/LICENSE-2.0 # -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os import sys @@ -49,11 +47,12 @@ def execd_submodule_paths(command, execd_dir=None): yield path -def execd_run(command, execd_dir=None, die_on_error=False, stderr=None): +def execd_run(command, execd_dir=None, die_on_error=True, stderr=subprocess.STDOUT): """Run command for each module within execd_dir which defines it.""" for submodule_path in execd_submodule_paths(command, execd_dir): try: - subprocess.check_call(submodule_path, shell=True, stderr=stderr) + subprocess.check_output(submodule_path, stderr=stderr, + universal_newlines=True) except subprocess.CalledProcessError as e: hookenv.log("Error ({}) running {}. Output: {}".format( e.returncode, e.cmd, e.output)) diff --git a/hooks/hooks.py b/hooks/hooks.py index 4801dc8..254fca3 100755 --- a/hooks/hooks.py +++ b/hooks/hooks.py @@ -46,6 +46,7 @@ from charmhelpers.core.host import ( from charmhelpers.core.hookenv import ( close_port, config, + is_relation_made, open_port, unit_get, relation_get, @@ -328,8 +329,8 @@ def mongodb_conf(config_data=None): config.append("") # nohttpinterface - if not config_data['web_admin_ui']: - config.append("nohttpinterface = true") + if config_data['web_admin_ui']: + config.append("rest = true") config.append("") # noscripting @@ -367,14 +368,23 @@ def mongodb_conf(config_data=None): config.append("mms-interval = %s" % config_data['mms-interval']) config.append("") - # master/slave - if config_data['master'] == "self": - config.append("master = true") + # Set either replica-set or master, depending upon whether the + # the replica-set (peer) relation is established. If the user + # chooses to use juju scale out (e.g. juju add-unit) then the + # charm will use replica-set replication. The user may opt to + # do master/slave replication or sharding as a different form + # of scaleout. + if is_relation_made('replica-set'): + config.append("replSet = %s" % config_data['replicaset']) config.append("") else: - config.append("slave = true") - config.append("source = %s" % config_data['master']) - config.append("") + if config_data['master'] == "self": + config.append("master = true") + config.append("") + else: + config.append("slave = true") + config.append("source = %s" % config_data['master']) + config.append("") # arbiter if config_data['arbiter'] != "disabled" and \ @@ -405,6 +415,27 @@ def mongodb_conf(config_data=None): return('\n'.join(config)) +def get_current_mongo_config(): + """Reads the current mongo configuration file and returns a dict + containing the key/value pairs found in the configuration file. + + :returns dict: key/value pairs of the configuration file. + """ + results = {} + with open(default_mongodb_config, 'r') as f: + for line in f: + line = line.strip() + + # Skip over comments, blank lines, and any other line + # that appears to not contain a key = value pair. + if line.startswith('#') or '=' not in line: + continue + + key, value = line.split('=', 1) + results[key.strip()] = value.strip() + return results + + def mongo_client(host=None, command=None): if host is None or command is None: return(False) @@ -539,24 +570,20 @@ def enable_replset(replicaset_name=None): juju_log('enable_replset: replicaset_name is None, exiting', level=DEBUG) try: - juju_log('enable_replset: trying to get lock on: %s' % - default_mongodb_init_config) - with FileLock(INIT_LOCKFILE): - juju_log('enable_replset: lock acquired', level=DEBUG) - with open(default_mongodb_init_config) as mongo_init_file: - mongodb_init_config = mongo_init_file.read() - if re.search(' --replSet %s ' % replicaset_name, - mongodb_init_config, re.MULTILINE) is None: - juju_log('enable_replset: --replset not preset,' - ' enabling', - level=DEBUG) - mongodb_init_config = regex_sub([(' -- ', - ' -- --replSet %s ' % - replicaset_name)], - mongodb_init_config) - update_file(default_mongodb_init_config, - mongodb_init_config) - retVal = True + juju_log('enable_replset: Enabling replicaset configuration:') + + current_config = get_current_mongo_config() + config_data = config() + + if 'replSet' in current_config and \ + current_config['replSet'] == config_data['replicaset']: + juju_log('enable_replset: replica set is already enabled', + level=DEBUG) + else: + juju_log('enable_replset: enabling replicaset %s' % + config_data['replicaset'], level=DEBUG) + mongodb_config = mongodb_conf(config_data) + retVal = update_file(default_mongodb_config, mongodb_config) juju_log('enable_replset will return: %s' % str(retVal), level=DEBUG) @@ -566,32 +593,16 @@ def enable_replset(replicaset_name=None): finally: return retVal - -def update_daemon_options(daemon_options=None): - mongodb_init_config = open(default_mongodb_init_config).read() - pat_replace = [] - if daemon_options is None or daemon_options == "none": - pat_replace.append( - (' --config /etc/mongodb.conf.*', - ' --config /etc/mongodb.conf; fi')) - else: - pat_replace.append( - (' --config /etc/mongodb.conf.*', - ' --config /etc/mongodb.conf %s; fi' % daemon_options)) - regex_sub(pat_replace, mongodb_init_config) - return(update_file(default_mongodb_init_config, mongodb_init_config)) - - -def disable_replset(replicaset_name=None): - if replicaset_name is None: - retVal = False +def remove_replset_from_upstart(): + """Removes replicaset configuration from upstart. + """ try: mongodb_init_config = open(default_mongodb_init_config).read() - if re.search(' --replSet %s ' % replicaset_name, - mongodb_init_config, re.MULTILINE) is not None: - mongodb_init_config = regex_sub([ - (' --replSet %s ' % replicaset_name, ' ') - ], mongodb_init_config) + + if re.search(' --replSet', mongodb_init_config, + re.MULTILINE) is not None: + mongodb_init_config = re.sub(' --replSet .\w+', '', + mongodb_init_config) retVal = update_file(default_mongodb_init_config, mongodb_init_config) except Exception, e: juju_log(str(e)) @@ -600,44 +611,36 @@ def disable_replset(replicaset_name=None): return(retVal) -def enable_web_admin_ui(port=None): - if port is None: - juju_log("enable_web_admin_ui: port not defined.") - return(False) +def remove_rest_from_upstart(): + """Removes --rest from upstart script + """ try: mongodb_init_config = open(default_mongodb_init_config).read() - if re.search(' --rest ', mongodb_init_config, re.MULTILINE) is None: - mongodb_init_config = regex_sub([(' -- ', ' -- --rest ')], + if re.search(' --rest ', mongodb_init_config, + re.MULTILINE) is not None: + mongodb_init_config = regex_sub([(' --rest ', ' ')], mongodb_init_config) retVal = update_file(default_mongodb_init_config, mongodb_init_config) except Exception, e: juju_log(str(e)) retVal = False finally: - if retVal: - open_port(port) return(retVal) -def disable_web_admin_ui(port=None): - if port is None: - juju_log("disable_web_admin_ui: port not defined.") - return(False) - try: - mongodb_init_config = open(default_mongodb_init_config).read() - if re.search(' --rest ', - mongodb_init_config, - re.MULTILINE) is not None: - mongodb_init_config = regex_sub([(' --rest ', ' ')], - mongodb_init_config) - retVal = update_file(default_mongodb_init_config, mongodb_init_config) - except Exception, e: - juju_log(str(e)) - retVal = False - finally: - if retVal: - close_port(port) - return(retVal) +def update_daemon_options(daemon_options=None): + mongodb_init_config = open(default_mongodb_init_config).read() + pat_replace = [] + if daemon_options is None or daemon_options == "none": + pat_replace.append( + (' --config /etc/mongodb.conf.*', + ' --config /etc/mongodb.conf; fi')) + else: + pat_replace.append( + (' --config /etc/mongodb.conf.*', + ' --config /etc/mongodb.conf %s; fi' % daemon_options)) + regex_sub(pat_replace, mongodb_init_config) + return(update_file(default_mongodb_init_config, mongodb_init_config)) def enable_arbiter(master_node=None, host=None): @@ -1002,9 +1005,9 @@ def config_changed(): # web_admin_ui if config_data['web_admin_ui']: - enable_web_admin_ui(new_web_admin_ui_port) + open_port(new_web_admin_ui_port) else: - disable_web_admin_ui(current_web_admin_ui_port) + close_port(current_web_admin_ui_port) # replicaset_master if config_data['replicaset_master'] != "auto": @@ -1141,7 +1144,7 @@ def database_relation_joined(): relation_set(relation_id(), relation_data) -@hooks.hook('replicaset-relation-joined') +@hooks.hook('replica-set-relation-joined') def replica_set_relation_joined(): juju_log("replica_set_relation_joined-start") my_hostname = unit_get('private-address') @@ -1194,7 +1197,7 @@ def am_i_primary(): raise TimeoutException('Unable to determine if local unit is primary') -@hooks.hook('replicaset-relation-changed') +@hooks.hook('replica-set-relation-changed') def replica_set_relation_changed(): private_address = unit_get('private-address') remote_hostname = relation_get('hostname') @@ -1228,7 +1231,7 @@ def replica_set_relation_changed(): juju_log('replica_set_relation_changed-finish') -@hooks.hook('replicaset-relation-departed') +@hooks.hook('replica-set-relation-departed') def replica_set_relation_departed(): juju_log('replica_set_relation_departed-start') @@ -1259,7 +1262,7 @@ def replica_set_relation_departed(): juju_log('replica_set_relation_departed-finish') -@hooks.hook('replicaset-relation-broken') +@hooks.hook('replica-set-relation-broken') def replica_set_relation_broken(): juju_log('replica_set_relation_broken-start') @@ -1420,15 +1423,28 @@ def update_nrpe_config(): else: current_unit = local_unit() + if lsb_release()['DISTRIB_RELEASE'] > '15.04': + check_mongo_script='check_systemd.py mongodb' + else: + check_mongo_script='check_upstart_job mongodb' + nrpe.add_check( shortname='mongodb', description='process check {%s}' % current_unit, - check_cmd='check_upstart_job mongodb', + check_cmd=check_mongo_script, ) nrpe.write() +@hooks.hook('upgrade-charm') +def uprade_charm(): + juju_log('upgrade-charm: removing --replset from upstart script') + remove_replset_from_upstart() + juju_log('upgrade-charm: removing --rest from upstart script') + remove_rest_from_upstart() + + def run(command, exit_on_error=True): '''Run a command and return the output.''' try: @@ -1663,7 +1679,7 @@ def write_logrotate_config(config_data, copytruncate delaycompress compress - noifempty + notifempty missingok }}""") contents = contents.format(**config_data) diff --git a/hooks/install b/hooks/install index 83a9d3c..8712bfa 100755 --- a/hooks/install +++ b/hooks/install @@ -4,9 +4,14 @@ declare -a DEPS=('apt' 'netaddr' 'netifaces' 'pip' 'yaml') +wait_for_dpkg_unlock() { + while $(fuser -s /var/lib/dpkg/lock); do sleep .5;done +} + check_and_install() { pkg="${1}-${2}" if ! dpkg -s ${pkg} 2>&1 > /dev/null; then + wait_for_dpkg_unlock apt-get -y install ${pkg} fi } diff --git a/hooks/upgrade-charm b/hooks/upgrade-charm new file mode 120000 index 0000000..9416ca6 --- /dev/null +++ b/hooks/upgrade-charm @@ -0,0 +1 @@ +hooks.py \ No newline at end of file diff --git a/tests/01_deploy_single.py b/tests/01_deploy_single.py deleted file mode 100755 index 885ee26..0000000 --- a/tests/01_deploy_single.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 - -import amulet -from pymongo import MongoClient - -seconds = 900 - -d = amulet.Deployment(series='trusty') -d.add('mongodb', charm='mongodb') -d.expose('mongodb') - -# Perform the setup for the deployment. -try: - d.setup(seconds) - d.sentry.wait(seconds) -except amulet.helpers.TimeoutError: - message = 'The environment did not setup in %d seconds.', seconds - amulet.raise_status(amulet.SKIP, msg=message) -except: - raise - - -############################################################ -# Validate connectivity from $WORLD -############################################################# -def validate_world_connectivity(): - addy = d.sentry['mongodb'][0].info['public-address'] - if ":" in addy: - addy = "[{}]".format(addy) - client = MongoClient(addy) - - db = client['test'] - # Can we successfully insert? - insert_id = db.amulet.insert({'assert': True}) - if insert_id is None: - amulet.raise_status(amulet.FAIL, msg="Failed to insert test data") - # Can we delete from a shard using the Mongos hub? - result = db.amulet.remove(insert_id) - if result['err'] is not None: - amulet.raise_status(amulet.FAIL, msg="Failed to remove test data") - - -validate_world_connectivity() diff --git a/tests/02_deploy_shard_test.py b/tests/02_deploy_shard_test.py deleted file mode 100755 index 2ff0750..0000000 --- a/tests/02_deploy_shard_test.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 - -import amulet -import requests -import sys -import time -import traceback -from pymongo import MongoClient - -######################################################### -# Test Quick Config -######################################################### -scale = 1 -seconds = 1400 - -######################################################### -# 3shard cluster configuration -######################################################### -d = amulet.Deployment(series='trusty') - -d.add('configsvr', charm='mongodb', units=scale) -d.add('mongos', charm='mongodb', units=scale) -d.add('shard1', charm='mongodb', units=scale) -d.add('shard2', charm='mongodb', units=scale) - -# Setup the config svr -d.configure('configsvr', {'replicaset': 'configsvr'}) - -# define each shardset -d.configure('shard1', {'replicaset': 'shard1'}) -d.configure('shard2', {'replicaset': 'shard2'}) - -d.configure('mongos', {}) - -# Connect the config servers to mongo shell -d.relate('configsvr:configsvr', 'mongos:mongos-cfg') - -# connect each shard to the mongo shell -d.relate('mongos:mongos', 'shard1:database') -d.relate('mongos:mongos', 'shard2:database') -d.expose('configsvr') -d.expose('mongos') - -# Perform the setup for the deployment. -try: - d.setup(seconds) - d.sentry.wait(seconds) -except amulet.helpers.TimeoutError: - message = 'The environment did not setup in %d seconds.', seconds - amulet.raise_status(amulet.SKIP, msg=message) -except: - raise - -sentry_dict = { - 'config-sentry': d.sentry['configsvr'][0], - 'mongos-sentry': d.sentry['mongos'][0], - 'shard1-sentry': d.sentry['shard1'][0], - 'shard2-sentry': d.sentry['shard2'][0] -} - - -############################################################# -# Check presence of MongoDB GUI HEALTH Status -############################################################# -def validate_status_interface(): - pubaddy = sentry_dict['config-sentry'].info['public-address'] - fmt = "http://{}:28017" - if ":" in pubaddy: - fmt = "http://[{}]:28017" - time_between = 10 - tries = seconds / time_between - try: - r = requests.get(fmt.format(pubaddy), verify=False) - r.raise_for_status() - except requests.exception.ConnectionError as ex: - sys.stderr.write( - 'Connection error, sleep and retry... to {}: {}\n'. - format(pubaddy, ex)) - tb_lines = traceback.format_exception(ex.__class__, - ex, ex.__traceback__) - tb_text = ''.join(tb_lines) - sys.stderr.write(tb_text) - tries = tries - 1 - if tries < 0: - sys.stderr.write('retry limit caught, failing...\n') - time.sleep(time_between) - - -############################################################# -# Validate that each unit has an active mongo service -############################################################# -def validate_running_services(): - for service in sentry_dict: - output = sentry_dict[service].run('service mongodb status') - service_active = str(output).find('mongodb start/running') - if service_active == -1: - message = "Failed to find running MongoDB on host {}".format( - service) - amulet.raise_status(amulet.SKIP, msg=message) - - -############################################################# -# Validate connectivity from $WORLD -############################################################# -def validate_world_connectivity(): - pubaddy = d.sentry['mongos'][0].info['public-address'] - if ":" in pubaddy: - pubaddy = "[{}]".format(pubaddy) - client = MongoClient(pubaddy) - - db = client['test'] - # Can we successfully insert? - insert_id = db.amulet.insert({'assert': True}) - if insert_id is None: - amulet.raise_status(amulet.FAIL, msg="Failed to insert test data") - # Can we delete from a shard using the Mongos hub? - result = db.amulet.remove(insert_id) - if result['err'] is not None: - amulet.raise_status(amulet.FAIL, msg="Failed to remove test data") - - -############################################################# -# Validate relationships -############################################################# -# broken pending 1273312 -def validate_relationships(): - d.sentry['configsvr'][0].relation('configsvr', 'mongos:mongos-cfg') - d.sentry['shard1'][0].relation('database', 'mongos:mongos') - d.sentry['shard2'][0].relation('database', 'mongos:mongos') - print(d.sentry['shard1'][0].relation('database', 'mongos:mongos')) - - -def validate_manual_connection(): - fmt = "mongo {}" - addy = d.sentry['mongos'][0].info['public-address'] - if ":" in addy: - fmt = "mongo --ipv6 {}:27017" - jujuruncmd = fmt.format(addy) - output, code = d.sentry['shard1'][0].run(jujuruncmd) - if code != 0: - message = ("Manual Connection failed for unit shard1:{} code:{} cmd:{}" - .format(output, code, jujuruncmd)) - amulet.raise_status(amulet.SKIP, msg=message) - - output, code = d.sentry['shard2'][0].run(jujuruncmd) - if code != 0: - message = ("Manual Connection failed for unit shard2:{} code:{} cmd:{}" - .format(output, code, jujuruncmd)) - amulet.raise_status(amulet.SKIP, msg=message) - - -validate_status_interface() -validate_running_services() -validate_manual_connection() -validate_world_connectivity() diff --git a/tests/03_deploy_replicaset.py b/tests/03_deploy_replicaset.py deleted file mode 100755 index 7add444..0000000 --- a/tests/03_deploy_replicaset.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 - -import amulet -import sys -import time -import traceback -import requests -import pymongo -from pymongo import MongoClient -from collections import Counter - - -######################################################### -# Test Quick Config -######################################################### -scale = 3 -seconds = 1800 - -# max amount of time to wait before testing for replicaset -# status -wait_for_replicaset = 600 - -######################################################### -# 3shard cluster configuration -######################################################### -d = amulet.Deployment(series='trusty') - -d.add('mongodb', charm='mongodb', units=scale) -d.expose('mongodb') - -# Perform the setup for the deployment. -try: - d.setup(seconds) - d.sentry.wait(seconds) -except amulet.helpers.TimeoutError: - message = 'The environment did not setup in %d seconds.', seconds - amulet.raise_status(amulet.SKIP, msg=message) -except: - raise - -sentry_dict = { - 'mongodb0-sentry': d.sentry['mongodb'][0], - 'mongodb1-sentry': d.sentry['mongodb'][1], - 'mongodb2-sentry': d.sentry['mongodb'][2], -} - - -############################################################# -# Test Utilities -############################################################# -def _expect_replicaset_counts(primaries_count, - secondaries_count, - time_between=10): - unit_status = [] - tries = wait_for_replicaset / time_between - - for service in sentry_dict: - addy = sentry_dict[service].info['public-address'] - if ":" in addy: - addy = "[{}]".format(addy) - while True: - try: - client = MongoClient(addy) - r = client.admin.command('replSetGetStatus') - break - except pymongo.errors.OperationFailure as ex: - sys.stderr.write( - 'OperationFailure, sleep and retry... to {}: {}\n'. - format(addy, ex)) - tb_lines = traceback.format_exception(ex.__class__, - ex, ex.__traceback__) - tb_text = ''.join(tb_lines) - sys.stderr.write(tb_text) - tries = tries - 1 - if tries < 0: - sys.stderr.write('retry limit caught, failing...\n') - break - time.sleep(time_between) - unit_status.append(r['myState']) - client.close() - - primaries = Counter(unit_status)[1] - if primaries != primaries_count: - message = "Expected %d PRIMARY unit(s)! Found: %s %s" % ( - primaries_count, - primaries, - unit_status) - amulet.raise_status(amulet.FAIL, message) - - secondrs = Counter(unit_status)[2] - if secondrs != secondaries_count: - message = ("Expected %d secondary units! (Found %s) %s" % - (secondaries_count, secondrs, unit_status)) - amulet.raise_status(amulet.FAIL, message) - - -############################################################# -# Check presence of MongoDB GUI HEALTH Status -############################################################# -def validate_status_interface(): - pubaddy = d.sentry['mongodb'][0].info['public-address'] - fmt = "http://{}:28017" - if ":" in pubaddy: - fmt = "http://[{}]:28017" - r = requests.get(fmt.format(pubaddy), verify=False) - r.raise_for_status - - -############################################################# -# Validate that each unit has an active mongo service -############################################################# -def validate_running_services(): - for service in sentry_dict: - output = sentry_dict[service].run('service mongodb status') - service_active = str(output).find('mongodb start/running') - if service_active == -1: - message = "Failed to find running MongoDB on host {}".format( - service) - amulet.raise_status(amulet.SKIP, msg=message) - - -############################################################# -# Validate proper replicaset setup -############################################################# -def validate_replicaset_setup(): - d.sentry.wait(seconds) - _expect_replicaset_counts(1, 2) - - -############################################################# -# Validate replicaset joined -############################################################# -def validate_replicaset_relation_joined(): - d.add_unit('mongodb', units=2) - d.sentry.wait(wait_for_replicaset) - sentry_dict.update({'mongodb3-sentry': d.sentry['mongodb'][3], - 'mongodb4-sentry': d.sentry['mongodb'][4]}) - _expect_replicaset_counts(1, 4) - - -############################################################# -# Validate connectivity from $WORLD -############################################################# -def validate_world_connectivity(): - d.sentry['mongodb'] - ordered_units = sorted(d.sentry['mongodb'], key=lambda u: u.info['unit']) - # We assume minimum unit number is master. - addy = ordered_units[0].info['public-address'] - if ":" in addy: - addy = "[{}]".format(addy) - - time_between = 10 - tries = wait_for_replicaset / time_between - insert_id = None - while True: - try: - client = MongoClient(addy) - db = client.test - # Can we successfully insert? - insert_id = db.amulet.insert({'assert': True}) - break - except pymongo.errors.AutoReconnect as ex: - sys.stderr.write( - 'AutoReconnect error, sleep and retry... to {}: {}\n'. - format(addy, ex)) - tb_lines = traceback.format_exception(ex.__class__, - ex, ex.__traceback__) - tb_text = ''.join(tb_lines) - sys.stderr.write(tb_text) - tries = tries - 1 - if tries < 0: - sys.stderr.write('retry limit caught, failing...\n') - break - time.sleep(time_between) - - if insert_id is None: - amulet.raise_status(amulet.FAIL, msg="Failed to insert test data") - # Can we delete from a shard using the Mongos hub? - result = db.amulet.remove(insert_id) - if result['err'] is not None: - amulet.raise_status(amulet.FAIL, msg="Failed to remove test data") - - -validate_status_interface() -validate_running_services() -validate_replicaset_setup() -validate_replicaset_relation_joined() -validate_world_connectivity() diff --git a/tests/04_deploy_with_storage.py b/tests/04_deploy_with_storage.py deleted file mode 100755 index 46aef57..0000000 --- a/tests/04_deploy_with_storage.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 - -import amulet -from pymongo import MongoClient -from collections import Counter - - -######################################################### -# Test Quick Config -######################################################### -scale = 2 -seconds = 1800 - -# amount of time to wait before testing for replicaset -# status -wait_for_replicaset = 60*5 -# amount of time to wait for the data relation -wait_for_relation = 60*5 - -######################################################### -# 3shard cluster configuration -######################################################### -d = amulet.Deployment(series='trusty') - -d.add('mongodb', units=scale, series='trusty', - constraints={'root-disk': '20480M'}) -d.add('storage', charm='cs:~chris-gondolin/trusty/storage-5', series='trusty') -d.configure('storage', {'provider': 'local'}) - -d.expose('mongodb') - -# Perform the setup for the deployment. -try: - d.setup(seconds) - d.sentry.wait(wait_for_replicaset) -except amulet.helpers.TimeoutError: - message = 'The environment did not setup in %d seconds.', seconds - amulet.raise_status(amulet.SKIP, msg=message) -except: - raise - -ordered_units = sorted(d.sentry['mongodb'], key=lambda u: u.info['unit']) -sentry_dict = { - 'mongodb0-sentry': ordered_units[0], - 'mongodb1-sentry': ordered_units[1] -} - - -############################################################# -# Check agent status -############################################################# -def validate_status(): - d.sentry.wait_for_status(d.juju_env, ['mongodb']) - - -############################################################# -# Validate proper replicaset setup -############################################################# -def validate_replicaset_setup(): - - d.sentry.wait(seconds) - - unit_status = [] - - for service in sentry_dict: - addy = sentry_dict[service].info['public-address'] - if ":" in addy: - addy = "[{}]".format(addy) - client = MongoClient(addy) - r = client.admin.command('replSetGetStatus') - unit_status.append(r['myState']) - client.close() - - primaries = Counter(unit_status)[1] - if primaries != 1: - message = "Only one PRIMARY unit allowed! Found: %s" % (primaries) - amulet.raise_status(amulet.FAIL, message) - - secondrs = Counter(unit_status)[2] - if secondrs != 1: - message = "Only one SECONDARY unit allowed! (Found %s)" % (secondrs) - amulet.raise_status(amulet.FAIL, message) - - -validate_status() -validate_replicaset_setup() -print("Adding storage relation, and sleeping for 2 min.") -try: - d.relate('mongodb:data', 'storage:data') -except OSError as e: - print("ignoring error:{}", e) -d.sentry.wait(wait_for_relation) -validate_status() -validate_replicaset_setup() diff --git a/tests/50_relate_ceilometer_test.py b/tests/50_relate_ceilometer_test.py deleted file mode 100755 index ef45166..0000000 --- a/tests/50_relate_ceilometer_test.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 - -import amulet - - -class TestDeploy(object): - - def __init__(self, time=2500): - # Attempt to load the deployment topology from a bundle. - self.deploy = amulet.Deployment(series="trusty") - - # If something errored out, attempt to continue by - # manually specifying a standalone deployment - self.deploy.add('mongodb') - self.deploy.add('ceilometer', 'cs:trusty/ceilometer') - # send blank configs to finalize the objects in the deployment map - self.deploy.configure('mongodb', {}) - self.deploy.configure('ceilometer', {}) - - self.deploy.relate('mongodb:database', 'ceilometer:shared-db') - - try: - self.deploy.setup(time) - self.deploy.sentry.wait(time) - except: - amulet.raise_status(amulet.FAIL, msg="Environment standup timeout") - # sentry = self.deploy.sentry - - def run(self): - for test in dir(self): - if test.startswith('test_'): - getattr(self, test)() - - def test_mongo_relation(self): - unit = self.deploy.sentry['ceilometer'][0] - mongo = self.deploy.sentry['mongodb'][0].info['public-address'] - mongo_reladdr = self.deploy.sentry['mongodb'][0].relation( - 'database', 'ceilometer:shared-db') - cont = unit.file_contents('/etc/ceilometer/ceilometer.conf') - if (mongo not in cont and mongo_reladdr.get( - 'hostname', 'I SURE HOPE NOT') not in cont): - amulet.raise_status(amulet.FAIL, "Unable to verify ceilometer cfg") - -if __name__ == '__main__': - runner = TestDeploy() - runner.run() diff --git a/tests/base_deploy.py b/tests/base_deploy.py new file mode 100644 index 0000000..8930966 --- /dev/null +++ b/tests/base_deploy.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +import amulet +import requests +import sys +import time +import traceback +from pymongo import MongoClient + + +class BasicMongo(object): + def __init__(self, units, series, deploy_timeout): + self.units = units + self.series = series + self.deploy_timeout = deploy_timeout + self.d = amulet.Deployment(series=self.series) + self.addy = None + + def deploy(self): + try: + self.d.setup(self.deploy_timeout) + self.d.sentry.wait(self.deploy_timeout) + except amulet.helpers.TimeoutError: + message = 'The environment did not setup in %d seconds.', + self.deploy_timeout + amulet.raise_status(amulet.SKIP, msg=message) + except: + raise + + self.sentry_dict = {svc: self.d.sentry[svc] + for svc in list(self.d.sentry.unit)} + + def validate_status_interface(self): + addy = self.addy + fmt = "http://{}:28017" + if ":" in addy: + fmt = "http://[{}]:28017" + + time_between = 10 + tries = self.deploy_timeout / time_between + + try: + r = requests.get(fmt.format(addy), verify=False) + r.raise_for_status() + except requests.exception.ConnectionError as ex: + sys.stderr.write( + 'Connection error, sleep and retry... to {}: {}\n'. + format(addy, ex)) + tb_lines = traceback.format_exception(ex.__class__, + ex, ex.__traceback__) + tb_text = ''.join(tb_lines) + sys.stderr.write(tb_text) + tries = tries - 1 + if tries < 0: + sys.stderr.write('retry limit caught, failing...\n') + time.sleep(time_between) + + def validate_world_connectivity(self): + addy = self.addy + # ipv6 proper formating + if ":" in addy: + addy = "[{}]".format(addy) + + client = MongoClient(addy) + db = client['test'] + + # Can we successfully insert? + insert_id = db.amulet.insert({'assert': True}) + if insert_id is None: + amulet.raise_status(amulet.FAIL, msg="Failed to insert test data") + + # Can we delete from a shard using the Mongos hub? + result = db.amulet.remove(insert_id) + if 'err' in result and result['err'] is not None: + amulet.raise_status(amulet.FAIL, msg="Failed to remove test data") + + def validate_running_services(self): + for service in self.sentry_dict: + grep_command = 'grep RELEASE /etc/lsb-release' + release = self.sentry_dict[service].run(grep_command) + release = str(release).split('=')[1] + if release >= '15.10': + status_string = 'active (running)' # systemd + else: + status_string = 'mongodb start/running' # upstart + + output = self.sentry_dict[service].run('service mongodb status') + service_active = str(output).find(status_string) + if service_active == -1: + message = "Failed to find running MongoDB on host {}".format( + service) + amulet.raise_status(amulet.SKIP, msg=message) diff --git a/tests/deploy_replicaset-trusty b/tests/deploy_replicaset-trusty new file mode 100755 index 0000000..9ed77a8 --- /dev/null +++ b/tests/deploy_replicaset-trusty @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import deploy_replicaset + +t = deploy_replicaset.Replicaset('trusty') +t.run() \ No newline at end of file diff --git a/tests/deploy_replicaset-xenial b/tests/deploy_replicaset-xenial new file mode 100755 index 0000000..4f142dd --- /dev/null +++ b/tests/deploy_replicaset-xenial @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import deploy_replicaset + +t = deploy_replicaset.Replicaset('xenial') +t.run() diff --git a/tests/deploy_replicaset.py b/tests/deploy_replicaset.py new file mode 100644 index 0000000..9506407 --- /dev/null +++ b/tests/deploy_replicaset.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +import amulet +import sys +import time +import traceback +from pymongo import MongoClient +from pymongo.errors import OperationFailure +from collections import Counter + +from base_deploy import BasicMongo + +# max amount of time to wait before testing for replicaset status +wait_for_replicaset = 600 + + +class Replicaset(BasicMongo): + def __init__(self, series): + super(Replicaset, self).__init__(units=3, + series=series, + deploy_timeout=1800) + + def _expect_replicaset_counts(self, + primaries_count, + secondaries_count, + time_between=10): + unit_status = [] + tries = wait_for_replicaset / time_between + + for service in self.sentry_dict: + addy = self.sentry_dict[service].info['public-address'] + if ":" in addy: + addy = "[{}]".format(addy) + while True: + try: + client = MongoClient(addy) + r = client.admin.command('replSetGetStatus') + break + except OperationFailure as ex: + sys.stderr.write( + 'OperationFailure, sleep and retry... to {}: {}\n'. + format(addy, ex)) + tb_lines = traceback.format_exception(ex.__class__, + ex, ex.__traceback__) + tb_text = ''.join(tb_lines) + sys.stderr.write(tb_text) + tries = tries - 1 + if tries < 0: + sys.stderr.write('retry limit caught, failing...\n') + break + time.sleep(time_between) + unit_status.append(r['myState']) + client.close() + + primaries = Counter(unit_status)[1] + if primaries != primaries_count: + message = "Expected %d PRIMARY unit(s)! Found: %s %s" % ( + primaries_count, + primaries, + unit_status) + amulet.raise_status(amulet.FAIL, message) + + secondrs = Counter(unit_status)[2] + if secondrs != secondaries_count: + message = ("Expected %d secondary units! (Found %s) %s" % + (secondaries_count, secondrs, unit_status)) + amulet.raise_status(amulet.FAIL, message) + + def deploy(self): + self.d.add('mongodb', charm='mongodb', units=self.units) + self.d.expose('mongodb') + super(Replicaset, self).deploy() + self.wait_for_replicaset = 600 + + def validate_status_interface(self): + self.addy = self.d.sentry['mongodb'][0].info['public-address'] + super(Replicaset, self).validate_status_interface() + + def validate_replicaset_setup(self): + self.d.sentry.wait(self.deploy_timeout) + self._expect_replicaset_counts(1, 2) + + def validate_replicaset_relation_joined(self): + self.d.add_unit('mongodb', units=2) + self.d.sentry.wait(wait_for_replicaset) + self.sentry_dict = {svc: self.d.sentry[svc] + for svc in list(self.d.sentry.unit)} + self._expect_replicaset_counts(1, 4) + + def validate_world_connectivity(self): + # figuring out which unit is primary + primary = False + while not primary: + for unit in self.sentry_dict: + unit_address = self.sentry_dict[unit].info['public-address'] + if ":" in unit_address: + unit_address = "[{}]".format(unit_address) + c = MongoClient(unit_address) + r = c.admin.command('replSetGetStatus') + if r['myState'] == 1: + # reusing address without possible brackets [] + primary = self.sentry_dict[unit].info['public-address'] + break + time.sleep(.1) + + self.addy = primary + super(Replicaset, self).validate_world_connectivity() + + def validate_running_services(self): + super(Replicaset, self).validate_running_services() + + def run(self): + self.deploy() + self.validate_status_interface() + self.validate_running_services() + self.validate_replicaset_setup() + self.validate_replicaset_relation_joined() + self.validate_world_connectivity() diff --git a/tests/deploy_shard-trusty b/tests/deploy_shard-trusty new file mode 100755 index 0000000..200110f --- /dev/null +++ b/tests/deploy_shard-trusty @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import deploy_shard + +t = deploy_shard.ShardNode('trusty') +t.run() \ No newline at end of file diff --git a/tests/deploy_shard-xenial b/tests/deploy_shard-xenial new file mode 100755 index 0000000..cb00363 --- /dev/null +++ b/tests/deploy_shard-xenial @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import deploy_shard + +t = deploy_shard.ShardNode('xenial') +t.run() diff --git a/tests/deploy_shard.py b/tests/deploy_shard.py new file mode 100644 index 0000000..d384ef4 --- /dev/null +++ b/tests/deploy_shard.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +import amulet + +from base_deploy import BasicMongo + + +class ShardNode(BasicMongo): + def __init__(self, series): + super(ShardNode, self).__init__(units=1, + series=series, + deploy_timeout=900) + + def deploy(self): + self.d.add('configsvr', charm='mongodb', units=self.units) + self.d.add('mongos', charm='mongodb', units=self.units) + self.d.add('shard1', charm='mongodb', units=self.units) + self.d.add('shard2', charm='mongodb', units=self.units) + + # Setup the config svr + self.d.configure('configsvr', {'replicaset': 'configsvr'}) + + # define each shardset + self.d.configure('shard1', {'replicaset': 'shard1'}) + self.d.configure('shard2', {'replicaset': 'shard2'}) + + self.d.configure('mongos', {}) + + # Connect the config servers to mongo shell + self.d.relate('configsvr:configsvr', 'mongos:mongos-cfg') + + # connect each shard to the mongo shell + self.d.relate('mongos:mongos', 'shard1:database') + self.d.relate('mongos:mongos', 'shard2:database') + self.d.expose('configsvr') + self.d.expose('mongos') + super(ShardNode, self).deploy() + + self.sentry_dict = { + 'config-sentry': self.d.sentry['configsvr'][0], + 'mongos-sentry': self.d.sentry['mongos'][0], + 'shard1-sentry': self.d.sentry['shard1'][0], + 'shard2-sentry': self.d.sentry['shard2'][0] + } + + def validate_world_connectivity(self): + self.addy = self.d.sentry['mongos'][0].info['public-address'] + super(ShardNode, self).validate_world_connectivity() + + def validate_running_services(self): + super(ShardNode, self).validate_running_services() + + def validate_status_interface(self): + self.addy = self.sentry_dict['config-sentry'].info['public-address'] + super(ShardNode, self).validate_status_interface() + + def validate_manual_connection(self): + fmt = "mongo {}" + addy = self.d.sentry['mongos'][0].info['public-address'] + if ":" in addy: + fmt = "mongo --ipv6 {}:27017" + jujuruncmd = fmt.format(addy) + output, code = self.d.sentry['shard1'][0].run(jujuruncmd) + if code != 0: + msg = ("Manual Connection failed, unit shard1:{} code:{} cmd:{}" + .format(output, code, jujuruncmd)) + amulet.raise_status(amulet.SKIP, msg=msg) + + output, code = self.d.sentry['shard2'][0].run(jujuruncmd) + if code != 0: + msg = ("Manual Connection failed, unit shard2:{} code:{} cmd:{}" + .format(output, code, jujuruncmd)) + amulet.raise_status(amulet.SKIP, msg=msg) + + def run(self): + self.deploy() + self.validate_world_connectivity() + self.validate_status_interface() + self.validate_running_services() + self.validate_manual_connection() diff --git a/tests/deploy_single-trusty b/tests/deploy_single-trusty new file mode 100755 index 0000000..59a2231 --- /dev/null +++ b/tests/deploy_single-trusty @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +import deploy_single + +t = deploy_single.SingleNode('trusty') +t.run() + + diff --git a/tests/deploy_single-xenial b/tests/deploy_single-xenial new file mode 100755 index 0000000..3f718d8 --- /dev/null +++ b/tests/deploy_single-xenial @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +import deploy_single + +t = deploy_single.SingleNode('xenial') +t.run() + + diff --git a/tests/deploy_single.py b/tests/deploy_single.py new file mode 100644 index 0000000..c3181b1 --- /dev/null +++ b/tests/deploy_single.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +from base_deploy import BasicMongo + + +class SingleNode(BasicMongo): + def __init__(self, series): + super(SingleNode, self).__init__(units=1, + series=series, + deploy_timeout=900) + + def deploy(self): + self.d.add('mongodb', charm='mongodb', units=self.units) + self.d.expose('mongodb') + super(SingleNode, self).deploy() + + def validate_world_connectivity(self): + self.addy = self.d.sentry['mongodb'][0].info['public-address'] + super(SingleNode, self).validate_world_connectivity() + + def run(self): + self.deploy() + self.validate_world_connectivity() diff --git a/tests/deploy_with_ceilometer-trusty b/tests/deploy_with_ceilometer-trusty new file mode 100755 index 0000000..b73d870 --- /dev/null +++ b/tests/deploy_with_ceilometer-trusty @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import deploy_with_ceilometer + +t = deploy_with_ceilometer.TestCeilometer('trusty') +t.run() diff --git a/tests/deploy_with_ceilometer-xenial b/tests/deploy_with_ceilometer-xenial new file mode 100755 index 0000000..4678457 --- /dev/null +++ b/tests/deploy_with_ceilometer-xenial @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +import deploy_with_ceilometer + +#Not running this because of: https://launchpad.net/bugs/1656651 +#t = deploy_with_ceilometer.TestCeilometer('xenial') +#t.run() diff --git a/tests/deploy_with_ceilometer.py b/tests/deploy_with_ceilometer.py new file mode 100644 index 0000000..133eee3 --- /dev/null +++ b/tests/deploy_with_ceilometer.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +import amulet + +from base_deploy import BasicMongo + + +class TestCeilometer(BasicMongo): + def __init__(self, series): + super(TestCeilometer, self).__init__(units=1, series=series, + deploy_timeout=900) + + def deploy(self): + self.d.add('mongodb', charm='mongodb', units=self.units) + self.d.add('ceilometer', 'cs:{}/ceilometer'.format(self.series)) + self.d.relate('mongodb:database', 'ceilometer:shared-db') + self.d.expose('mongodb') + super(TestCeilometer, self).deploy() + + def validate_world_connectivity(self): + self.addy = self.d.sentry['mongodb'][0].info['public-address'] + super(TestCeilometer, self).validate_world_connectivity() + + def validate_mongo_relation(self): + unit = self.d.sentry['ceilometer'][0] + mongo = self.d.sentry['mongodb'][0].info['public-address'] + mongo_reladdr = self.d.sentry['mongodb'][0].relation( + 'database', 'ceilometer:shared-db') + cont = unit.file_contents('/etc/ceilometer/ceilometer.conf') + if (mongo not in cont and mongo_reladdr.get( + 'hostname', 'I SURE HOPE NOT') not in cont): + amulet.raise_status(amulet.FAIL, "Unable to verify ceilometer cfg") + + def run(self): + self.deploy() + self.validate_world_connectivity() diff --git a/tests/deploy_with_storage-trusty b/tests/deploy_with_storage-trusty new file mode 100755 index 0000000..f7e8314 --- /dev/null +++ b/tests/deploy_with_storage-trusty @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import deploy_with_storage + +t = deploy_with_storage.WithStorage('trusty') +t.run() diff --git a/tests/deploy_with_storage-xenial b/tests/deploy_with_storage-xenial new file mode 100755 index 0000000..6b0d660 --- /dev/null +++ b/tests/deploy_with_storage-xenial @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +import deploy_with_storage + +# We are not testing this against xenial yet, because +# cs:~chris-gondolin/trusty/storage-5 does not exist for xenial (yet) +#t = deploy_with_storage.WithStorage('xenial') +#t.run() diff --git a/tests/deploy_with_storage.py b/tests/deploy_with_storage.py new file mode 100644 index 0000000..92a5fe6 --- /dev/null +++ b/tests/deploy_with_storage.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +from base_deploy import BasicMongo + +import amulet +from pymongo import MongoClient +from collections import Counter + + +class WithStorage(BasicMongo): + def __init__(self, series): + super(WithStorage, self).__init__(units=2, + series=series, + deploy_timeout=1800) + + def deploy(self): + self.d.add('mongodb', + charm='mongodb', + units=self.units, + constraints={'root-disk': '20480M'}) + + storage_charm = 'cs:~chris-gondolin/{}/storage-5'.format(self.series) + self.d.add('storage', charm=storage_charm, series=self.series) + self.d.configure('storage', {'provider': 'local'}) + super(WithStorage, self).deploy() + self.d.expose('mongodb') + + ordered_units = sorted(self.d.sentry['mongodb'], + key=lambda u: u.info['unit']) + self.sentry_dict = { + 'mongodb0-sentry': ordered_units[0], + 'mongodb1-sentry': ordered_units[1] + } + + def validate_status(self): + self.d.sentry.wait_for_status(self.d.juju_env, ['mongodb']) + + def validate_replicaset_setup(self): + self.d.sentry.wait(self.deploy_timeout) + + unit_status = [] + for service in self.sentry_dict: + addy = self.sentry_dict[service].info['public-address'] + if ":" in addy: + addy = "[{}]".format(addy) + client = MongoClient(addy) + r = client.admin.command('replSetGetStatus') + unit_status.append(r['myState']) + client.close() + + prims = Counter(unit_status)[1] + if prims != 1: + message = "Only one PRIMARY unit allowed! Found: %s" % (prims) + amulet.raise_status(amulet.FAIL, message) + + secnds = Counter(unit_status)[2] + if secnds != 1: + message = "Only one SECONDARY unit allowed! (Found %s)" % (secnds) + amulet.raise_status(amulet.FAIL, message) + + def run(self): + self.deploy() + self.validate_status() + self.validate_replicaset_setup() + + print("Adding storage relation, and sleeping for 2 min.") + try: + self.d.relate('mongodb:data', 'storage:data') + except OSError as e: + print("Ignoring error: {}", e) + self.d.sentry.wait(120) # 2 minute + + self.validate_status() + self.validate_replicaset_setup() diff --git a/unit_tests/test_hooks.py b/unit_tests/test_hooks.py index 4573b2c..aad1a96 100644 --- a/unit_tests/test_hooks.py +++ b/unit_tests/test_hooks.py @@ -3,9 +3,13 @@ from mock import patch, call import hooks from test_utils import CharmTestCase +from test_utils import mock_open from pymongo.errors import OperationFailure from subprocess import CalledProcessError +import tempfile +import os + # Defines a set of functions to patch on the hooks object. Any of these # methods will be patched by default on the default invocations of the # hooks.some_func(). Invoking the the interface change relations will cause @@ -339,3 +343,43 @@ class MongoHooksTest(CharmTestCase): call1 = call('juju-local:27017', 'juju-remote:27017') mock_leave_replset.assert_has_calls([call1]) + + def test_get_current_mongo_config(self): + test_config = u""" + # Comment + key = value + one=two + + three =four + """ + expected = { + 'key': 'value', + 'one': 'two', + 'three': 'four' + } + with mock_open('/etc/mongodb.conf', test_config): + results = hooks.get_current_mongo_config() + self.assertEqual(results, expected) + + def test_remove_replset_from_upstart(self): + test_contents = u""" +--exec /usr/bin/mongod -- --replSet myset --rest --config /etc/mongodb.conf + """ + expected = u""" +--exec /usr/bin/mongod -- --rest --config /etc/mongodb.conf + """ + + mocked_upstart = tempfile.NamedTemporaryFile(delete=False) + hooks.default_mongodb_init_config = mocked_upstart.name + + try: + mocked_upstart.write(test_contents) + mocked_upstart.close() + + hooks.remove_replset_from_upstart() + + with open(hooks.default_mongodb_init_config) as changed_upstart: + changed_contents = changed_upstart.read() + self.assertEqual(changed_contents, expected) + finally: + os.unlink(mocked_upstart.name) |
