summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Ames <david.ames@canonical.com>2018-04-11 14:26:49 -0700
committerDavid Ames <david.ames@canonical.com>2018-04-11 14:26:49 -0700
commit794b8ed0bb9f908ca7c12362cdcba6c3b24f5dc8 (patch)
tree8c9a4c26186491586167d6be2bbf5090500e5e5d
parentdc4f54a6e3e34047cf831ba35a04283ca2f96fe7 (diff)
Charm-helpers sync to fix CA cert comparison
The comparison of bytes vs string of the CA certificate produces a false negative. This leads to rewriting certificates and affecting connectivity to services. Read in the certificate as bytes as well for a bytes vs bytes comparison. Closes-Bug: #1762431 Change-Id: Ie2348a83671b9636bd94227e903b1a50bff7aecc
Notes
Notes (review): Verified+1: Canonical CI <uosci-testing-bot@ubuntu.com> Code-Review+2: Liam Young <liam.young@canonical.com> Workflow+1: Liam Young <liam.young@canonical.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Thu, 12 Apr 2018 08:54:46 +0000 Reviewed-on: https://review.openstack.org/560660 Project: openstack/charm-nova-cloud-controller Branch: refs/heads/master
-rw-r--r--hooks/charmhelpers/contrib/hahelpers/apache.py5
-rw-r--r--hooks/charmhelpers/contrib/hahelpers/cluster.py14
-rw-r--r--hooks/charmhelpers/contrib/openstack/amulet/deployment.py10
-rw-r--r--hooks/charmhelpers/contrib/openstack/amulet/utils.py225
-rw-r--r--hooks/charmhelpers/contrib/openstack/context.py1
-rw-r--r--hooks/charmhelpers/contrib/openstack/utils.py2
-rw-r--r--hooks/charmhelpers/contrib/storage/linux/lvm.py29
-rw-r--r--hooks/charmhelpers/core/hookenv.py86
-rw-r--r--hooks/charmhelpers/core/host.py11
-rw-r--r--hooks/charmhelpers/core/services/base.py21
-rw-r--r--hooks/charmhelpers/fetch/ubuntu.py1
11 files changed, 342 insertions, 63 deletions
diff --git a/hooks/charmhelpers/contrib/hahelpers/apache.py b/hooks/charmhelpers/contrib/hahelpers/apache.py
index 22acb68..605a1be 100644
--- a/hooks/charmhelpers/contrib/hahelpers/apache.py
+++ b/hooks/charmhelpers/contrib/hahelpers/apache.py
@@ -65,7 +65,8 @@ def get_ca_cert():
65 if ca_cert is None: 65 if ca_cert is None:
66 log("Inspecting identity-service relations for CA SSL certificate.", 66 log("Inspecting identity-service relations for CA SSL certificate.",
67 level=INFO) 67 level=INFO)
68 for r_id in relation_ids('identity-service'): 68 for r_id in (relation_ids('identity-service') +
69 relation_ids('identity-credentials')):
69 for unit in relation_list(r_id): 70 for unit in relation_list(r_id):
70 if ca_cert is None: 71 if ca_cert is None:
71 ca_cert = relation_get('ca_cert', 72 ca_cert = relation_get('ca_cert',
@@ -76,7 +77,7 @@ def get_ca_cert():
76def retrieve_ca_cert(cert_file): 77def retrieve_ca_cert(cert_file):
77 cert = None 78 cert = None
78 if os.path.isfile(cert_file): 79 if os.path.isfile(cert_file):
79 with open(cert_file, 'r') as crt: 80 with open(cert_file, 'rb') as crt:
80 cert = crt.read() 81 cert = crt.read()
81 return cert 82 return cert
82 83
diff --git a/hooks/charmhelpers/contrib/hahelpers/cluster.py b/hooks/charmhelpers/contrib/hahelpers/cluster.py
index 4207e42..47facd9 100644
--- a/hooks/charmhelpers/contrib/hahelpers/cluster.py
+++ b/hooks/charmhelpers/contrib/hahelpers/cluster.py
@@ -371,6 +371,7 @@ def distributed_wait(modulo=None, wait=None, operation_name='operation'):
371 ''' Distribute operations by waiting based on modulo_distribution 371 ''' Distribute operations by waiting based on modulo_distribution
372 372
373 If modulo and or wait are not set, check config_get for those values. 373 If modulo and or wait are not set, check config_get for those values.
374 If config values are not set, default to modulo=3 and wait=30.
374 375
375 :param modulo: int The modulo number creates the group distribution 376 :param modulo: int The modulo number creates the group distribution
376 :param wait: int The constant time wait value 377 :param wait: int The constant time wait value
@@ -382,10 +383,17 @@ def distributed_wait(modulo=None, wait=None, operation_name='operation'):
382 :side effect: Calls time.sleep() 383 :side effect: Calls time.sleep()
383 ''' 384 '''
384 if modulo is None: 385 if modulo is None:
385 modulo = config_get('modulo-nodes') 386 modulo = config_get('modulo-nodes') or 3
386 if wait is None: 387 if wait is None:
387 wait = config_get('known-wait') 388 wait = config_get('known-wait') or 30
388 calculated_wait = modulo_distribution(modulo=modulo, wait=wait) 389 if juju_is_leader():
390 # The leader should never wait
391 calculated_wait = 0
392 else:
393 # non_zero_wait=True guarantees the non-leader who gets modulo 0
394 # will still wait
395 calculated_wait = modulo_distribution(modulo=modulo, wait=wait,
396 non_zero_wait=True)
389 msg = "Waiting {} seconds for {} ...".format(calculated_wait, 397 msg = "Waiting {} seconds for {} ...".format(calculated_wait,
390 operation_name) 398 operation_name)
391 log(msg, DEBUG) 399 log(msg, DEBUG)
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
index 5afbbd8..66beeda 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
@@ -21,6 +21,9 @@ from collections import OrderedDict
21from charmhelpers.contrib.amulet.deployment import ( 21from charmhelpers.contrib.amulet.deployment import (
22 AmuletDeployment 22 AmuletDeployment
23) 23)
24from charmhelpers.contrib.openstack.amulet.utils import (
25 OPENSTACK_RELEASES_PAIRS
26)
24 27
25DEBUG = logging.DEBUG 28DEBUG = logging.DEBUG
26ERROR = logging.ERROR 29ERROR = logging.ERROR
@@ -271,11 +274,8 @@ class OpenStackAmuletDeployment(AmuletDeployment):
271 release. 274 release.
272 """ 275 """
273 # Must be ordered by OpenStack release (not by Ubuntu release): 276 # Must be ordered by OpenStack release (not by Ubuntu release):
274 (self.trusty_icehouse, self.trusty_kilo, self.trusty_liberty, 277 for i, os_pair in enumerate(OPENSTACK_RELEASES_PAIRS):
275 self.trusty_mitaka, self.xenial_mitaka, self.xenial_newton, 278 setattr(self, os_pair, i)
276 self.yakkety_newton, self.xenial_ocata, self.zesty_ocata,
277 self.xenial_pike, self.artful_pike, self.xenial_queens,
278 self.bionic_queens,) = range(13)
279 279
280 releases = { 280 releases = {
281 ('trusty', None): self.trusty_icehouse, 281 ('trusty', None): self.trusty_icehouse,
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/utils.py b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
index d93cff3..84e87f5 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
@@ -50,6 +50,13 @@ ERROR = logging.ERROR
50 50
51NOVA_CLIENT_VERSION = "2" 51NOVA_CLIENT_VERSION = "2"
52 52
53OPENSTACK_RELEASES_PAIRS = [
54 'trusty_icehouse', 'trusty_kilo', 'trusty_liberty',
55 'trusty_mitaka', 'xenial_mitaka', 'xenial_newton',
56 'yakkety_newton', 'xenial_ocata', 'zesty_ocata',
57 'xenial_pike', 'artful_pike', 'xenial_queens',
58 'bionic_queens']
59
53 60
54class OpenStackAmuletUtils(AmuletUtils): 61class OpenStackAmuletUtils(AmuletUtils):
55 """OpenStack amulet utilities. 62 """OpenStack amulet utilities.
@@ -63,7 +70,34 @@ class OpenStackAmuletUtils(AmuletUtils):
63 super(OpenStackAmuletUtils, self).__init__(log_level) 70 super(OpenStackAmuletUtils, self).__init__(log_level)
64 71
65 def validate_endpoint_data(self, endpoints, admin_port, internal_port, 72 def validate_endpoint_data(self, endpoints, admin_port, internal_port,
66 public_port, expected): 73 public_port, expected, openstack_release=None):
74 """Validate endpoint data. Pick the correct validator based on
75 OpenStack release. Expected data should be in the v2 format:
76 {
77 'id': id,
78 'region': region,
79 'adminurl': adminurl,
80 'internalurl': internalurl,
81 'publicurl': publicurl,
82 'service_id': service_id}
83
84 """
85 validation_function = self.validate_v2_endpoint_data
86 xenial_queens = OPENSTACK_RELEASES_PAIRS.index('xenial_queens')
87 if openstack_release and openstack_release >= xenial_queens:
88 validation_function = self.validate_v3_endpoint_data
89 expected = {
90 'id': expected['id'],
91 'region': expected['region'],
92 'region_id': 'RegionOne',
93 'url': self.valid_url,
94 'interface': self.not_null,
95 'service_id': expected['service_id']}
96 return validation_function(endpoints, admin_port, internal_port,
97 public_port, expected)
98
99 def validate_v2_endpoint_data(self, endpoints, admin_port, internal_port,
100 public_port, expected):
67 """Validate endpoint data. 101 """Validate endpoint data.
68 102
69 Validate actual endpoint data vs expected endpoint data. The ports 103 Validate actual endpoint data vs expected endpoint data. The ports
@@ -141,7 +175,86 @@ class OpenStackAmuletUtils(AmuletUtils):
141 if len(found) != expected_num_eps: 175 if len(found) != expected_num_eps:
142 return 'Unexpected number of endpoints found' 176 return 'Unexpected number of endpoints found'
143 177
144 def validate_svc_catalog_endpoint_data(self, expected, actual): 178 def convert_svc_catalog_endpoint_data_to_v3(self, ep_data):
179 """Convert v2 endpoint data into v3.
180
181 {
182 'service_name1': [
183 {
184 'adminURL': adminURL,
185 'id': id,
186 'region': region.
187 'publicURL': publicURL,
188 'internalURL': internalURL
189 }],
190 'service_name2': [
191 {
192 'adminURL': adminURL,
193 'id': id,
194 'region': region.
195 'publicURL': publicURL,
196 'internalURL': internalURL
197 }],
198 }
199 """
200 self.log.warn("Endpoint ID and Region ID validation is limited to not "
201 "null checks after v2 to v3 conversion")
202 for svc in ep_data.keys():
203 assert len(ep_data[svc]) == 1, "Unknown data format"
204 svc_ep_data = ep_data[svc][0]
205 ep_data[svc] = [
206 {
207 'url': svc_ep_data['adminURL'],
208 'interface': 'admin',
209 'region': svc_ep_data['region'],
210 'region_id': self.not_null,
211 'id': self.not_null},
212 {
213 'url': svc_ep_data['publicURL'],
214 'interface': 'public',
215 'region': svc_ep_data['region'],
216 'region_id': self.not_null,
217 'id': self.not_null},
218 {
219 'url': svc_ep_data['internalURL'],
220 'interface': 'internal',
221 'region': svc_ep_data['region'],
222 'region_id': self.not_null,
223 'id': self.not_null}]
224 return ep_data
225
226 def validate_svc_catalog_endpoint_data(self, expected, actual,
227 openstack_release=None):
228 """Validate service catalog endpoint data. Pick the correct validator
229 for the OpenStack version. Expected data should be in the v2 format:
230 {
231 'service_name1': [
232 {
233 'adminURL': adminURL,
234 'id': id,
235 'region': region.
236 'publicURL': publicURL,
237 'internalURL': internalURL
238 }],
239 'service_name2': [
240 {
241 'adminURL': adminURL,
242 'id': id,
243 'region': region.
244 'publicURL': publicURL,
245 'internalURL': internalURL
246 }],
247 }
248
249 """
250 validation_function = self.validate_v2_svc_catalog_endpoint_data
251 xenial_queens = OPENSTACK_RELEASES_PAIRS.index('xenial_queens')
252 if openstack_release and openstack_release >= xenial_queens:
253 validation_function = self.validate_v3_svc_catalog_endpoint_data
254 expected = self.convert_svc_catalog_endpoint_data_to_v3(expected)
255 return validation_function(expected, actual)
256
257 def validate_v2_svc_catalog_endpoint_data(self, expected, actual):
145 """Validate service catalog endpoint data. 258 """Validate service catalog endpoint data.
146 259
147 Validate a list of actual service catalog endpoints vs a list of 260 Validate a list of actual service catalog endpoints vs a list of
@@ -328,7 +441,7 @@ class OpenStackAmuletUtils(AmuletUtils):
328 if rel.get('api_version') != str(api_version): 441 if rel.get('api_version') != str(api_version):
329 raise Exception("api_version not propagated through relation" 442 raise Exception("api_version not propagated through relation"
330 " data yet ('{}' != '{}')." 443 " data yet ('{}' != '{}')."
331 "".format(rel['api_version'], api_version)) 444 "".format(rel.get('api_version'), api_version))
332 445
333 def keystone_configure_api_version(self, sentry_relation_pairs, deployment, 446 def keystone_configure_api_version(self, sentry_relation_pairs, deployment,
334 api_version): 447 api_version):
@@ -350,16 +463,13 @@ class OpenStackAmuletUtils(AmuletUtils):
350 deployment._auto_wait_for_status() 463 deployment._auto_wait_for_status()
351 self.keystone_wait_for_propagation(sentry_relation_pairs, api_version) 464 self.keystone_wait_for_propagation(sentry_relation_pairs, api_version)
352 465
353 def authenticate_cinder_admin(self, keystone_sentry, username, 466 def authenticate_cinder_admin(self, keystone, api_version=2):
354 password, tenant, api_version=2):
355 """Authenticates admin user with cinder.""" 467 """Authenticates admin user with cinder."""
356 # NOTE(beisner): cinder python client doesn't accept tokens. 468 self.log.debug('Authenticating cinder admin...')
357 keystone_ip = keystone_sentry.info['public-address']
358 ept = "http://{}:5000/v2.0".format(keystone_ip.strip().decode('utf-8'))
359 _clients = { 469 _clients = {
360 1: cinder_client.Client, 470 1: cinder_client.Client,
361 2: cinder_clientv2.Client} 471 2: cinder_clientv2.Client}
362 return _clients[api_version](username, password, tenant, ept) 472 return _clients[api_version](session=keystone.session)
363 473
364 def authenticate_keystone(self, keystone_ip, username, password, 474 def authenticate_keystone(self, keystone_ip, username, password,
365 api_version=False, admin_port=False, 475 api_version=False, admin_port=False,
@@ -367,13 +477,36 @@ class OpenStackAmuletUtils(AmuletUtils):
367 project_domain_name=None, project_name=None): 477 project_domain_name=None, project_name=None):
368 """Authenticate with Keystone""" 478 """Authenticate with Keystone"""
369 self.log.debug('Authenticating with keystone...') 479 self.log.debug('Authenticating with keystone...')
370 port = 5000 480 if not api_version:
371 if admin_port: 481 api_version = 2
372 port = 35357 482 sess, auth = self.get_keystone_session(
373 base_ep = "http://{}:{}".format(keystone_ip.strip().decode('utf-8'), 483 keystone_ip=keystone_ip,
374 port) 484 username=username,
375 if not api_version or api_version == 2: 485 password=password,
376 ep = base_ep + "/v2.0" 486 api_version=api_version,
487 admin_port=admin_port,
488 user_domain_name=user_domain_name,
489 domain_name=domain_name,
490 project_domain_name=project_domain_name,
491 project_name=project_name
492 )
493 if api_version == 2:
494 client = keystone_client.Client(session=sess)
495 else:
496 client = keystone_client_v3.Client(session=sess)
497 # This populates the client.service_catalog
498 client.auth_ref = auth.get_access(sess)
499 return client
500
501 def get_keystone_session(self, keystone_ip, username, password,
502 api_version=False, admin_port=False,
503 user_domain_name=None, domain_name=None,
504 project_domain_name=None, project_name=None):
505 """Return a keystone session object"""
506 ep = self.get_keystone_endpoint(keystone_ip,
507 api_version=api_version,
508 admin_port=admin_port)
509 if api_version == 2:
377 auth = v2.Password( 510 auth = v2.Password(
378 username=username, 511 username=username,
379 password=password, 512 password=password,
@@ -381,12 +514,7 @@ class OpenStackAmuletUtils(AmuletUtils):
381 auth_url=ep 514 auth_url=ep
382 ) 515 )
383 sess = keystone_session.Session(auth=auth) 516 sess = keystone_session.Session(auth=auth)
384 client = keystone_client.Client(session=sess)
385 # This populates the client.service_catalog
386 client.auth_ref = auth.get_access(sess)
387 return client
388 else: 517 else:
389 ep = base_ep + "/v3"
390 auth = v3.Password( 518 auth = v3.Password(
391 user_domain_name=user_domain_name, 519 user_domain_name=user_domain_name,
392 username=username, 520 username=username,
@@ -397,10 +525,57 @@ class OpenStackAmuletUtils(AmuletUtils):
397 auth_url=ep 525 auth_url=ep
398 ) 526 )
399 sess = keystone_session.Session(auth=auth) 527 sess = keystone_session.Session(auth=auth)
400 client = keystone_client_v3.Client(session=sess) 528 return (sess, auth)
401 # This populates the client.service_catalog 529
402 client.auth_ref = auth.get_access(sess) 530 def get_keystone_endpoint(self, keystone_ip, api_version=None,
403 return client 531 admin_port=False):
532 """Return keystone endpoint"""
533 port = 5000
534 if admin_port:
535 port = 35357
536 base_ep = "http://{}:{}".format(keystone_ip.strip().decode('utf-8'),
537 port)
538 if api_version == 2:
539 ep = base_ep + "/v2.0"
540 else:
541 ep = base_ep + "/v3"
542 return ep
543
544 def get_default_keystone_session(self, keystone_sentry,
545 openstack_release=None):
546 """Return a keystone session object and client object assuming standard
547 default settings
548
549 Example call in amulet tests:
550 self.keystone_session, self.keystone = u.get_default_keystone_session(
551 self.keystone_sentry,
552 openstack_release=self._get_openstack_release())
553
554 The session can then be used to auth other clients:
555 neutronclient.Client(session=session)
556 aodh_client.Client(session=session)
557 eyc
558 """
559 self.log.debug('Authenticating keystone admin...')
560 api_version = 2
561 client_class = keystone_client.Client
562 # 11 => xenial_queens
563 if openstack_release and openstack_release >= 11:
564 api_version = 3
565 client_class = keystone_client_v3.Client
566 keystone_ip = keystone_sentry.info['public-address']
567 session, auth = self.get_keystone_session(
568 keystone_ip,
569 api_version=api_version,
570 username='admin',
571 password='openstack',
572 project_name='admin',
573 user_domain_name='admin_domain',
574 project_domain_name='admin_domain')
575 client = client_class(session=session)
576 # This populates the client.service_catalog
577 client.auth_ref = auth.get_access(session)
578 return session, client
404 579
405 def authenticate_keystone_admin(self, keystone_sentry, user, password, 580 def authenticate_keystone_admin(self, keystone_sentry, user, password,
406 tenant=None, api_version=None, 581 tenant=None, api_version=None,
diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py
index 36cf32f..6c4497b 100644
--- a/hooks/charmhelpers/contrib/openstack/context.py
+++ b/hooks/charmhelpers/contrib/openstack/context.py
@@ -384,6 +384,7 @@ class IdentityServiceContext(OSContextGenerator):
384 # so a missing value just indicates keystone needs 384 # so a missing value just indicates keystone needs
385 # upgrading 385 # upgrading
386 ctxt['admin_tenant_id'] = rdata.get('service_tenant_id') 386 ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
387 ctxt['admin_domain_id'] = rdata.get('service_domain_id')
387 return ctxt 388 return ctxt
388 389
389 return {} 390 return {}
diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py
index b753275..e719426 100644
--- a/hooks/charmhelpers/contrib/openstack/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/utils.py
@@ -182,7 +182,7 @@ SWIFT_CODENAMES = OrderedDict([
182 ('pike', 182 ('pike',
183 ['2.13.0', '2.15.0']), 183 ['2.13.0', '2.15.0']),
184 ('queens', 184 ('queens',
185 ['2.16.0']), 185 ['2.16.0', '2.17.0']),
186]) 186])
187 187
188# >= Liberty version->codename mapping 188# >= Liberty version->codename mapping
diff --git a/hooks/charmhelpers/contrib/storage/linux/lvm.py b/hooks/charmhelpers/contrib/storage/linux/lvm.py
index 79a7a24..c8bde69 100644
--- a/hooks/charmhelpers/contrib/storage/linux/lvm.py
+++ b/hooks/charmhelpers/contrib/storage/linux/lvm.py
@@ -151,3 +151,32 @@ def extend_logical_volume_by_device(lv_name, block_device):
151 ''' 151 '''
152 cmd = ['lvextend', lv_name, block_device] 152 cmd = ['lvextend', lv_name, block_device]
153 check_call(cmd) 153 check_call(cmd)
154
155
156def create_logical_volume(lv_name, volume_group, size=None):
157 '''
158 Create a new logical volume in an existing volume group
159
160 :param lv_name: str: name of logical volume to be created.
161 :param volume_group: str: Name of volume group to use for the new volume.
162 :param size: str: Size of logical volume to create (100% if not supplied)
163 :raises subprocess.CalledProcessError: in the event that the lvcreate fails.
164 '''
165 if size:
166 check_call([
167 'lvcreate',
168 '--yes',
169 '-L',
170 '{}'.format(size),
171 '-n', lv_name, volume_group
172 ])
173 # create the lv with all the space available, this is needed because the
174 # system call is different for LVM
175 else:
176 check_call([
177 'lvcreate',
178 '--yes',
179 '-l',
180 '100%FREE',
181 '-n', lv_name, volume_group
182 ])
diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py
index 7ed1cc4..89f1024 100644
--- a/hooks/charmhelpers/core/hookenv.py
+++ b/hooks/charmhelpers/core/hookenv.py
@@ -27,6 +27,7 @@ import glob
27import os 27import os
28import json 28import json
29import yaml 29import yaml
30import re
30import subprocess 31import subprocess
31import sys 32import sys
32import errno 33import errno
@@ -67,7 +68,7 @@ def cached(func):
67 @wraps(func) 68 @wraps(func)
68 def wrapper(*args, **kwargs): 69 def wrapper(*args, **kwargs):
69 global cache 70 global cache
70 key = str((func, args, kwargs)) 71 key = json.dumps((func, args, kwargs), sort_keys=True, default=str)
71 try: 72 try:
72 return cache[key] 73 return cache[key]
73 except KeyError: 74 except KeyError:
@@ -1043,7 +1044,6 @@ def juju_version():
1043 universal_newlines=True).strip() 1044 universal_newlines=True).strip()
1044 1045
1045 1046
1046@cached
1047def has_juju_version(minimum_version): 1047def has_juju_version(minimum_version):
1048 """Return True if the Juju version is at least the provided version""" 1048 """Return True if the Juju version is at least the provided version"""
1049 return LooseVersion(juju_version()) >= LooseVersion(minimum_version) 1049 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
@@ -1103,6 +1103,8 @@ def _run_atexit():
1103@translate_exc(from_exc=OSError, to_exc=NotImplementedError) 1103@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1104def network_get_primary_address(binding): 1104def network_get_primary_address(binding):
1105 ''' 1105 '''
1106 Deprecated since Juju 2.3; use network_get()
1107
1106 Retrieve the primary network address for a named binding 1108 Retrieve the primary network address for a named binding
1107 1109
1108 :param binding: string. The name of a relation of extra-binding 1110 :param binding: string. The name of a relation of extra-binding
@@ -1123,7 +1125,6 @@ def network_get_primary_address(binding):
1123 return response 1125 return response
1124 1126
1125 1127
1126@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1127def network_get(endpoint, relation_id=None): 1128def network_get(endpoint, relation_id=None):
1128 """ 1129 """
1129 Retrieve the network details for a relation endpoint 1130 Retrieve the network details for a relation endpoint
@@ -1131,24 +1132,20 @@ def network_get(endpoint, relation_id=None):
1131 :param endpoint: string. The name of a relation endpoint 1132 :param endpoint: string. The name of a relation endpoint
1132 :param relation_id: int. The ID of the relation for the current context. 1133 :param relation_id: int. The ID of the relation for the current context.
1133 :return: dict. The loaded YAML output of the network-get query. 1134 :return: dict. The loaded YAML output of the network-get query.
1134 :raise: NotImplementedError if run on Juju < 2.1 1135 :raise: NotImplementedError if request not supported by the Juju version.
1135 """ 1136 """
1137 if not has_juju_version('2.2'):
1138 raise NotImplementedError(juju_version()) # earlier versions require --primary-address
1139 if relation_id and not has_juju_version('2.3'):
1140 raise NotImplementedError # 2.3 added the -r option
1141
1136 cmd = ['network-get', endpoint, '--format', 'yaml'] 1142 cmd = ['network-get', endpoint, '--format', 'yaml']
1137 if relation_id: 1143 if relation_id:
1138 cmd.append('-r') 1144 cmd.append('-r')
1139 cmd.append(relation_id) 1145 cmd.append(relation_id)
1140 try: 1146 response = subprocess.check_output(
1141 response = subprocess.check_output( 1147 cmd,
1142 cmd, 1148 stderr=subprocess.STDOUT).decode('UTF-8').strip()
1143 stderr=subprocess.STDOUT).decode('UTF-8').strip()
1144 except CalledProcessError as e:
1145 # Early versions of Juju 2.0.x required the --primary-address argument.
1146 # We catch that condition here and raise NotImplementedError since
1147 # the requested semantics are not available - the caller can then
1148 # use the network_get_primary_address() method instead.
1149 if '--primary-address is currently required' in e.output.decode('UTF-8'):
1150 raise NotImplementedError
1151 raise
1152 return yaml.safe_load(response) 1149 return yaml.safe_load(response)
1153 1150
1154 1151
@@ -1204,9 +1201,23 @@ def iter_units_for_relation_name(relation_name):
1204 1201
1205def ingress_address(rid=None, unit=None): 1202def ingress_address(rid=None, unit=None):
1206 """ 1203 """
1207 Retrieve the ingress-address from a relation when available. Otherwise, 1204 Retrieve the ingress-address from a relation when available.
1208 return the private-address. This function is to be used on the consuming 1205 Otherwise, return the private-address.
1209 side of the relation. 1206
1207 When used on the consuming side of the relation (unit is a remote
1208 unit), the ingress-address is the IP address that this unit needs
1209 to use to reach the provided service on the remote unit.
1210
1211 When used on the providing side of the relation (unit == local_unit()),
1212 the ingress-address is the IP address that is advertised to remote
1213 units on this relation. Remote units need to use this address to
1214 reach the local provided service on this unit.
1215
1216 Note that charms may document some other method to use in
1217 preference to the ingress_address(), such as an address provided
1218 on a different relation attribute or a service discovery mechanism.
1219 This allows charms to redirect inbound connections to their peers
1220 or different applications such as load balancers.
1210 1221
1211 Usage: 1222 Usage:
1212 addresses = [ingress_address(rid=u.rid, unit=u.unit) 1223 addresses = [ingress_address(rid=u.rid, unit=u.unit)
@@ -1220,3 +1231,40 @@ def ingress_address(rid=None, unit=None):
1220 settings = relation_get(rid=rid, unit=unit) 1231 settings = relation_get(rid=rid, unit=unit)
1221 return (settings.get('ingress-address') or 1232 return (settings.get('ingress-address') or
1222 settings.get('private-address')) 1233 settings.get('private-address'))
1234
1235
1236def egress_subnets(rid=None, unit=None):
1237 """
1238 Retrieve the egress-subnets from a relation.
1239
1240 This function is to be used on the providing side of the
1241 relation, and provides the ranges of addresses that client
1242 connections may come from. The result is uninteresting on
1243 the consuming side of a relation (unit == local_unit()).
1244
1245 Returns a stable list of subnets in CIDR format.
1246 eg. ['192.168.1.0/24', '2001::F00F/128']
1247
1248 If egress-subnets is not available, falls back to using the published
1249 ingress-address, or finally private-address.
1250
1251 :param rid: string relation id
1252 :param unit: string unit name
1253 :side effect: calls relation_get
1254 :return: list of subnets in CIDR format. eg. ['192.168.1.0/24', '2001::F00F/128']
1255 """
1256 def _to_range(addr):
1257 if re.search(r'^(?:\d{1,3}\.){3}\d{1,3}$', addr) is not None:
1258 addr += '/32'
1259 elif ':' in addr and '/' not in addr: # IPv6
1260 addr += '/128'
1261 return addr
1262
1263 settings = relation_get(rid=rid, unit=unit)
1264 if 'egress-subnets' in settings:
1265 return [n.strip() for n in settings['egress-subnets'].split(',') if n.strip()]
1266 if 'ingress-address' in settings:
1267 return [_to_range(settings['ingress-address'])]
1268 if 'private-address' in settings:
1269 return [_to_range(settings['private-address'])]
1270 return [] # Should never happen
diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py
index fd14d60..322ab2a 100644
--- a/hooks/charmhelpers/core/host.py
+++ b/hooks/charmhelpers/core/host.py
@@ -993,7 +993,7 @@ def updatedb(updatedb_text, new_path):
993 return output 993 return output
994 994
995 995
996def modulo_distribution(modulo=3, wait=30): 996def modulo_distribution(modulo=3, wait=30, non_zero_wait=False):
997 """ Modulo distribution 997 """ Modulo distribution
998 998
999 This helper uses the unit number, a modulo value and a constant wait time 999 This helper uses the unit number, a modulo value and a constant wait time
@@ -1015,7 +1015,14 @@ def modulo_distribution(modulo=3, wait=30):
1015 1015
1016 @param modulo: int The modulo number creates the group distribution 1016 @param modulo: int The modulo number creates the group distribution
1017 @param wait: int The constant time wait value 1017 @param wait: int The constant time wait value
1018 @param non_zero_wait: boolean Override unit % modulo == 0,
1019 return modulo * wait. Used to avoid collisions with
1020 leader nodes which are often given priority.
1018 @return: int Calculated time to wait for unit operation 1021 @return: int Calculated time to wait for unit operation
1019 """ 1022 """
1020 unit_number = int(local_unit().split('/')[1]) 1023 unit_number = int(local_unit().split('/')[1])
1021 return (unit_number % modulo) * wait 1024 calculated_wait_time = (unit_number % modulo) * wait
1025 if non_zero_wait and calculated_wait_time == 0:
1026 return modulo * wait
1027 else:
1028 return calculated_wait_time
diff --git a/hooks/charmhelpers/core/services/base.py b/hooks/charmhelpers/core/services/base.py
index ca9dc99..345b60d 100644
--- a/hooks/charmhelpers/core/services/base.py
+++ b/hooks/charmhelpers/core/services/base.py
@@ -313,17 +313,26 @@ class PortManagerCallback(ManagerCallback):
313 with open(port_file) as fp: 313 with open(port_file) as fp:
314 old_ports = fp.read().split(',') 314 old_ports = fp.read().split(',')
315 for old_port in old_ports: 315 for old_port in old_ports:
316 if bool(old_port): 316 if bool(old_port) and not self.ports_contains(old_port, new_ports):
317 old_port = int(old_port) 317 hookenv.close_port(old_port)
318 if old_port not in new_ports:
319 hookenv.close_port(old_port)
320 with open(port_file, 'w') as fp: 318 with open(port_file, 'w') as fp:
321 fp.write(','.join(str(port) for port in new_ports)) 319 fp.write(','.join(str(port) for port in new_ports))
322 for port in new_ports: 320 for port in new_ports:
321 # A port is either a number or 'ICMP'
322 protocol = 'TCP'
323 if str(port).upper() == 'ICMP':
324 protocol = 'ICMP'
323 if event_name == 'start': 325 if event_name == 'start':
324 hookenv.open_port(port) 326 hookenv.open_port(port, protocol)
325 elif event_name == 'stop': 327 elif event_name == 'stop':
326 hookenv.close_port(port) 328 hookenv.close_port(port, protocol)
329
330 def ports_contains(self, port, ports):
331 if not bool(port):
332 return False
333 if str(port).upper() != 'ICMP':
334 port = int(port)
335 return port in ports
327 336
328 337
329def service_stop(service_name): 338def service_stop(service_name):
diff --git a/hooks/charmhelpers/fetch/ubuntu.py b/hooks/charmhelpers/fetch/ubuntu.py
index 910e96a..653d58f 100644
--- a/hooks/charmhelpers/fetch/ubuntu.py
+++ b/hooks/charmhelpers/fetch/ubuntu.py
@@ -44,6 +44,7 @@ ARCH_TO_PROPOSED_POCKET = {
44 'x86_64': PROPOSED_POCKET, 44 'x86_64': PROPOSED_POCKET,
45 'ppc64le': PROPOSED_PORTS_POCKET, 45 'ppc64le': PROPOSED_PORTS_POCKET,
46 'aarch64': PROPOSED_PORTS_POCKET, 46 'aarch64': PROPOSED_PORTS_POCKET,
47 's390x': PROPOSED_PORTS_POCKET,
47} 48}
48CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu" 49CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
49CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA' 50CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'