Merge lp:~hopem/charms/trusty/ceph-radosgw/lp1513524 into lp:~openstack-charmers-archive/charms/trusty/ceph-radosgw/next

Proposed by Edward Hope-Morley
Status: Superseded
Proposed branch: lp:~hopem/charms/trusty/ceph-radosgw/lp1513524
Merge into: lp:~openstack-charmers-archive/charms/trusty/ceph-radosgw/next
Diff against target: 971 lines (+336/-275)
9 files modified
config.yaml (+12/-0)
hooks/ceph_radosgw_context.py (+110/-27)
hooks/hooks.py (+70/-86)
hooks/utils.py (+58/-33)
templates/ceph.conf (+3/-0)
templates/rgw.conf (+25/-0)
tests/basic_deployment.py (+26/-51)
unit_tests/test_ceph_radosgw_context.py (+32/-11)
unit_tests/test_hooks.py (+0/-67)
To merge this branch: bzr merge lp:~hopem/charms/trusty/ceph-radosgw/lp1513524
Reviewer Review Type Date Requested Status
Chris Holcombe (community) Approve
OpenStack Charmers Pending
Review via email: mp+285808@code.launchpad.net

This proposal has been superseded by a proposal from 2016-02-18.

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

charm_lint_check #574 ceph-radosgw-next for hopem mp285808
    LINT OK: passed

Build: http://10.245.162.36:8080/job/charm_lint_check/574/

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

charm_unit_test #488 ceph-radosgw-next for hopem mp285808
    UNIT OK: passed

Build: http://10.245.162.36:8080/job/charm_unit_test/488/

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

charm_amulet_test #235 ceph-radosgw-next for hopem mp285808
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 1
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/15073352/
Build: http://10.245.162.36:8080/job/charm_amulet_test/235/

63. By Edward Hope-Morley

fix amulet

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

charm_unit_test #489 ceph-radosgw-next for hopem mp285808
    UNIT OK: passed

Build: http://10.245.162.36:8080/job/charm_unit_test/489/

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

charm_lint_check #575 ceph-radosgw-next for hopem mp285808
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.

Full lint test output: http://paste.ubuntu.com/15074156/
Build: http://10.245.162.36:8080/job/charm_lint_check/575/

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

charm_amulet_test #236 ceph-radosgw-next for hopem mp285808
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 1
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/15074271/
Build: http://10.245.162.36:8080/job/charm_amulet_test/236/

64. By Edward Hope-Morley

fx amulet

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

charm_lint_check #576 ceph-radosgw-next for hopem mp285808
    LINT FAIL: lint-test failed

LINT Results (max last 2 lines):
make: *** [lint] Error 1
ERROR:root:Make target returned non-zero.

Full lint test output: http://paste.ubuntu.com/15074967/
Build: http://10.245.162.36:8080/job/charm_lint_check/576/

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

charm_unit_test #490 ceph-radosgw-next for hopem mp285808
    UNIT OK: passed

Build: http://10.245.162.36:8080/job/charm_unit_test/490/

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

charm_amulet_test #237 ceph-radosgw-next for hopem mp285808
    AMULET OK: passed

Build: http://10.245.162.36:8080/job/charm_amulet_test/237/

65. By Edward Hope-Morley

fix lint error

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

charm_lint_check #577 ceph-radosgw-next for hopem mp285808
    LINT OK: passed

Build: http://10.245.162.36:8080/job/charm_lint_check/577/

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

charm_unit_test #491 ceph-radosgw-next for hopem mp285808
    UNIT OK: passed

Build: http://10.245.162.36:8080/job/charm_unit_test/491/

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

charm_amulet_test #238 ceph-radosgw-next for hopem mp285808
    AMULET OK: passed

Build: http://10.245.162.36:8080/job/charm_amulet_test/238/

Revision history for this message
Chris Holcombe (xfactor973) wrote :

Hey Ed thanks for writing this patch! This looks great. My only comments are just little nits about catching errors and what you would like to do with them. You could handle them locally or raise but it would be best to be explicit about it. Other than that this looks good and I'm looking forward to trying it out

review: Needs Fixing
66. By Edward Hope-Morley

post-review fixups

Revision history for this message
Edward Hope-Morley (hopem) wrote :

Thanks for the review Chris! I've fixed up based on your comments and added some inline responses.

Revision history for this message
Chris Holcombe (xfactor973) wrote :

Cool. Lets get this merged!

review: Approve
67. By Edward Hope-Morley

sync /next

68. By Edward Hope-Morley

very minor edit

69. By Edward Hope-Morley

only write /etc/hosts if hostname not v6 resolvable

70. By Edward Hope-Morley

fix logs

71. By Edward Hope-Morley

sync /next

Unmerged revisions

71. By Edward Hope-Morley

sync /next

70. By Edward Hope-Morley

fix logs

69. By Edward Hope-Morley

only write /etc/hosts if hostname not v6 resolvable

68. By Edward Hope-Morley

very minor edit

67. By Edward Hope-Morley

sync /next

66. By Edward Hope-Morley

post-review fixups

65. By Edward Hope-Morley

fix lint error

64. By Edward Hope-Morley

fx amulet

63. By Edward Hope-Morley

fix amulet

62. By Edward Hope-Morley

[hopem,r=]

Add ipv6 support
Closes-Bug: 1513524

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'config.yaml'
--- config.yaml 2016-01-22 13:29:40 +0000
+++ config.yaml 2016-02-18 09:59:16 +0000
@@ -153,3 +153,15 @@
153 description: |153 description: |
154 Connect timeout configuration in ms for haproxy, used in HA154 Connect timeout configuration in ms for haproxy, used in HA
155 configurations. If not provided, default value of 5000ms is used.155 configurations. If not provided, default value of 5000ms is used.
156 prefer-ipv6:
157 type: boolean
158 default: False
159 description: |
160 If True enables IPv6 support. The charm will expect network interfaces
161 to be configured with an IPv6 address. If set to False (default) IPv4
162 is expected.
163 .
164 NOTE: these charms do not currently support IPv6 privacy extension. In
165 order for this charm to function correctly, the privacy extension must be
166 disabled and a non-temporary address must be configured/available on
167 your network interface.
156168
=== modified file 'hooks/ceph_radosgw_context.py'
--- hooks/ceph_radosgw_context.py 2016-01-11 12:21:07 +0000
+++ hooks/ceph_radosgw_context.py 2016-02-18 09:59:16 +0000
@@ -1,3 +1,11 @@
1import os
2import re
3import socket
4import tempfile
5import glob
6import shutil
7import subprocess
8
1from charmhelpers.contrib.openstack import context9from charmhelpers.contrib.openstack import context
2from charmhelpers.contrib.hahelpers.cluster import (10from charmhelpers.contrib.hahelpers.cluster import (
3 determine_api_port,11 determine_api_port,
@@ -5,17 +13,69 @@
5)13)
6from charmhelpers.core.host import cmp_pkgrevno14from charmhelpers.core.host import cmp_pkgrevno
7from charmhelpers.core.hookenv import (15from charmhelpers.core.hookenv import (
16 DEBUG,
8 WARNING,17 WARNING,
9 config,18 config,
10 log,19 log,
11 relation_ids,20 relation_ids,
12 related_units,21 related_units,
13 relation_get,22 relation_get,
14 unit_get,23 status_set,
15)24)
16import os25from charmhelpers.contrib.network.ip import (
17import socket26 format_ipv6_addr,
18import dns.resolver27 get_host_ip,
28 get_ipv6_addr,
29)
30
31
32def is_apache_24():
33 if os.path.exists('/etc/apache2/conf-available'):
34 return True
35 else:
36 return False
37
38
39class ApacheContext(context.OSContextGenerator):
40 interfaces = ['http']
41 service_namespace = 'ceph-radosgw'
42
43 def __call__(self):
44 ctxt = {}
45 if config('use-embedded-webserver'):
46 log("Skipping ApacheContext since we are using the embedded "
47 "webserver")
48 return {}
49
50 status_set('maintenance', 'configuring apache')
51
52 src = 'files/www/*'
53 dst = '/var/www/'
54 log("Installing www scripts", level=DEBUG)
55 try:
56 for x in glob.glob(src):
57 shutil.copy(x, dst)
58 except IOError as e:
59 log("Error copying files from '%s' to '%s': %s" % (src, dst, e),
60 level=WARNING)
61
62 try:
63 subprocess.check_call(['a2enmod', 'fastcgi'])
64 subprocess.check_call(['a2enmod', 'rewrite'])
65 except subprocess.CalledProcessError as e:
66 log("Error enabling apache modules - %s" % e, level=WARNING)
67
68 try:
69 if is_apache_24():
70 subprocess.check_call(['a2dissite', '000-default'])
71 else:
72 subprocess.check_call(['a2dissite', 'default'])
73 except subprocess.CalledProcessError as e:
74 log("Error disabling apache sites - %s" % e, level=WARNING)
75
76 ctxt['hostname'] = socket.gethostname()
77 ctxt['port'] = determine_api_port(config('port'), singlenode_mode=True)
78 return ctxt
1979
2080
21class HAProxyContext(context.HAProxyContext):81class HAProxyContext(context.HAProxyContext):
@@ -66,24 +126,50 @@
66 return {}126 return {}
67127
68128
129def ensure_host_resolvable_v6(hostname):
130 """Ensure that we can resolve our hostname to an IPv6 address by adding it
131 to /etc/hosts.
132 """
133 # This must be the backend address used by haproxy
134 host_addr = get_ipv6_addr(exc_list=[config('vip')])[0]
135 dtmp = tempfile.mkdtemp()
136 try:
137 tmp_hosts = os.path.join(dtmp, 'hosts')
138 shutil.copy('/etc/hosts', tmp_hosts)
139 with open(tmp_hosts, 'a+') as fd:
140 lines = fd.readlines()
141 for line in lines:
142 key = "^%s\s+" % (host_addr)
143 if re.search(key, line):
144 break
145 else:
146 fd.write("%s\t%s\n" % (host_addr, hostname))
147
148 os.rename(tmp_hosts, '/etc/hosts')
149 finally:
150 shutil.rmtree(dtmp)
151
152
69class MonContext(context.OSContextGenerator):153class MonContext(context.OSContextGenerator):
70 interfaces = ['ceph-radosgw']154 interfaces = ['ceph-radosgw']
71155
72 def __call__(self):156 def __call__(self):
73 if not relation_ids('mon'):157 if not relation_ids('mon'):
74 return {}158 return {}
75 hosts = []159 mon_hosts = []
76 auths = []160 auths = []
77 for relid in relation_ids('mon'):161 for relid in relation_ids('mon'):
78 for unit in related_units(relid):162 for unit in related_units(relid):
79 ceph_public_addr = relation_get('ceph-public-address', unit,163 ceph_public_addr = relation_get('ceph-public-address', unit,
80 relid)164 relid)
81 if ceph_public_addr:165 if ceph_public_addr:
82 host_ip = self.get_host_ip(ceph_public_addr)166 host_ip = format_ipv6_addr(ceph_public_addr) or \
83 hosts.append('{}:6789'.format(host_ip))167 get_host_ip(ceph_public_addr)
168 mon_hosts.append('{}:6789'.format(host_ip))
84 _auth = relation_get('auth', unit, relid)169 _auth = relation_get('auth', unit, relid)
85 if _auth:170 if _auth:
86 auths.append(_auth)171 auths.append(_auth)
172
87 if len(set(auths)) != 1:173 if len(set(auths)) != 1:
88 e = ("Inconsistent or absent auth returned by mon units. Setting "174 e = ("Inconsistent or absent auth returned by mon units. Setting "
89 "auth_supported to 'none'")175 "auth_supported to 'none'")
@@ -91,17 +177,28 @@
91 auth = 'none'177 auth = 'none'
92 else:178 else:
93 auth = auths[0]179 auth = auths[0]
94 hosts.sort()180
181 # /etc/init.d/radosgw mandates that a dns name is used for this
182 # parameter so ensure that address is resolvable
183 host = socket.gethostname()
184 if config('prefer-ipv6'):
185 ensure_host_resolvable_v6(host)
186
187 port = determine_apache_port(config('port'), singlenode_mode=True)
188 if config('prefer-ipv6'):
189 port = "[::]:%s" % (port)
190
191 mon_hosts.sort()
95 ctxt = {192 ctxt = {
96 'auth_supported': auth,193 'auth_supported': auth,
97 'mon_hosts': ' '.join(hosts),194 'mon_hosts': ' '.join(mon_hosts),
98 'hostname': socket.gethostname(),195 'hostname': format_ipv6_addr(host) or host,
99 'old_auth': cmp_pkgrevno('radosgw', "0.51") < 0,196 'old_auth': cmp_pkgrevno('radosgw', "0.51") < 0,
100 'use_syslog': str(config('use-syslog')).lower(),197 'use_syslog': str(config('use-syslog')).lower(),
101 'embedded_webserver': config('use-embedded-webserver'),198 'embedded_webserver': config('use-embedded-webserver'),
102 'loglevel': config('loglevel'),199 'loglevel': config('loglevel'),
103 'port': determine_apache_port(config('port'),200 'port': port,
104 singlenode_mode=True)201 'ipv6': config('prefer-ipv6')
105 }202 }
106203
107 certs_path = '/var/lib/ceph/nss'204 certs_path = '/var/lib/ceph/nss'
@@ -121,17 +218,3 @@
121 return ctxt218 return ctxt
122219
123 return {}220 return {}
124
125 def get_host_ip(self, hostname=None):
126 try:
127 if not hostname:
128 hostname = unit_get('private-address')
129 # Test to see if already an IPv4 address
130 socket.inet_aton(hostname)
131 return hostname
132 except socket.error:
133 # This may throw an NXDOMAIN exception; in which case
134 # things are badly broken so just let it kill the hook
135 answers = dns.resolver.query(hostname, 'A')
136 if answers:
137 return answers[0].address
138221
=== modified file 'hooks/hooks.py'
--- hooks/hooks.py 2016-01-22 13:29:40 +0000
+++ hooks/hooks.py 2016-02-18 09:59:16 +0000
@@ -1,17 +1,16 @@
1#!/usr/bin/python1#!/usr/bin/python
2
3#2#
4# Copyright 2012 Canonical Ltd.3# Copyright 2016 Canonical Ltd.
5#4#
6# Authors:5# Authors:
7# James Page <james.page@ubuntu.com>6# James Page <james.page@ubuntu.com>
7# Edward Hope-Morley <edward.hope-morley@canonical.com>
8#8#
99
10import shutil10import os
11import subprocess11import subprocess
12import sys12import sys
13import glob13
14import os
15import ceph14import ceph
1615
17from charmhelpers.core.hookenv import (16from charmhelpers.core.hookenv import (
@@ -39,27 +38,17 @@
39 lsb_release,38 lsb_release,
40 restart_on_change,39 restart_on_change,
41)40)
42from charmhelpers.contrib.hahelpers.cluster import (
43 determine_apache_port,
44)
45from utils import (
46 render_template,
47 enable_pocket,
48 is_apache_24,
49 CEPHRG_HA_RES,
50 register_configs,
51 REQUIRED_INTERFACES,
52 check_optional_relations,
53)
54from charmhelpers.payload.execd import execd_preinstall41from charmhelpers.payload.execd import execd_preinstall
55from charmhelpers.core.host import (42from charmhelpers.core.host import (
56 cmp_pkgrevno,43 cmp_pkgrevno,
57 mkdir,44 mkdir,
58)45)
59
60from charmhelpers.contrib.network.ip import (46from charmhelpers.contrib.network.ip import (
47 format_ipv6_addr,
48 get_ipv6_addr,
61 get_iface_for_address,49 get_iface_for_address,
62 get_netmask_for_address,50 get_netmask_for_address,
51 is_ipv6,
63)52)
64from charmhelpers.contrib.openstack.ip import (53from charmhelpers.contrib.openstack.ip import (
65 canonical_url,54 canonical_url,
@@ -72,18 +61,17 @@
72 send_request_if_needed,61 send_request_if_needed,
73 is_request_complete,62 is_request_complete,
74)63)
7564from utils import (
76APACHE_PORTS_CONF = '/etc/apache2/ports.conf'65 enable_pocket,
66 CEPHRG_HA_RES,
67 register_configs,
68 REQUIRED_INTERFACES,
69 check_optional_relations,
70 setup_ipv6,
71)
7772
78hooks = Hooks()73hooks = Hooks()
79CONFIGS = register_configs()74CONFIGS = register_configs()
80
81
82def install_www_scripts():
83 for x in glob.glob('files/www/*'):
84 shutil.copy(x, '/var/www/')
85
86
87NSS_DIR = '/var/lib/ceph/nss'75NSS_DIR = '/var/lib/ceph/nss'
8876
8977
@@ -145,43 +133,6 @@
145 os.makedirs('/etc/ceph')133 os.makedirs('/etc/ceph')
146134
147135
148def emit_apacheconf():
149 apachecontext = {
150 "hostname": unit_get('private-address'),
151 "port": determine_apache_port(config('port'), singlenode_mode=True)
152 }
153 site_conf = '/etc/apache2/sites-available/rgw'
154 if is_apache_24():
155 site_conf = '/etc/apache2/sites-available/rgw.conf'
156 with open(site_conf, 'w') as apacheconf:
157 apacheconf.write(render_template('rgw', apachecontext))
158
159
160def apache_sites():
161 if is_apache_24():
162 subprocess.check_call(['a2dissite', '000-default'])
163 else:
164 subprocess.check_call(['a2dissite', 'default'])
165 subprocess.check_call(['a2ensite', 'rgw'])
166
167
168def apache_modules():
169 subprocess.check_call(['a2enmod', 'fastcgi'])
170 subprocess.check_call(['a2enmod', 'rewrite'])
171
172
173def apache_reload():
174 subprocess.call(['service', 'apache2', 'reload'])
175
176
177def apache_ports():
178 portscontext = {
179 "port": determine_apache_port(config('port'), singlenode_mode=True)
180 }
181 with open(APACHE_PORTS_CONF, 'w') as portsconf:
182 portsconf.write(render_template('ports.conf', portscontext))
183
184
185def setup_keystone_certs(unit=None, rid=None):136def setup_keystone_certs(unit=None, rid=None):
186 """137 """
187 Get CA and signing certs from Keystone used to decrypt revoked token list.138 Get CA and signing certs from Keystone used to decrypt revoked token list.
@@ -208,6 +159,9 @@
208 for key in required_keys:159 for key in required_keys:
209 settings[key] = rdata.get(key)160 settings[key] = rdata.get(key)
210161
162 if is_ipv6(settings.get('auth_host')):
163 settings['auth_host'] = format_ipv6_addr(settings.get('auth_host'))
164
211 if not all(settings.values()):165 if not all(settings.values()):
212 log("Missing relation settings (%s) - skipping cert setup" %166 log("Missing relation settings (%s) - skipping cert setup" %
213 (', '.join([k for k in settings.keys() if not settings[k]])),167 (', '.join([k for k in settings.keys() if not settings[k]])),
@@ -283,18 +237,28 @@
283 '/etc/haproxy/haproxy.cfg': ['haproxy']})237 '/etc/haproxy/haproxy.cfg': ['haproxy']})
284def config_changed():238def config_changed():
285 install_packages()239 install_packages()
240
241 if config('prefer-ipv6'):
242 status_set('maintenance', 'configuring ipv6')
243 setup_ipv6()
244
245 for r_id in relation_ids('identity-service'):
246 identity_changed(relid=r_id)
247
248 for r_id in relation_ids('cluster'):
249 cluster_joined(rid=r_id)
250
286 CONFIGS.write_all()251 CONFIGS.write_all()
252
287 if not config('use-embedded-webserver'):253 if not config('use-embedded-webserver'):
288 status_set('maintenance', 'configuring apache')254 try:
289 emit_apacheconf()255 subprocess.check_call(['a2ensite', 'rgw'])
290 install_www_scripts()256 except subprocess.CalledProcessError as e:
291 apache_sites()257 log("Error enabling apache module 'rgw' - %s" % e, level=WARNING)
292 apache_modules()
293 apache_ports()
294 apache_reload()
295258
296 for r_id in relation_ids('identity-service'):259 # Ensure started but do a soft reload
297 identity_changed(relid=r_id)260 subprocess.call(['service', 'apache2', 'start'])
261 subprocess.call(['service', 'apache2', 'reload'])
298262
299263
300@hooks.hook('mon-relation-departed',264@hooks.hook('mon-relation-departed',
@@ -368,8 +332,18 @@
368 restart()332 restart()
369333
370334
371@hooks.hook('cluster-relation-changed',335@hooks.hook('cluster-relation-joined')
372 'cluster-relation-joined')336@restart_on_change({'/etc/haproxy/haproxy.cfg': ['haproxy']})
337def cluster_joined(rid=None):
338 settings = {}
339 if config('prefer-ipv6'):
340 private_addr = get_ipv6_addr(exc_list=[config('vip')])[0]
341 settings['private-address'] = private_addr
342
343 relation_set(relation_id=rid, **settings)
344
345
346@hooks.hook('cluster-relation-changed')
373@restart_on_change({'/etc/haproxy/haproxy.cfg': ['haproxy']})347@restart_on_change({'/etc/haproxy/haproxy.cfg': ['haproxy']})
374def cluster_changed():348def cluster_changed():
375 CONFIGS.write_all()349 CONFIGS.write_all()
@@ -379,17 +353,12 @@
379353
380@hooks.hook('ha-relation-joined')354@hooks.hook('ha-relation-joined')
381def ha_relation_joined():355def ha_relation_joined():
382 # Obtain the config values necessary for the cluster config. These
383 # include multicast port and interface to bind to.
384 corosync_bindiface = config('ha-bindiface')
385 corosync_mcastport = config('ha-mcastport')
386 vip = config('vip')356 vip = config('vip')
387 if not vip:357 if not vip:
388 log('Unable to configure hacluster as vip not provided',358 log('Unable to configure hacluster as vip not provided', level=ERROR)
389 level=ERROR)
390 sys.exit(1)359 sys.exit(1)
360
391 # Obtain resources361 # Obtain resources
392 # SWIFT_HA_RES = 'grp_swift_vips'
393 resources = {362 resources = {
394 'res_cephrg_haproxy': 'lsb:haproxy'363 'res_cephrg_haproxy': 'lsb:haproxy'
395 }364 }
@@ -399,15 +368,25 @@
399368
400 vip_group = []369 vip_group = []
401 for vip in vip.split():370 for vip in vip.split():
371 if is_ipv6(vip):
372 res_rgw_vip = 'ocf:heartbeat:IPv6addr'
373 vip_params = 'ipv6addr'
374 else:
375 res_rgw_vip = 'ocf:heartbeat:IPaddr2'
376 vip_params = 'ip'
377
402 iface = get_iface_for_address(vip)378 iface = get_iface_for_address(vip)
379 netmask = get_netmask_for_address(vip)
380
403 if iface is not None:381 if iface is not None:
404 vip_key = 'res_cephrg_{}_vip'.format(iface)382 vip_key = 'res_cephrg_{}_vip'.format(iface)
405 resources[vip_key] = 'ocf:heartbeat:IPaddr2'383 resources[vip_key] = res_rgw_vip
406 resource_params[vip_key] = (384 resource_params[vip_key] = (
407 'params ip="{vip}" cidr_netmask="{netmask}"'385 'params {ip}="{vip}" cidr_netmask="{netmask}"'
408 ' nic="{iface}"'.format(vip=vip,386 ' nic="{iface}"'.format(ip=vip_params,
387 vip=vip,
409 iface=iface,388 iface=iface,
410 netmask=get_netmask_for_address(vip))389 netmask=netmask)
411 )390 )
412 vip_group.append(vip_key)391 vip_group.append(vip_key)
413392
@@ -421,6 +400,11 @@
421 'cl_cephrg_haproxy': 'res_cephrg_haproxy'400 'cl_cephrg_haproxy': 'res_cephrg_haproxy'
422 }401 }
423402
403 # Obtain the config values necessary for the cluster config. These
404 # include multicast port and interface to bind to.
405 corosync_bindiface = config('ha-bindiface')
406 corosync_mcastport = config('ha-mcastport')
407
424 relation_set(init_services=init_services,408 relation_set(init_services=init_services,
425 corosync_bindiface=corosync_bindiface,409 corosync_bindiface=corosync_bindiface,
426 corosync_mcastport=corosync_mcastport,410 corosync_mcastport=corosync_mcastport,
427411
=== modified file 'hooks/utils.py'
--- hooks/utils.py 2015-10-12 10:56:01 +0000
+++ hooks/utils.py 2016-02-18 09:59:16 +0000
@@ -1,26 +1,43 @@
1#1#
2# Copyright 2012 Canonical Ltd.2# Copyright 2016 Canonical Ltd.
3#3#
4# Authors:4# Authors:
5# James Page <james.page@ubuntu.com>5# James Page <james.page@ubuntu.com>
6# Paul Collins <paul.collins@canonical.com>6# Paul Collins <paul.collins@canonical.com>
7# Edward Hope-Morley <edward.hope-morley@canonical.com>
7#8#
89
9import socket
10import re10import re
11import os
12import dns.resolver
13import jinja211import jinja2
12
14from copy import deepcopy13from copy import deepcopy
15from collections import OrderedDict14from collections import OrderedDict
16from charmhelpers.core.hookenv import unit_get, relation_ids, status_get15
17from charmhelpers.contrib.openstack import context, templating16import ceph_radosgw_context
18from charmhelpers.contrib.openstack.utils import set_os_workload_status17
18from charmhelpers.core.hookenv import (
19 relation_ids,
20 status_get,
21)
22from charmhelpers.contrib.openstack import (
23 context,
24 templating,
25)
26from charmhelpers.contrib.openstack.utils import (
27 os_release,
28 set_os_workload_status,
29)
19from charmhelpers.contrib.hahelpers.cluster import get_hacluster_config30from charmhelpers.contrib.hahelpers.cluster import get_hacluster_config
20from charmhelpers.core.host import cmp_pkgrevno31from charmhelpers.core.host import (
21from charmhelpers.fetch import filter_installed_packages32 cmp_pkgrevno,
2233 lsb_release,
23import ceph_radosgw_context34)
35from charmhelpers.fetch import (
36 apt_install,
37 apt_update,
38 add_source,
39 filter_installed_packages,
40)
2441
25# The interface is said to be satisfied if anyone of the interfaces in the42# The interface is said to be satisfied if anyone of the interfaces in the
26# list has a complete context.43# list has a complete context.
@@ -32,6 +49,9 @@
32TEMPLATES = 'templates/'49TEMPLATES = 'templates/'
33HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'50HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
34CEPH_CONF = '/etc/ceph/ceph.conf'51CEPH_CONF = '/etc/ceph/ceph.conf'
52APACHE_CONF = '/etc/apache2/sites-available/rgw'
53APACHE_24_CONF = '/etc/apache2/sites-available/rgw.conf'
54APACHE_PORTS_CONF = '/etc/apache2/ports.conf'
3555
36BASE_RESOURCE_MAP = OrderedDict([56BASE_RESOURCE_MAP = OrderedDict([
37 (HAPROXY_CONF, {57 (HAPROXY_CONF, {
@@ -39,6 +59,18 @@
39 ceph_radosgw_context.HAProxyContext()],59 ceph_radosgw_context.HAProxyContext()],
40 'services': ['haproxy'],60 'services': ['haproxy'],
41 }),61 }),
62 (APACHE_CONF, {
63 'contexts': [ceph_radosgw_context.ApacheContext()],
64 'services': ['apache2'],
65 }),
66 (APACHE_24_CONF, {
67 'contexts': [ceph_radosgw_context.ApacheContext()],
68 'services': ['apache2'],
69 }),
70 (APACHE_PORTS_CONF, {
71 'contexts': [ceph_radosgw_context.ApacheContext()],
72 'services': ['apache2'],
73 }),
42 (CEPH_CONF, {74 (CEPH_CONF, {
43 'contexts': [ceph_radosgw_context.MonContext()],75 'contexts': [ceph_radosgw_context.MonContext()],
44 'services': ['radosgw'],76 'services': ['radosgw'],
@@ -92,28 +124,6 @@
92 sources.write(line)124 sources.write(line)
93125
94126
95def get_host_ip(hostname=None):
96 try:
97 if not hostname:
98 hostname = unit_get('private-address')
99 # Test to see if already an IPv4 address
100 socket.inet_aton(hostname)
101 return hostname
102 except socket.error:
103 # This may throw an NXDOMAIN exception; in which case
104 # things are badly broken so just let it kill the hook
105 answers = dns.resolver.query(hostname, 'A')
106 if answers:
107 return answers[0].address
108
109
110def is_apache_24():
111 if os.path.exists('/etc/apache2/conf-available'):
112 return True
113 else:
114 return False
115
116
117def check_optional_relations(configs):127def check_optional_relations(configs):
118 required_interfaces = {}128 required_interfaces = {}
119 if relation_ids('ha'):129 if relation_ids('ha'):
@@ -132,3 +142,18 @@
132 return status_get()142 return status_get()
133 else:143 else:
134 return 'unknown', 'No optional relations'144 return 'unknown', 'No optional relations'
145
146
147def setup_ipv6():
148 ubuntu_rel = lsb_release()['DISTRIB_CODENAME'].lower()
149 if ubuntu_rel < "trusty":
150 raise Exception("IPv6 is not supported in the charms for Ubuntu "
151 "versions less than Trusty 14.04")
152
153 # Need haproxy >= 1.5.3 for ipv6 so for Trusty if we are <= Kilo we need to
154 # use trusty-backports otherwise we can use the UCA.
155 if ubuntu_rel == 'trusty' and os_release('ceph-common') < 'liberty':
156 add_source('deb http://archive.ubuntu.com/ubuntu trusty-backports '
157 'main')
158 apt_update()
159 apt_install('haproxy/trusty-backports', fatal=True)
135160
=== modified file 'templates/ceph.conf'
--- templates/ceph.conf 2016-01-11 12:21:07 +0000
+++ templates/ceph.conf 2016-02-18 09:59:16 +0000
@@ -11,6 +11,9 @@
11err to syslog = {{ use_syslog }}11err to syslog = {{ use_syslog }}
12clog to syslog = {{ use_syslog }}12clog to syslog = {{ use_syslog }}
13debug rgw = {{ loglevel }}/513debug rgw = {{ loglevel }}/5
14{% if ipv6 -%}
15ms bind ipv6 = true
16{% endif %}
1417
15[client.radosgw.gateway]18[client.radosgw.gateway]
16host = {{ hostname }}19host = {{ hostname }}
1720
=== added file 'templates/rgw.conf'
--- templates/rgw.conf 1970-01-01 00:00:00 +0000
+++ templates/rgw.conf 2016-02-18 09:59:16 +0000
@@ -0,0 +1,25 @@
1<IfModule mod_fastcgi.c>
2 FastCgiExternalServer /var/www/s3gw.fcgi -socket /tmp/radosgw.sock
3</IfModule>
4
5<VirtualHost *:{{ port }}>
6 ServerName {{ hostname }}
7 ServerAdmin ceph@ubuntu.com
8 DocumentRoot /var/www
9 RewriteEngine On
10 RewriteRule ^/([a-zA-Z0-9-_.]*)([/]?.*) /s3gw.fcgi?page=$1&params=$2&%{QUERY_STRING} [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]
11 <IfModule mod_fastcgi.c>
12 <Directory /var/www>
13 Options +ExecCGI
14 AllowOverride All
15 SetHandler fastcgi-script
16 Order allow,deny
17 Allow from all
18 AuthBasicAuthoritative Off
19 </Directory>
20 </IfModule>
21 AllowEncodedSlashes On
22 ErrorLog /var/log/apache2/error.log
23 CustomLog /var/log/apache2/access.log combined
24 ServerSignature Off
25</VirtualHost>
026
=== modified file 'tests/basic_deployment.py'
--- tests/basic_deployment.py 2015-07-02 15:15:08 +0000
+++ tests/basic_deployment.py 2016-02-18 09:59:16 +0000
@@ -228,58 +228,33 @@
228 message = u.relation_error('ceph-radosgw to ceph', ret)228 message = u.relation_error('ceph-radosgw to ceph', ret)
229 amulet.raise_status(amulet.FAIL, msg=message)229 amulet.raise_status(amulet.FAIL, msg=message)
230230
231 def test_201_ceph0_ceph_radosgw_relation(self):231 def test_201_ceph_radosgw_relation(self):
232 """Verify the ceph0 to ceph-radosgw relation data."""232 """Verify the ceph to ceph-radosgw relation data.
233
234 At least one unit (the leader) must have all data provided by the ceph
235 charm.
236 """
233 u.log.debug('Checking ceph0:radosgw radosgw:mon relation data...')237 u.log.debug('Checking ceph0:radosgw radosgw:mon relation data...')
234 unit = self.ceph0_sentry238 s_entries = [
235 relation = ['radosgw', 'ceph-radosgw:mon']239 self.ceph0_sentry,
236 expected = {240 self.ceph1_sentry,
237 'private-address': u.valid_ip,241 self.ceph2_sentry
238 'radosgw_key': u.not_null,242 ]
239 'auth': 'none',243 relation = ['radosgw', 'ceph-radosgw:mon']
240 'ceph-public-address': u.valid_ip,244 expected = {
241 'fsid': u'6547bd3e-1397-11e2-82e5-53567c8d32dc'245 'private-address': u.valid_ip,
242 }246 'radosgw_key': u.not_null,
243247 'auth': 'none',
244 ret = u.validate_relation_data(unit, relation, expected)248 'ceph-public-address': u.valid_ip,
245 if ret:249 'fsid': u'6547bd3e-1397-11e2-82e5-53567c8d32dc'
246 message = u.relation_error('ceph0 to ceph-radosgw', ret)250 }
247 amulet.raise_status(amulet.FAIL, msg=message)251
248252 ret = []
249 def test_202_ceph1_ceph_radosgw_relation(self):253 for unit in s_entries:
250 """Verify the ceph1 to ceph-radosgw relation data."""254 ret.append(u.validate_relation_data(unit, relation, expected))
251 u.log.debug('Checking ceph1:radosgw ceph-radosgw:mon relation data...')255
252 unit = self.ceph1_sentry256 if not any(ret):
253 relation = ['radosgw', 'ceph-radosgw:mon']257 message = u.relation_error('ceph to ceph-radosgw', ret)
254 expected = {
255 'private-address': u.valid_ip,
256 'radosgw_key': u.not_null,
257 'auth': 'none',
258 'ceph-public-address': u.valid_ip,
259 'fsid': u'6547bd3e-1397-11e2-82e5-53567c8d32dc'
260 }
261
262 ret = u.validate_relation_data(unit, relation, expected)
263 if ret:
264 message = u.relation_error('ceph1 to ceph-radosgw', ret)
265 amulet.raise_status(amulet.FAIL, msg=message)
266
267 def test_203_ceph2_ceph_radosgw_relation(self):
268 """Verify the ceph2 to ceph-radosgw relation data."""
269 u.log.debug('Checking ceph2:radosgw ceph-radosgw:mon relation data...')
270 unit = self.ceph2_sentry
271 relation = ['radosgw', 'ceph-radosgw:mon']
272 expected = {
273 'private-address': u.valid_ip,
274 'radosgw_key': u.not_null,
275 'auth': 'none',
276 'ceph-public-address': u.valid_ip,
277 'fsid': u'6547bd3e-1397-11e2-82e5-53567c8d32dc'
278 }
279
280 ret = u.validate_relation_data(unit, relation, expected)
281 if ret:
282 message = u.relation_error('ceph2 to ceph-radosgw', ret)
283 amulet.raise_status(amulet.FAIL, msg=message)258 amulet.raise_status(amulet.FAIL, msg=message)
284259
285 def test_204_ceph_radosgw_keystone_relation(self):260 def test_204_ceph_radosgw_keystone_relation(self):
286261
=== modified file 'unit_tests/test_ceph_radosgw_context.py'
--- unit_tests/test_ceph_radosgw_context.py 2016-01-11 12:21:07 +0000
+++ unit_tests/test_ceph_radosgw_context.py 2016-02-18 09:59:16 +0000
@@ -13,6 +13,7 @@
13 'related_units',13 'related_units',
14 'cmp_pkgrevno',14 'cmp_pkgrevno',
15 'socket',15 'socket',
16 'is_apache_24',
16]17]
1718
1819
@@ -146,8 +147,9 @@
146 super(MonContextTest, self).setUp(context, TO_PATCH)147 super(MonContextTest, self).setUp(context, TO_PATCH)
147 self.config.side_effect = self.test_config.get148 self.config.side_effect = self.test_config.get
148149
149 def test_ctxt(self):150 @patch.object(context, 'ensure_host_resolvable_v6')
150 self.socket.gethostname.return_value = '10.0.0.10'151 def test_ctxt(self, mock_ensure_rsv_v6):
152 self.socket.gethostname.return_value = 'testhost'
151 mon_ctxt = context.MonContext()153 mon_ctxt = context.MonContext()
152 addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3']154 addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3']
153155
@@ -156,6 +158,7 @@
156 return addresses.pop()158 return addresses.pop()
157 elif attr == 'auth':159 elif attr == 'auth':
158 return 'cephx'160 return 'cephx'
161
159 self.relation_get.side_effect = _relation_get162 self.relation_get.side_effect = _relation_get
160 self.relation_ids.return_value = ['mon:6']163 self.relation_ids.return_value = ['mon:6']
161 self.related_units.return_value = ['ceph/0', 'ceph/1', 'ceph/2']164 self.related_units.return_value = ['ceph/0', 'ceph/1', 'ceph/2']
@@ -163,17 +166,26 @@
163 'auth_supported': 'cephx',166 'auth_supported': 'cephx',
164 'embedded_webserver': False,167 'embedded_webserver': False,
165 'disable_100_continue': True,168 'disable_100_continue': True,
166 'hostname': '10.0.0.10',169 'hostname': 'testhost',
167 'mon_hosts': '10.5.4.1:6789 10.5.4.2:6789 10.5.4.3:6789',170 'mon_hosts': '10.5.4.1:6789 10.5.4.2:6789 10.5.4.3:6789',
168 'old_auth': False,171 'old_auth': False,
169 'use_syslog': 'false',172 'use_syslog': 'false',
170 'loglevel': 1,173 'loglevel': 1,
171 'port': 70174 'port': 70,
175 'ipv6': False
172 }176 }
173 self.assertEqual(expect, mon_ctxt())177 self.assertEqual(expect, mon_ctxt())
178 self.assertFalse(mock_ensure_rsv_v6.called)
179
180 self.test_config.set('prefer-ipv6', True)
181 addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3']
182 expect['ipv6'] = True
183 expect['port'] = "[::]:%s" % (70)
184 self.assertEqual(expect, mon_ctxt())
185 self.assertTrue(mock_ensure_rsv_v6.called)
174186
175 def test_ctxt_missing_data(self):187 def test_ctxt_missing_data(self):
176 self.socket.gethostname.return_value = '10.0.0.10'188 self.socket.gethostname.return_value = 'testhost'
177 mon_ctxt = context.MonContext()189 mon_ctxt = context.MonContext()
178 self.relation_get.return_value = None190 self.relation_get.return_value = None
179 self.relation_ids.return_value = ['mon:6']191 self.relation_ids.return_value = ['mon:6']
@@ -181,7 +193,7 @@
181 self.assertEqual({}, mon_ctxt())193 self.assertEqual({}, mon_ctxt())
182194
183 def test_ctxt_inconsistent_auths(self):195 def test_ctxt_inconsistent_auths(self):
184 self.socket.gethostname.return_value = '10.0.0.10'196 self.socket.gethostname.return_value = 'testhost'
185 mon_ctxt = context.MonContext()197 mon_ctxt = context.MonContext()
186 addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3']198 addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3']
187 auths = ['cephx', 'cephy', 'cephz']199 auths = ['cephx', 'cephy', 'cephz']
@@ -198,17 +210,18 @@
198 'auth_supported': 'none',210 'auth_supported': 'none',
199 'embedded_webserver': False,211 'embedded_webserver': False,
200 'disable_100_continue': True,212 'disable_100_continue': True,
201 'hostname': '10.0.0.10',213 'hostname': 'testhost',
202 'mon_hosts': '10.5.4.1:6789 10.5.4.2:6789 10.5.4.3:6789',214 'mon_hosts': '10.5.4.1:6789 10.5.4.2:6789 10.5.4.3:6789',
203 'old_auth': False,215 'old_auth': False,
204 'use_syslog': 'false',216 'use_syslog': 'false',
205 'loglevel': 1,217 'loglevel': 1,
206 'port': 70218 'port': 70,
219 'ipv6': False
207 }220 }
208 self.assertEqual(expect, mon_ctxt())221 self.assertEqual(expect, mon_ctxt())
209222
210 def test_ctxt_consistent_auths(self):223 def test_ctxt_consistent_auths(self):
211 self.socket.gethostname.return_value = '10.0.0.10'224 self.socket.gethostname.return_value = 'testhost'
212 mon_ctxt = context.MonContext()225 mon_ctxt = context.MonContext()
213 addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3']226 addresses = ['10.5.4.1', '10.5.4.2', '10.5.4.3']
214 auths = ['cephx', 'cephx', 'cephx']227 auths = ['cephx', 'cephx', 'cephx']
@@ -225,11 +238,19 @@
225 'auth_supported': 'cephx',238 'auth_supported': 'cephx',
226 'embedded_webserver': False,239 'embedded_webserver': False,
227 'disable_100_continue': True,240 'disable_100_continue': True,
228 'hostname': '10.0.0.10',241 'hostname': 'testhost',
229 'mon_hosts': '10.5.4.1:6789 10.5.4.2:6789 10.5.4.3:6789',242 'mon_hosts': '10.5.4.1:6789 10.5.4.2:6789 10.5.4.3:6789',
230 'old_auth': False,243 'old_auth': False,
231 'use_syslog': 'false',244 'use_syslog': 'false',
232 'loglevel': 1,245 'loglevel': 1,
233 'port': 70246 'port': 70,
247 'ipv6': False
234 }248 }
235 self.assertEqual(expect, mon_ctxt())249 self.assertEqual(expect, mon_ctxt())
250
251
252class ApacheContextTest(CharmTestCase):
253
254 def setUp(self):
255 super(ApacheContextTest, self).setUp(context, TO_PATCH)
256 self.config.side_effect = self.test_config.get
236257
=== modified file 'unit_tests/test_hooks.py'
--- unit_tests/test_hooks.py 2016-01-22 13:29:40 +0000
+++ unit_tests/test_hooks.py 2016-02-18 09:59:16 +0000
@@ -6,7 +6,6 @@
66
7from test_utils import (7from test_utils import (
8 CharmTestCase,8 CharmTestCase,
9 patch_open
10)9)
11from charmhelpers.contrib.openstack.ip import PUBLIC10from charmhelpers.contrib.openstack.ip import PUBLIC
1211
@@ -34,8 +33,6 @@
34 'enable_pocket',33 'enable_pocket',
35 'get_iface_for_address',34 'get_iface_for_address',
36 'get_netmask_for_address',35 'get_netmask_for_address',
37 'glob',
38 'is_apache_24',
39 'log',36 'log',
40 'lsb_release',37 'lsb_release',
41 'open_port',38 'open_port',
@@ -44,8 +41,6 @@
44 'relation_set',41 'relation_set',
45 'relation_get',42 'relation_get',
46 'related_units',43 'related_units',
47 'render_template',
48 'shutil',
49 'status_set',44 'status_set',
50 'subprocess',45 'subprocess',
51 'sys',46 'sys',
@@ -62,11 +57,6 @@
62 self.test_config.set('key', 'secretkey')57 self.test_config.set('key', 'secretkey')
63 self.test_config.set('use-syslog', False)58 self.test_config.set('use-syslog', False)
6459
65 def test_install_www_scripts(self):
66 self.glob.glob.return_value = ['files/www/bob']
67 ceph_hooks.install_www_scripts()
68 self.shutil.copy.assert_called_with('files/www/bob', '/var/www/')
69
70 def test_install_ceph_optimised_packages(self):60 def test_install_ceph_optimised_packages(self):
71 self.lsb_release.return_value = {'DISTRIB_CODENAME': 'vivid'}61 self.lsb_release.return_value = {'DISTRIB_CODENAME': 'vivid'}
72 fastcgi_source = (62 fastcgi_source = (
@@ -122,69 +112,12 @@
122 self.enable_pocket.assert_called_with('multiverse')112 self.enable_pocket.assert_called_with('multiverse')
123 self.os.makedirs.called_with('/var/lib/ceph/nss')113 self.os.makedirs.called_with('/var/lib/ceph/nss')
124114
125 def test_emit_apacheconf(self):
126 self.is_apache_24.return_value = True
127 self.unit_get.return_value = '10.0.0.1'
128 apachecontext = {
129 "hostname": '10.0.0.1',
130 "port": 70,
131 }
132 vhost_file = '/etc/apache2/sites-available/rgw.conf'
133 with patch_open() as (_open, _file):
134 ceph_hooks.emit_apacheconf()
135 _open.assert_called_with(vhost_file, 'w')
136 self.render_template.assert_called_with('rgw', apachecontext)
137
138 def test_apache_sites24(self):
139 self.is_apache_24.return_value = True
140 ceph_hooks.apache_sites()
141 calls = [
142 call(['a2dissite', '000-default']),
143 call(['a2ensite', 'rgw']),
144 ]
145 self.subprocess.check_call.assert_has_calls(calls)
146
147 def test_apache_sites22(self):
148 self.is_apache_24.return_value = False
149 ceph_hooks.apache_sites()
150 calls = [
151 call(['a2dissite', 'default']),
152 call(['a2ensite', 'rgw']),
153 ]
154 self.subprocess.check_call.assert_has_calls(calls)
155
156 def test_apache_modules(self):
157 ceph_hooks.apache_modules()
158 calls = [
159 call(['a2enmod', 'fastcgi']),
160 call(['a2enmod', 'rewrite']),
161 ]
162 self.subprocess.check_call.assert_has_calls(calls)
163
164 def test_apache_reload(self):
165 ceph_hooks.apache_reload()
166 calls = [
167 call(['service', 'apache2', 'reload']),
168 ]
169 self.subprocess.call.assert_has_calls(calls)
170
171 @patch.object(ceph_hooks, 'apache_ports', lambda *args: True)
172 @patch.object(ceph_hooks, 'mkdir', lambda *args: None)115 @patch.object(ceph_hooks, 'mkdir', lambda *args: None)
173 def test_config_changed(self):116 def test_config_changed(self):
174 _install_packages = self.patch('install_packages')117 _install_packages = self.patch('install_packages')
175 _emit_apacheconf = self.patch('emit_apacheconf')
176 _install_www_scripts = self.patch('install_www_scripts')
177 _apache_sites = self.patch('apache_sites')
178 _apache_modules = self.patch('apache_modules')
179 _apache_reload = self.patch('apache_reload')
180 ceph_hooks.config_changed()118 ceph_hooks.config_changed()
181 self.assertTrue(_install_packages.called)119 self.assertTrue(_install_packages.called)
182 self.CONFIGS.write_all.assert_called_with()120 self.CONFIGS.write_all.assert_called_with()
183 self.assertTrue(_emit_apacheconf.called)
184 self.assertTrue(_install_www_scripts.called)
185 self.assertTrue(_apache_sites.called)
186 self.assertTrue(_apache_modules.called)
187 self.assertTrue(_apache_reload.called)
188121
189 @patch.object(ceph_hooks, 'is_request_complete',122 @patch.object(ceph_hooks, 'is_request_complete',
190 lambda *args, **kwargs: True)123 lambda *args, **kwargs: True)

Subscribers

People subscribed via source and target branches