Merge lp:~billy-olsen/charms/trusty/ceph-radosgw/public-endpoint-host into lp:~openstack-charmers-archive/charms/trusty/ceph-radosgw/next

Proposed by Billy Olsen
Status: Merged
Merged at revision: 39
Proposed branch: lp:~billy-olsen/charms/trusty/ceph-radosgw/public-endpoint-host
Merge into: lp:~openstack-charmers-archive/charms/trusty/ceph-radosgw/next
Diff against target: 1126 lines (+460/-135)
16 files modified
config.yaml (+12/-0)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+25/-0)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+15/-6)
hooks/charmhelpers/contrib/openstack/ip.py (+49/-44)
hooks/charmhelpers/contrib/openstack/neutron.py (+10/-5)
hooks/charmhelpers/contrib/openstack/utils.py (+65/-18)
hooks/charmhelpers/contrib/python/packages.py (+28/-5)
hooks/charmhelpers/core/hookenv.py (+147/-10)
hooks/charmhelpers/core/host.py (+1/-1)
hooks/charmhelpers/core/services/base.py (+32/-11)
hooks/charmhelpers/fetch/__init__.py (+1/-1)
hooks/charmhelpers/fetch/giturl.py (+7/-5)
hooks/hooks.py (+4/-15)
tests/charmhelpers/contrib/amulet/utils.py (+8/-1)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+15/-6)
unit_tests/test_hooks.py (+41/-7)
To merge this branch: bzr merge lp:~billy-olsen/charms/trusty/ceph-radosgw/public-endpoint-host
Reviewer Review Type Date Requested Status
Corey Bryant Approve
Review via email: mp+261011@code.launchpad.net

Description of the change

Provides a config option which allows the user to specify the public hostname used to advertise to keystone when creating endpoints.

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #5049 ceph-radosgw-next for billy-olsen mp261011
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/5049/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #4729 ceph-radosgw-next for billy-olsen mp261011
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/4729/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #4455 ceph-radosgw-next for billy-olsen mp261011
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/4455/

42. By Billy Olsen

c-h sync. unit test updates for sync

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #5073 ceph-radosgw-next for billy-olsen mp261011
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/5073/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #4752 ceph-radosgw-next for billy-olsen mp261011
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/4752/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #4480 ceph-radosgw-next for billy-olsen mp261011
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/4480/

Revision history for this message
Corey Bryant (corey.bryant) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'config.yaml'
2--- config.yaml 2015-01-14 13:27:02 +0000
3+++ config.yaml 2015-06-04 23:07:06 +0000
4@@ -87,3 +87,15 @@
5 description: |
6 Default multicast port number that will be used to communicate between
7 HA Cluster nodes.
8+ os-public-hostname:
9+ type: string
10+ default:
11+ description: |
12+ The hostname or address of the public endpoints created for ceph-radosgw
13+ in the keystone identity provider.
14+ .
15+ This value will be used for public endpoints. For example, an
16+ os-public-hostname set to 'files.example.com' with will create
17+ the following public endpoint for the ceph-radosgw.
18+ .
19+ https://files.example.com:80/swift/v1
20\ No newline at end of file
21
22=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
23--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-02-24 11:02:02 +0000
24+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-04 23:07:06 +0000
25@@ -52,6 +52,8 @@
26 bool_from_string,
27 )
28
29+DC_RESOURCE_NAME = 'DC'
30+
31
32 class HAIncompleteConfig(Exception):
33 pass
34@@ -95,6 +97,27 @@
35 return False
36
37
38+def is_crm_dc():
39+ """
40+ Determine leadership by querying the pacemaker Designated Controller
41+ """
42+ cmd = ['crm', 'status']
43+ try:
44+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
45+ if not isinstance(status, six.text_type):
46+ status = six.text_type(status, "utf-8")
47+ except subprocess.CalledProcessError:
48+ return False
49+ current_dc = ''
50+ for line in status.split('\n'):
51+ if line.startswith('Current DC'):
52+ # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
53+ current_dc = line.split(':')[1].split()[0]
54+ if current_dc == get_unit_hostname():
55+ return True
56+ return False
57+
58+
59 @retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
60 def is_crm_leader(resource, retry=False):
61 """
62@@ -104,6 +127,8 @@
63 We allow this operation to be retried to avoid the possibility of getting a
64 false negative. See LP #1396246 for more info.
65 """
66+ if resource == DC_RESOURCE_NAME:
67+ return is_crm_dc()
68 cmd = ['crm', 'resource', 'show', resource]
69 try:
70 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
71
72=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
73--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-04-16 21:32:59 +0000
74+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-04 23:07:06 +0000
75@@ -46,15 +46,22 @@
76 stable or next branches for the other_services."""
77 base_charms = ['mysql', 'mongodb']
78
79+ if self.series in ['precise', 'trusty']:
80+ base_series = self.series
81+ else:
82+ base_series = self.current_next
83+
84 if self.stable:
85 for svc in other_services:
86- temp = 'lp:charms/{}'
87- svc['location'] = temp.format(svc['name'])
88+ temp = 'lp:charms/{}/{}'
89+ svc['location'] = temp.format(base_series,
90+ svc['name'])
91 else:
92 for svc in other_services:
93 if svc['name'] in base_charms:
94- temp = 'lp:charms/{}'
95- svc['location'] = temp.format(svc['name'])
96+ temp = 'lp:charms/{}/{}'
97+ svc['location'] = temp.format(base_series,
98+ svc['name'])
99 else:
100 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
101 svc['location'] = temp.format(self.current_next,
102@@ -99,10 +106,12 @@
103 Return an integer representing the enum value of the openstack
104 release.
105 """
106+ # Must be ordered by OpenStack release (not by Ubuntu release):
107 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
108 self.precise_havana, self.precise_icehouse,
109- self.trusty_icehouse, self.trusty_juno, self.trusty_kilo,
110- self.utopic_juno, self.vivid_kilo) = range(10)
111+ self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
112+ self.trusty_kilo, self.vivid_kilo) = range(10)
113+
114 releases = {
115 ('precise', None): self.precise_essex,
116 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
117
118=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
119--- hooks/charmhelpers/contrib/openstack/ip.py 2015-02-24 11:02:02 +0000
120+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-06-04 23:07:06 +0000
121@@ -17,6 +17,7 @@
122 from charmhelpers.core.hookenv import (
123 config,
124 unit_get,
125+ service_name,
126 )
127 from charmhelpers.contrib.network.ip import (
128 get_address_in_network,
129@@ -26,8 +27,6 @@
130 )
131 from charmhelpers.contrib.hahelpers.cluster import is_clustered
132
133-from functools import partial
134-
135 PUBLIC = 'public'
136 INTERNAL = 'int'
137 ADMIN = 'admin'
138@@ -35,15 +34,18 @@
139 ADDRESS_MAP = {
140 PUBLIC: {
141 'config': 'os-public-network',
142- 'fallback': 'public-address'
143+ 'fallback': 'public-address',
144+ 'override': 'os-public-hostname',
145 },
146 INTERNAL: {
147 'config': 'os-internal-network',
148- 'fallback': 'private-address'
149+ 'fallback': 'private-address',
150+ 'override': 'os-internal-hostname',
151 },
152 ADMIN: {
153 'config': 'os-admin-network',
154- 'fallback': 'private-address'
155+ 'fallback': 'private-address',
156+ 'override': 'os-admin-hostname',
157 }
158 }
159
160@@ -57,15 +59,50 @@
161 :param endpoint_type: str endpoint type to resolve.
162 :param returns: str base URL for services on the current service unit.
163 """
164- scheme = 'http'
165- if 'https' in configs.complete_contexts():
166- scheme = 'https'
167+ scheme = _get_scheme(configs)
168+
169 address = resolve_address(endpoint_type)
170 if is_ipv6(address):
171 address = "[{}]".format(address)
172+
173 return '%s://%s' % (scheme, address)
174
175
176+def _get_scheme(configs):
177+ """Returns the scheme to use for the url (either http or https)
178+ depending upon whether https is in the configs value.
179+
180+ :param configs: OSTemplateRenderer config templating object to inspect
181+ for a complete https context.
182+ :returns: either 'http' or 'https' depending on whether https is
183+ configured within the configs context.
184+ """
185+ scheme = 'http'
186+ if configs and 'https' in configs.complete_contexts():
187+ scheme = 'https'
188+ return scheme
189+
190+
191+def _get_address_override(endpoint_type=PUBLIC):
192+ """Returns any address overrides that the user has defined based on the
193+ endpoint type.
194+
195+ Note: this function allows for the service name to be inserted into the
196+ address if the user specifies {service_name}.somehost.org.
197+
198+ :param endpoint_type: the type of endpoint to retrieve the override
199+ value for.
200+ :returns: any endpoint address or hostname that the user has overridden
201+ or None if an override is not present.
202+ """
203+ override_key = ADDRESS_MAP[endpoint_type]['override']
204+ addr_override = config(override_key)
205+ if not addr_override:
206+ return None
207+ else:
208+ return addr_override.format(service_name=service_name())
209+
210+
211 def resolve_address(endpoint_type=PUBLIC):
212 """Return unit address depending on net config.
213
214@@ -77,7 +114,10 @@
215
216 :param endpoint_type: Network endpoing type
217 """
218- resolved_address = None
219+ resolved_address = _get_address_override(endpoint_type)
220+ if resolved_address:
221+ return resolved_address
222+
223 vips = config('vip')
224 if vips:
225 vips = vips.split()
226@@ -109,38 +149,3 @@
227 "clustered=%s)" % (net_type, clustered))
228
229 return resolved_address
230-
231-
232-def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC,
233- override=None):
234- """Returns the correct endpoint URL to advertise to Keystone.
235-
236- This method provides the correct endpoint URL which should be advertised to
237- the keystone charm for endpoint creation. This method allows for the url to
238- be overridden to force a keystone endpoint to have specific URL for any of
239- the defined scopes (admin, internal, public).
240-
241- :param configs: OSTemplateRenderer config templating object to inspect
242- for a complete https context.
243- :param url_template: str format string for creating the url template. Only
244- two values will be passed - the scheme+hostname
245- returned by the canonical_url and the port.
246- :param endpoint_type: str endpoint type to resolve.
247- :param override: str the name of the config option which overrides the
248- endpoint URL defined by the charm itself. None will
249- disable any overrides (default).
250- """
251- if override:
252- # Return any user-defined overrides for the keystone endpoint URL.
253- user_value = config(override)
254- if user_value:
255- return user_value.strip()
256-
257- return url_template % (canonical_url(configs, endpoint_type), port)
258-
259-
260-public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC)
261-
262-internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL)
263-
264-admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN)
265
266=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
267--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-04-16 21:32:59 +0000
268+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-04 23:07:06 +0000
269@@ -256,11 +256,14 @@
270 def parse_mappings(mappings):
271 parsed = {}
272 if mappings:
273- mappings = mappings.split(' ')
274+ mappings = mappings.split()
275 for m in mappings:
276 p = m.partition(':')
277- if p[1] == ':':
278- parsed[p[0].strip()] = p[2].strip()
279+ key = p[0].strip()
280+ if p[1]:
281+ parsed[key] = p[2].strip()
282+ else:
283+ parsed[key] = ''
284
285 return parsed
286
287@@ -283,13 +286,13 @@
288 Returns dict of the form {bridge:port}.
289 """
290 _mappings = parse_mappings(mappings)
291- if not _mappings:
292+ if not _mappings or list(_mappings.values()) == ['']:
293 if not mappings:
294 return {}
295
296 # For backwards-compatibility we need to support port-only provided in
297 # config.
298- _mappings = {default_bridge: mappings.split(' ')[0]}
299+ _mappings = {default_bridge: mappings.split()[0]}
300
301 bridges = _mappings.keys()
302 ports = _mappings.values()
303@@ -309,6 +312,8 @@
304
305 Mappings must be a space-delimited list of provider:start:end mappings.
306
307+ The start:end range is optional and may be omitted.
308+
309 Returns dict of the form {provider: (start, end)}.
310 """
311 _mappings = parse_mappings(mappings)
312
313=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
314--- hooks/charmhelpers/contrib/openstack/utils.py 2015-04-16 21:32:59 +0000
315+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-06-04 23:07:06 +0000
316@@ -53,9 +53,13 @@
317 get_ipv6_addr
318 )
319
320+from charmhelpers.contrib.python.packages import (
321+ pip_create_virtualenv,
322+ pip_install,
323+)
324+
325 from charmhelpers.core.host import lsb_release, mounts, umount
326 from charmhelpers.fetch import apt_install, apt_cache, install_remote
327-from charmhelpers.contrib.python.packages import pip_install
328 from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
329 from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
330
331@@ -497,7 +501,17 @@
332 requirements_dir = None
333
334
335-def git_clone_and_install(projects_yaml, core_project):
336+def _git_yaml_load(projects_yaml):
337+ """
338+ Load the specified yaml into a dictionary.
339+ """
340+ if not projects_yaml:
341+ return None
342+
343+ return yaml.load(projects_yaml)
344+
345+
346+def git_clone_and_install(projects_yaml, core_project, depth=1):
347 """
348 Clone/install all specified OpenStack repositories.
349
350@@ -510,23 +524,22 @@
351 repository: 'git://git.openstack.org/openstack/requirements.git',
352 branch: 'stable/icehouse'}
353 directory: /mnt/openstack-git
354- http_proxy: http://squid.internal:3128
355- https_proxy: https://squid.internal:3128
356+ http_proxy: squid-proxy-url
357+ https_proxy: squid-proxy-url
358
359 The directory, http_proxy, and https_proxy keys are optional.
360 """
361 global requirements_dir
362 parent_dir = '/mnt/openstack-git'
363-
364- if not projects_yaml:
365- return
366-
367- projects = yaml.load(projects_yaml)
368+ http_proxy = None
369+
370+ projects = _git_yaml_load(projects_yaml)
371 _git_validate_projects_yaml(projects, core_project)
372
373 old_environ = dict(os.environ)
374
375 if 'http_proxy' in projects.keys():
376+ http_proxy = projects['http_proxy']
377 os.environ['http_proxy'] = projects['http_proxy']
378 if 'https_proxy' in projects.keys():
379 os.environ['https_proxy'] = projects['https_proxy']
380@@ -534,15 +547,19 @@
381 if 'directory' in projects.keys():
382 parent_dir = projects['directory']
383
384+ pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
385+
386 for p in projects['repositories']:
387 repo = p['repository']
388 branch = p['branch']
389 if p['name'] == 'requirements':
390- repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,
391+ repo_dir = _git_clone_and_install_single(repo, branch, depth,
392+ parent_dir, http_proxy,
393 update_requirements=False)
394 requirements_dir = repo_dir
395 else:
396- repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,
397+ repo_dir = _git_clone_and_install_single(repo, branch, depth,
398+ parent_dir, http_proxy,
399 update_requirements=True)
400
401 os.environ = old_environ
402@@ -574,7 +591,8 @@
403 error_out('openstack-origin-git key \'{}\' is missing'.format(key))
404
405
406-def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements):
407+def _git_clone_and_install_single(repo, branch, depth, parent_dir, http_proxy,
408+ update_requirements):
409 """
410 Clone and install a single git repository.
411 """
412@@ -587,7 +605,8 @@
413
414 if not os.path.exists(dest_dir):
415 juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
416- repo_dir = install_remote(repo, dest=parent_dir, branch=branch)
417+ repo_dir = install_remote(repo, dest=parent_dir, branch=branch,
418+ depth=depth)
419 else:
420 repo_dir = dest_dir
421
422@@ -598,7 +617,12 @@
423 _git_update_requirements(repo_dir, requirements_dir)
424
425 juju_log('Installing git repo from dir: {}'.format(repo_dir))
426- pip_install(repo_dir)
427+ if http_proxy:
428+ pip_install(repo_dir, proxy=http_proxy,
429+ venv=os.path.join(parent_dir, 'venv'))
430+ else:
431+ pip_install(repo_dir,
432+ venv=os.path.join(parent_dir, 'venv'))
433
434 return repo_dir
435
436@@ -621,16 +645,27 @@
437 os.chdir(orig_dir)
438
439
440+def git_pip_venv_dir(projects_yaml):
441+ """
442+ Return the pip virtualenv path.
443+ """
444+ parent_dir = '/mnt/openstack-git'
445+
446+ projects = _git_yaml_load(projects_yaml)
447+
448+ if 'directory' in projects.keys():
449+ parent_dir = projects['directory']
450+
451+ return os.path.join(parent_dir, 'venv')
452+
453+
454 def git_src_dir(projects_yaml, project):
455 """
456 Return the directory where the specified project's source is located.
457 """
458 parent_dir = '/mnt/openstack-git'
459
460- if not projects_yaml:
461- return
462-
463- projects = yaml.load(projects_yaml)
464+ projects = _git_yaml_load(projects_yaml)
465
466 if 'directory' in projects.keys():
467 parent_dir = projects['directory']
468@@ -640,3 +675,15 @@
469 return os.path.join(parent_dir, os.path.basename(p['repository']))
470
471 return None
472+
473+
474+def git_yaml_value(projects_yaml, key):
475+ """
476+ Return the value in projects_yaml for the specified key.
477+ """
478+ projects = _git_yaml_load(projects_yaml)
479+
480+ if key in projects.keys():
481+ return projects[key]
482+
483+ return None
484
485=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
486--- hooks/charmhelpers/contrib/python/packages.py 2015-02-24 11:02:02 +0000
487+++ hooks/charmhelpers/contrib/python/packages.py 2015-06-04 23:07:06 +0000
488@@ -17,8 +17,11 @@
489 # You should have received a copy of the GNU Lesser General Public License
490 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
491
492+import os
493+import subprocess
494+
495 from charmhelpers.fetch import apt_install, apt_update
496-from charmhelpers.core.hookenv import log
497+from charmhelpers.core.hookenv import charm_dir, log
498
499 try:
500 from pip import main as pip_execute
501@@ -51,11 +54,15 @@
502 pip_execute(command)
503
504
505-def pip_install(package, fatal=False, upgrade=False, **options):
506+def pip_install(package, fatal=False, upgrade=False, venv=None, **options):
507 """Install a python package"""
508- command = ["install"]
509+ if venv:
510+ venv_python = os.path.join(venv, 'bin/pip')
511+ command = [venv_python, "install"]
512+ else:
513+ command = ["install"]
514
515- available_options = ('proxy', 'src', 'log', "index-url", )
516+ available_options = ('proxy', 'src', 'log', 'index-url', )
517 for option in parse_options(options, available_options):
518 command.append(option)
519
520@@ -69,7 +76,10 @@
521
522 log("Installing {} package with options: {}".format(package,
523 command))
524- pip_execute(command)
525+ if venv:
526+ subprocess.check_call(command)
527+ else:
528+ pip_execute(command)
529
530
531 def pip_uninstall(package, **options):
532@@ -94,3 +104,16 @@
533 """Returns the list of current python installed packages
534 """
535 return pip_execute(["list"])
536+
537+
538+def pip_create_virtualenv(path=None):
539+ """Create an isolated Python environment."""
540+ apt_install('python-virtualenv')
541+
542+ if path:
543+ venv_path = path
544+ else:
545+ venv_path = os.path.join(charm_dir(), 'venv')
546+
547+ if not os.path.exists(venv_path):
548+ subprocess.check_call(['virtualenv', venv_path])
549
550=== modified file 'hooks/charmhelpers/core/hookenv.py'
551--- hooks/charmhelpers/core/hookenv.py 2015-04-16 21:32:59 +0000
552+++ hooks/charmhelpers/core/hookenv.py 2015-06-04 23:07:06 +0000
553@@ -21,12 +21,14 @@
554 # Charm Helpers Developers <juju@lists.ubuntu.com>
555
556 from __future__ import print_function
557+from functools import wraps
558 import os
559 import json
560 import yaml
561 import subprocess
562 import sys
563 import errno
564+import tempfile
565 from subprocess import CalledProcessError
566
567 import six
568@@ -58,15 +60,17 @@
569
570 will cache the result of unit_get + 'test' for future calls.
571 """
572+ @wraps(func)
573 def wrapper(*args, **kwargs):
574 global cache
575 key = str((func, args, kwargs))
576 try:
577 return cache[key]
578 except KeyError:
579- res = func(*args, **kwargs)
580- cache[key] = res
581- return res
582+ pass # Drop out of the exception handler scope.
583+ res = func(*args, **kwargs)
584+ cache[key] = res
585+ return res
586 return wrapper
587
588
589@@ -178,7 +182,7 @@
590
591 def remote_unit():
592 """The remote unit for the current relation hook"""
593- return os.environ['JUJU_REMOTE_UNIT']
594+ return os.environ.get('JUJU_REMOTE_UNIT', None)
595
596
597 def service_name():
598@@ -250,6 +254,12 @@
599 except KeyError:
600 return (self._prev_dict or {})[key]
601
602+ def get(self, key, default=None):
603+ try:
604+ return self[key]
605+ except KeyError:
606+ return default
607+
608 def keys(self):
609 prev_keys = []
610 if self._prev_dict is not None:
611@@ -353,18 +363,49 @@
612 """Set relation information for the current unit"""
613 relation_settings = relation_settings if relation_settings else {}
614 relation_cmd_line = ['relation-set']
615+ accepts_file = "--file" in subprocess.check_output(
616+ relation_cmd_line + ["--help"], universal_newlines=True)
617 if relation_id is not None:
618 relation_cmd_line.extend(('-r', relation_id))
619- for k, v in (list(relation_settings.items()) + list(kwargs.items())):
620- if v is None:
621- relation_cmd_line.append('{}='.format(k))
622- else:
623- relation_cmd_line.append('{}={}'.format(k, v))
624- subprocess.check_call(relation_cmd_line)
625+ settings = relation_settings.copy()
626+ settings.update(kwargs)
627+ for key, value in settings.items():
628+ # Force value to be a string: it always should, but some call
629+ # sites pass in things like dicts or numbers.
630+ if value is not None:
631+ settings[key] = "{}".format(value)
632+ if accepts_file:
633+ # --file was introduced in Juju 1.23.2. Use it by default if
634+ # available, since otherwise we'll break if the relation data is
635+ # too big. Ideally we should tell relation-set to read the data from
636+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
637+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
638+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
639+ subprocess.check_call(
640+ relation_cmd_line + ["--file", settings_file.name])
641+ os.remove(settings_file.name)
642+ else:
643+ for key, value in settings.items():
644+ if value is None:
645+ relation_cmd_line.append('{}='.format(key))
646+ else:
647+ relation_cmd_line.append('{}={}'.format(key, value))
648+ subprocess.check_call(relation_cmd_line)
649 # Flush cache of any relation-gets for local unit
650 flush(local_unit())
651
652
653+def relation_clear(r_id=None):
654+ ''' Clears any relation data already set on relation r_id '''
655+ settings = relation_get(rid=r_id,
656+ unit=local_unit())
657+ for setting in settings:
658+ if setting not in ['public-address', 'private-address']:
659+ settings[setting] = None
660+ relation_set(relation_id=r_id,
661+ **settings)
662+
663+
664 @cached
665 def relation_ids(reltype=None):
666 """A list of relation_ids"""
667@@ -509,6 +550,11 @@
668 return None
669
670
671+def unit_public_ip():
672+ """Get this unit's public IP address"""
673+ return unit_get('public-address')
674+
675+
676 def unit_private_ip():
677 """Get this unit's private IP address"""
678 return unit_get('private-address')
679@@ -605,3 +651,94 @@
680
681 The results set by action_set are preserved."""
682 subprocess.check_call(['action-fail', message])
683+
684+
685+def status_set(workload_state, message):
686+ """Set the workload state with a message
687+
688+ Use status-set to set the workload state with a message which is visible
689+ to the user via juju status. If the status-set command is not found then
690+ assume this is juju < 1.23 and juju-log the message unstead.
691+
692+ workload_state -- valid juju workload state.
693+ message -- status update message
694+ """
695+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
696+ if workload_state not in valid_states:
697+ raise ValueError(
698+ '{!r} is not a valid workload state'.format(workload_state)
699+ )
700+ cmd = ['status-set', workload_state, message]
701+ try:
702+ ret = subprocess.call(cmd)
703+ if ret == 0:
704+ return
705+ except OSError as e:
706+ if e.errno != errno.ENOENT:
707+ raise
708+ log_message = 'status-set failed: {} {}'.format(workload_state,
709+ message)
710+ log(log_message, level='INFO')
711+
712+
713+def status_get():
714+ """Retrieve the previously set juju workload state
715+
716+ If the status-set command is not found then assume this is juju < 1.23 and
717+ return 'unknown'
718+ """
719+ cmd = ['status-get']
720+ try:
721+ raw_status = subprocess.check_output(cmd, universal_newlines=True)
722+ status = raw_status.rstrip()
723+ return status
724+ except OSError as e:
725+ if e.errno == errno.ENOENT:
726+ return 'unknown'
727+ else:
728+ raise
729+
730+
731+def translate_exc(from_exc, to_exc):
732+ def inner_translate_exc1(f):
733+ def inner_translate_exc2(*args, **kwargs):
734+ try:
735+ return f(*args, **kwargs)
736+ except from_exc:
737+ raise to_exc
738+
739+ return inner_translate_exc2
740+
741+ return inner_translate_exc1
742+
743+
744+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
745+def is_leader():
746+ """Does the current unit hold the juju leadership
747+
748+ Uses juju to determine whether the current unit is the leader of its peers
749+ """
750+ cmd = ['is-leader', '--format=json']
751+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
752+
753+
754+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
755+def leader_get(attribute=None):
756+ """Juju leader get value(s)"""
757+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
758+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
759+
760+
761+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
762+def leader_set(settings=None, **kwargs):
763+ """Juju leader set value(s)"""
764+ log("Juju leader-set '%s'" % (settings), level=DEBUG)
765+ cmd = ['leader-set']
766+ settings = settings or {}
767+ settings.update(kwargs)
768+ for k, v in settings.iteritems():
769+ if v is None:
770+ cmd.append('{}='.format(k))
771+ else:
772+ cmd.append('{}={}'.format(k, v))
773+ subprocess.check_call(cmd)
774
775=== modified file 'hooks/charmhelpers/core/host.py'
776--- hooks/charmhelpers/core/host.py 2015-04-16 21:32:59 +0000
777+++ hooks/charmhelpers/core/host.py 2015-06-04 23:07:06 +0000
778@@ -90,7 +90,7 @@
779 ['service', service_name, 'status'],
780 stderr=subprocess.STDOUT).decode('UTF-8')
781 except subprocess.CalledProcessError as e:
782- return 'unrecognized service' not in e.output
783+ return b'unrecognized service' not in e.output
784 else:
785 return True
786
787
788=== modified file 'hooks/charmhelpers/core/services/base.py'
789--- hooks/charmhelpers/core/services/base.py 2015-01-26 11:53:19 +0000
790+++ hooks/charmhelpers/core/services/base.py 2015-06-04 23:07:06 +0000
791@@ -15,9 +15,9 @@
792 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
793
794 import os
795-import re
796 import json
797-from collections import Iterable
798+from inspect import getargspec
799+from collections import Iterable, OrderedDict
800
801 from charmhelpers.core import host
802 from charmhelpers.core import hookenv
803@@ -119,7 +119,7 @@
804 """
805 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
806 self._ready = None
807- self.services = {}
808+ self.services = OrderedDict()
809 for service in services or []:
810 service_name = service['service']
811 self.services[service_name] = service
812@@ -132,8 +132,8 @@
813 if hook_name == 'stop':
814 self.stop_services()
815 else:
816+ self.reconfigure_services()
817 self.provide_data()
818- self.reconfigure_services()
819 cfg = hookenv.config()
820 if cfg.implicit_save:
821 cfg.save()
822@@ -145,15 +145,36 @@
823 A provider must have a `name` attribute, which indicates which relation
824 to set data on, and a `provide_data()` method, which returns a dict of
825 data to set.
826+
827+ The `provide_data()` method can optionally accept two parameters:
828+
829+ * ``remote_service`` The name of the remote service that the data will
830+ be provided to. The `provide_data()` method will be called once
831+ for each connected service (not unit). This allows the method to
832+ tailor its data to the given service.
833+ * ``service_ready`` Whether or not the service definition had all of
834+ its requirements met, and thus the ``data_ready`` callbacks run.
835+
836+ Note that the ``provided_data`` methods are now called **after** the
837+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
838+ a chance to generate any data necessary for the providing to the remote
839+ services.
840 """
841- hook_name = hookenv.hook_name()
842- for service in self.services.values():
843+ for service_name, service in self.services.items():
844+ service_ready = self.is_ready(service_name)
845 for provider in service.get('provided_data', []):
846- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
847- data = provider.provide_data()
848- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
849- if _ready:
850- hookenv.relation_set(None, data)
851+ for relid in hookenv.relation_ids(provider.name):
852+ units = hookenv.related_units(relid)
853+ if not units:
854+ continue
855+ remote_service = units[0].split('/')[0]
856+ argspec = getargspec(provider.provide_data)
857+ if len(argspec.args) > 1:
858+ data = provider.provide_data(remote_service, service_ready)
859+ else:
860+ data = provider.provide_data()
861+ if data:
862+ hookenv.relation_set(relid, data)
863
864 def reconfigure_services(self, *service_names):
865 """
866
867=== modified file 'hooks/charmhelpers/fetch/__init__.py'
868--- hooks/charmhelpers/fetch/__init__.py 2015-01-26 11:53:19 +0000
869+++ hooks/charmhelpers/fetch/__init__.py 2015-06-04 23:07:06 +0000
870@@ -158,7 +158,7 @@
871
872 def apt_cache(in_memory=True):
873 """Build and return an apt cache"""
874- import apt_pkg
875+ from apt import apt_pkg
876 apt_pkg.init()
877 if in_memory:
878 apt_pkg.config.set("Dir::Cache::pkgcache", "")
879
880=== modified file 'hooks/charmhelpers/fetch/giturl.py'
881--- hooks/charmhelpers/fetch/giturl.py 2015-02-24 11:02:02 +0000
882+++ hooks/charmhelpers/fetch/giturl.py 2015-06-04 23:07:06 +0000
883@@ -45,14 +45,16 @@
884 else:
885 return True
886
887- def clone(self, source, dest, branch):
888+ def clone(self, source, dest, branch, depth=None):
889 if not self.can_handle(source):
890 raise UnhandledSource("Cannot handle {}".format(source))
891
892- repo = Repo.clone_from(source, dest)
893- repo.git.checkout(branch)
894+ if depth:
895+ Repo.clone_from(source, dest, branch=branch, depth=depth)
896+ else:
897+ Repo.clone_from(source, dest, branch=branch)
898
899- def install(self, source, branch="master", dest=None):
900+ def install(self, source, branch="master", dest=None, depth=None):
901 url_parts = self.parse_url(source)
902 branch_name = url_parts.path.strip("/").split("/")[-1]
903 if dest:
904@@ -63,7 +65,7 @@
905 if not os.path.exists(dest_dir):
906 mkdir(dest_dir, perms=0o755)
907 try:
908- self.clone(source, dest_dir, branch)
909+ self.clone(source, dest_dir, branch, depth)
910 except GitCommandError as e:
911 raise UnhandledSource(e.message)
912 except OSError as e:
913
914=== modified file 'hooks/hooks.py'
915--- hooks/hooks.py 2015-04-28 21:18:24 +0000
916+++ hooks/hooks.py 2015-06-04 23:07:06 +0000
917@@ -50,10 +50,9 @@
918 from charmhelpers.contrib.network.ip import (
919 get_iface_for_address,
920 get_netmask_for_address,
921- is_ipv6,
922 )
923 from charmhelpers.contrib.openstack.ip import (
924- resolve_address,
925+ canonical_url,
926 PUBLIC, INTERNAL, ADMIN,
927 )
928
929@@ -273,16 +272,6 @@
930 open_port(port=80)
931
932
933-# XXX Define local canonical_url until charm has been updated to use the
934-# standard context architecture.
935-def canonical_url(configs, endpoint_type=PUBLIC):
936- scheme = 'http'
937- address = resolve_address(endpoint_type)
938- if is_ipv6(address):
939- address = "[{}]".format(address)
940- return '%s://%s' % (scheme, address)
941-
942-
943 @hooks.hook('identity-service-relation-joined')
944 def identity_joined(relid=None):
945 if cmp_pkgrevno('radosgw', '0.55') < 0:
946@@ -290,11 +279,11 @@
947 sys.exit(1)
948
949 port = 80
950- admin_url = '%s:%i/swift' % (canonical_url(ADMIN), port)
951+ admin_url = '%s:%i/swift' % (canonical_url(None, ADMIN), port)
952 internal_url = '%s:%s/swift/v1' % \
953- (canonical_url(INTERNAL), port)
954+ (canonical_url(None, INTERNAL), port)
955 public_url = '%s:%s/swift/v1' % \
956- (canonical_url(PUBLIC), port)
957+ (canonical_url(None, PUBLIC), port)
958 relation_set(service='swift',
959 region=config('region'),
960 public_url=public_url, internal_url=internal_url,
961
962=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
963--- tests/charmhelpers/contrib/amulet/utils.py 2015-04-16 21:32:59 +0000
964+++ tests/charmhelpers/contrib/amulet/utils.py 2015-06-04 23:07:06 +0000
965@@ -79,6 +79,9 @@
966 for k, v in six.iteritems(commands):
967 for cmd in v:
968 output, code = k.run(cmd)
969+ self.log.debug('{} `{}` returned '
970+ '{}'.format(k.info['unit_name'],
971+ cmd, code))
972 if code != 0:
973 return "command `{}` returned {}".format(cmd, str(code))
974 return None
975@@ -86,7 +89,11 @@
976 def _get_config(self, unit, filename):
977 """Get a ConfigParser object for parsing a unit's config file."""
978 file_contents = unit.file_contents(filename)
979- config = ConfigParser.ConfigParser()
980+
981+ # NOTE(beisner): by default, ConfigParser does not handle options
982+ # with no value, such as the flags used in the mysql my.cnf file.
983+ # https://bugs.python.org/issue7005
984+ config = ConfigParser.ConfigParser(allow_no_value=True)
985 config.readfp(io.StringIO(file_contents))
986 return config
987
988
989=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
990--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-04-16 21:32:59 +0000
991+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-04 23:07:06 +0000
992@@ -46,15 +46,22 @@
993 stable or next branches for the other_services."""
994 base_charms = ['mysql', 'mongodb']
995
996+ if self.series in ['precise', 'trusty']:
997+ base_series = self.series
998+ else:
999+ base_series = self.current_next
1000+
1001 if self.stable:
1002 for svc in other_services:
1003- temp = 'lp:charms/{}'
1004- svc['location'] = temp.format(svc['name'])
1005+ temp = 'lp:charms/{}/{}'
1006+ svc['location'] = temp.format(base_series,
1007+ svc['name'])
1008 else:
1009 for svc in other_services:
1010 if svc['name'] in base_charms:
1011- temp = 'lp:charms/{}'
1012- svc['location'] = temp.format(svc['name'])
1013+ temp = 'lp:charms/{}/{}'
1014+ svc['location'] = temp.format(base_series,
1015+ svc['name'])
1016 else:
1017 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
1018 svc['location'] = temp.format(self.current_next,
1019@@ -99,10 +106,12 @@
1020 Return an integer representing the enum value of the openstack
1021 release.
1022 """
1023+ # Must be ordered by OpenStack release (not by Ubuntu release):
1024 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
1025 self.precise_havana, self.precise_icehouse,
1026- self.trusty_icehouse, self.trusty_juno, self.trusty_kilo,
1027- self.utopic_juno, self.vivid_kilo) = range(10)
1028+ self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
1029+ self.trusty_kilo, self.vivid_kilo) = range(10)
1030+
1031 releases = {
1032 ('precise', None): self.precise_essex,
1033 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
1034
1035=== modified file 'unit_tests/test_hooks.py'
1036--- unit_tests/test_hooks.py 2015-04-28 21:18:24 +0000
1037+++ unit_tests/test_hooks.py 2015-06-04 23:07:06 +0000
1038@@ -8,6 +8,7 @@
1039 CharmTestCase,
1040 patch_open
1041 )
1042+from charmhelpers.contrib.openstack.ip import PUBLIC
1043
1044 dnsmock = MagicMock()
1045 modules = {
1046@@ -45,7 +46,6 @@
1047 'relation_set',
1048 'relation_get',
1049 'render_template',
1050- 'resolve_address',
1051 'shutil',
1052 'subprocess',
1053 'sys',
1054@@ -323,14 +323,22 @@
1055 cmd = ['service', 'radosgw', 'restart']
1056 self.subprocess.call.assert_called_with(cmd)
1057
1058- def test_identity_joined_early_version(self):
1059+ @patch('charmhelpers.contrib.openstack.ip.service_name',
1060+ lambda *args: 'ceph-radosgw')
1061+ @patch('charmhelpers.contrib.openstack.ip.config')
1062+ def test_identity_joined_early_version(self, _config):
1063 self.cmp_pkgrevno.return_value = -1
1064 ceph_hooks.identity_joined()
1065 self.sys.exit.assert_called_with(1)
1066
1067- def test_identity_joined(self):
1068+ @patch('charmhelpers.contrib.openstack.ip.service_name',
1069+ lambda *args: 'ceph-radosgw')
1070+ @patch('charmhelpers.contrib.openstack.ip.resolve_address')
1071+ @patch('charmhelpers.contrib.openstack.ip.config')
1072+ def test_identity_joined(self, _config, _resolve_address):
1073 self.cmp_pkgrevno.return_value = 1
1074- self.resolve_address.return_value = 'myserv'
1075+ _resolve_address.return_value = 'myserv'
1076+ _config.side_effect = self.test_config.get
1077 self.test_config.set('region', 'region1')
1078 self.test_config.set('operator-roles', 'admin')
1079 self.unit_get.return_value = 'myserv'
1080@@ -344,6 +352,27 @@
1081 relation_id='rid',
1082 admin_url='http://myserv:80/swift')
1083
1084+ @patch('charmhelpers.contrib.openstack.ip.service_name',
1085+ lambda *args: 'ceph-radosgw')
1086+ @patch('charmhelpers.contrib.openstack.ip.is_clustered')
1087+ @patch('charmhelpers.contrib.openstack.ip.unit_get')
1088+ @patch('charmhelpers.contrib.openstack.ip.config')
1089+ def test_identity_joined_public_name(self, _config, _unit_get,
1090+ _is_clustered):
1091+ _config.side_effect = self.test_config.get
1092+ self.test_config.set('os-public-hostname', 'files.example.com')
1093+ _unit_get.return_value = 'myserv'
1094+ _is_clustered.return_value = False
1095+ ceph_hooks.identity_joined(relid='rid')
1096+ self.relation_set.assert_called_with(
1097+ service='swift',
1098+ region='RegionOne',
1099+ public_url='http://files.example.com:80/swift/v1',
1100+ internal_url='http://myserv:80/swift/v1',
1101+ requested_roles='Member,Admin',
1102+ relation_id='rid',
1103+ admin_url='http://myserv:80/swift')
1104+
1105 def test_identity_changed(self):
1106 _emit_cephconf = self.patch('emit_cephconf')
1107 _restart = self.patch('restart')
1108@@ -351,10 +380,15 @@
1109 _emit_cephconf.assert_called()
1110 _restart.assert_called()
1111
1112- def test_canonical_url_ipv6(self):
1113+ @patch('charmhelpers.contrib.openstack.ip.is_clustered')
1114+ @patch('charmhelpers.contrib.openstack.ip.unit_get')
1115+ @patch('charmhelpers.contrib.openstack.ip.config')
1116+ def test_canonical_url_ipv6(self, _config, _unit_get, _is_clustered):
1117 ipv6_addr = '2001:db8:85a3:8d3:1319:8a2e:370:7348'
1118- self.resolve_address.return_value = ipv6_addr
1119- self.assertEquals(ceph_hooks.canonical_url({}),
1120+ _config.side_effect = self.test_config.get
1121+ _unit_get.return_value = ipv6_addr
1122+ _is_clustered.return_value = False
1123+ self.assertEquals(ceph_hooks.canonical_url({}, PUBLIC),
1124 'http://[%s]' % ipv6_addr)
1125
1126 @patch.object(ceph_hooks, 'CONFIGS')

Subscribers

People subscribed via source and target branches