summaryrefslogtreecommitdiff
diff options
authorJorge Niedbalski <jorge.niedbalski@canonical.com>2017-02-10 17:35:18 -0300
committerJorge Niedbalski <jorge.niedbalski@canonical.com>2017-02-10 17:35:18 -0300
commit1de20087e562f7ddbe1811dd586f324fa6f83d2f (patch)
treeb10a69cd7e9258a7e3d9ed2ded84a4e8fb9e4864
parent0b7b328a92c521d6115019b1ea02108cd7ca4ecb (diff)
parent21a40c7ab606fd254183a9a70ca0ecc099076ea9 (diff)
[mario-splivalo, r=niedbalski, billy-olsen] Fixes LP: #1575534 and LP:#1513094
-rw-r--r--actions/backup_test.py1
-rwxr-xr-xactions/perf1
-rw-r--r--charm-helpers-sync.yaml1
-rw-r--r--charmhelpers/__init__.py20
-rw-r--r--charmhelpers/contrib/__init__.py20
-rw-r--r--charmhelpers/contrib/charmsupport/__init__.py20
-rw-r--r--charmhelpers/contrib/charmsupport/nrpe.py62
-rw-r--r--charmhelpers/contrib/charmsupport/volumes.py20
-rw-r--r--charmhelpers/contrib/hahelpers/__init__.py20
-rw-r--r--charmhelpers/contrib/hahelpers/cluster.py89
-rw-r--r--charmhelpers/contrib/python/__init__.py20
-rw-r--r--charmhelpers/contrib/python/packages.py26
-rw-r--r--charmhelpers/core/__init__.py20
-rw-r--r--charmhelpers/core/decorators.py20
-rw-r--r--charmhelpers/core/files.py20
-rw-r--r--charmhelpers/core/fstab.py20
-rw-r--r--charmhelpers/core/hookenv.py52
-rw-r--r--charmhelpers/core/host.py162
-rw-r--r--charmhelpers/core/host_factory/__init__.py0
-rw-r--r--charmhelpers/core/host_factory/centos.py56
-rw-r--r--charmhelpers/core/host_factory/ubuntu.py56
-rw-r--r--charmhelpers/core/hugepage.py20
-rw-r--r--charmhelpers/core/kernel.py56
-rw-r--r--charmhelpers/core/kernel_factory/__init__.py0
-rw-r--r--charmhelpers/core/kernel_factory/centos.py17
-rw-r--r--charmhelpers/core/kernel_factory/ubuntu.py13
-rw-r--r--charmhelpers/core/services/__init__.py20
-rw-r--r--charmhelpers/core/services/base.py20
-rw-r--r--charmhelpers/core/services/helpers.py20
-rw-r--r--charmhelpers/core/strutils.py20
-rw-r--r--charmhelpers/core/sysctl.py20
-rw-r--r--charmhelpers/core/templating.py31
-rw-r--r--charmhelpers/core/unitdata.py21
-rw-r--r--charmhelpers/fetch/__init__.py351
-rw-r--r--charmhelpers/fetch/archiveurl.py20
-rw-r--r--charmhelpers/fetch/bzrurl.py50
-rw-r--r--charmhelpers/fetch/centos.py171
-rw-r--r--charmhelpers/fetch/giturl.py27
-rw-r--r--charmhelpers/fetch/ubuntu.py344
-rw-r--r--charmhelpers/osplatform.py19
-rw-r--r--charmhelpers/payload/__init__.py20
-rw-r--r--charmhelpers/payload/execd.py25
-rwxr-xr-xhooks/hooks.py186
-rwxr-xr-xhooks/install5
l---------hooks/upgrade-charm1
-rwxr-xr-xtests/01_deploy_single.py43
-rwxr-xr-xtests/02_deploy_shard_test.py155
-rwxr-xr-xtests/03_deploy_replicaset.py188
-rwxr-xr-xtests/04_deploy_with_storage.py94
-rwxr-xr-xtests/50_relate_ceilometer_test.py46
-rw-r--r--tests/base_deploy.py92
-rwxr-xr-xtests/deploy_replicaset-trusty6
-rwxr-xr-xtests/deploy_replicaset-xenial6
-rw-r--r--tests/deploy_replicaset.py118
-rwxr-xr-xtests/deploy_shard-trusty6
-rwxr-xr-xtests/deploy_shard-xenial6
-rw-r--r--tests/deploy_shard.py80
-rwxr-xr-xtests/deploy_single-trusty8
-rwxr-xr-xtests/deploy_single-xenial8
-rw-r--r--tests/deploy_single.py23
-rwxr-xr-xtests/deploy_with_ceilometer-trusty6
-rwxr-xr-xtests/deploy_with_ceilometer-xenial7
-rw-r--r--tests/deploy_with_ceilometer.py36
-rwxr-xr-xtests/deploy_with_storage-trusty6
-rwxr-xr-xtests/deploy_with_storage-xenial8
-rw-r--r--tests/deploy_with_storage.py74
-rw-r--r--unit_tests/test_hooks.py44
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)