Merge lp:~james-page/charms/trusty/percona-cluster/network-splits into lp:~openstack-charmers-archive/charms/trusty/percona-cluster/trunk
- Trusty Tahr (14.04)
- network-splits
- Merge into trunk
Proposed by
James Page
Status: | Merged |
---|---|
Merged at revision: | 34 |
Proposed branch: | lp:~james-page/charms/trusty/percona-cluster/network-splits |
Merge into: | lp:~openstack-charmers-archive/charms/trusty/percona-cluster/trunk |
Diff against target: |
556 lines (+287/-74) 11 files modified
.bzrignore (+2/-0) Makefile (+10/-7) charm-helpers.yaml (+1/-0) config.yaml (+45/-36) hooks/charmhelpers/contrib/hahelpers/cluster.py (+3/-2) hooks/charmhelpers/contrib/network/ip.py (+156/-0) hooks/charmhelpers/core/hookenv.py (+5/-4) hooks/charmhelpers/core/host.py (+11/-5) hooks/charmhelpers/fetch/__init__.py (+23/-15) hooks/percona_hooks.py (+30/-4) hooks/percona_utils.py (+1/-1) |
To merge this branch: | bzr merge lp:~james-page/charms/trusty/percona-cluster/network-splits |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Liam Young (community) | Approve | ||
James Page | Needs Resubmitting | ||
Review via email: mp+228151@code.launchpad.net |
Commit message
Description of the change
Add support for separate 'access-network' configuration.
To post a comment you must log in.
Revision history for this message
James Page (james-page) : | # |
review:
Needs Resubmitting
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file '.bzrignore' |
2 | --- .bzrignore 1970-01-01 00:00:00 +0000 |
3 | +++ .bzrignore 2014-07-24 15:25:17 +0000 |
4 | @@ -0,0 +1,2 @@ |
5 | +bin |
6 | +.coverage |
7 | |
8 | === modified file 'Makefile' |
9 | --- Makefile 2013-09-20 13:04:42 +0000 |
10 | +++ Makefile 2014-07-24 15:25:17 +0000 |
11 | @@ -3,12 +3,15 @@ |
12 | |
13 | lint: |
14 | @flake8 --exclude hooks/charmhelpers hooks |
15 | - #@flake8 --exclude hooks/charmhelpers unit_tests |
16 | @charm proof |
17 | |
18 | -test: |
19 | - @echo Starting tests... |
20 | - @$(PYTHON) /usr/bin/nosetests --nologcapture unit_tests |
21 | - |
22 | -sync: |
23 | - @charm-helper-sync -c charm-helpers-sync.yaml |
24 | +bin/charm_helpers_sync.py: |
25 | + @mkdir -p bin |
26 | + @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \ |
27 | + > bin/charm_helpers_sync.py |
28 | + |
29 | +sync: bin/charm_helpers_sync.py |
30 | + @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml |
31 | + |
32 | +publish: lint |
33 | + bzr push lp:charms/trusty/percona-cluster |
34 | |
35 | === renamed file 'charm-helpers-sync.yaml' => 'charm-helpers.yaml' |
36 | --- charm-helpers-sync.yaml 2014-06-23 09:47:35 +0000 |
37 | +++ charm-helpers.yaml 2014-07-24 15:25:17 +0000 |
38 | @@ -6,3 +6,4 @@ |
39 | - contrib.hahelpers.cluster |
40 | - contrib.peerstorage |
41 | - payload.execd |
42 | + - contrib.network.ip |
43 | |
44 | === modified file 'config.yaml' |
45 | --- config.yaml 2014-01-13 16:07:19 +0000 |
46 | +++ config.yaml 2014-07-24 15:25:17 +0000 |
47 | @@ -1,37 +1,46 @@ |
48 | options: |
49 | - source: |
50 | - type: string |
51 | - description: Package install location for Percona XtraDB Cluster (defaults to distro for >= 14.04) |
52 | - dataset-size: |
53 | - default: '80%' |
54 | - type: string |
55 | - description: How much data do you want to keep in memory in the DB. This will be used to tune settings in the database server appropriately. Suffix this value with 'K','M','G', or 'T' to get the relevant kilo/mega/etc. bytes. If suffixed with %, one will get that percentage of RAM devoted to dataset. |
56 | - max-connections: |
57 | - default: -1 |
58 | - type: int |
59 | - description: Maximum connections to allow. -1 means use the server's compiled in default. |
60 | - root-password: |
61 | - type: string |
62 | - description: Root password for MySQL access; must be configured pre-deployment for Active-Active clusters. |
63 | - sst-password: |
64 | - type: string |
65 | - description: Re-sync account password for new cluster nodes; must be configured pre-deployment for Active-Active clusters. |
66 | - vip: |
67 | - type: string |
68 | - description: Virtual IP to use to front Percona XtraDB Cluster in active/active HA configuration |
69 | - vip_iface: |
70 | - type: string |
71 | - default: eth0 |
72 | - description: Network interface on which to place the Virtual IP |
73 | - vip_cidr: |
74 | - type: int |
75 | - default: 24 |
76 | - description: Netmask that will be used for the Virtual IP |
77 | - ha-bindiface: |
78 | - type: string |
79 | - default: eth0 |
80 | - description: Default network interface on which HA cluster will bind to communication with the other members of the HA Cluster. |
81 | - ha-mcastport: |
82 | - type: int |
83 | - default: 5490 |
84 | - description: Default multicast port number that will be used to communicate between HA Cluster nodes. |
85 | + source: |
86 | + type: string |
87 | + description: Package install location for Percona XtraDB Cluster (defaults to distro for >= 14.04) |
88 | + dataset-size: |
89 | + default: '80%' |
90 | + type: string |
91 | + description: How much data do you want to keep in memory in the DB. This will be used to tune settings in the database server appropriately. Suffix this value with 'K','M','G', or 'T' to get the relevant kilo/mega/etc. bytes. If suffixed with %, one will get that percentage of RAM devoted to dataset. |
92 | + max-connections: |
93 | + default: -1 |
94 | + type: int |
95 | + description: Maximum connections to allow. -1 means use the server's compiled in default. |
96 | + root-password: |
97 | + type: string |
98 | + description: Root password for MySQL access; must be configured pre-deployment for Active-Active clusters. |
99 | + sst-password: |
100 | + type: string |
101 | + description: Re-sync account password for new cluster nodes; must be configured pre-deployment for Active-Active clusters. |
102 | + vip: |
103 | + type: string |
104 | + description: Virtual IP to use to front Percona XtraDB Cluster in active/active HA configuration |
105 | + vip_iface: |
106 | + type: string |
107 | + default: eth0 |
108 | + description: Network interface on which to place the Virtual IP |
109 | + vip_cidr: |
110 | + type: int |
111 | + default: 24 |
112 | + description: Netmask that will be used for the Virtual IP |
113 | + ha-bindiface: |
114 | + type: string |
115 | + default: eth0 |
116 | + description: Default network interface on which HA cluster will bind to communication with the other members of the HA Cluster. |
117 | + ha-mcastport: |
118 | + type: int |
119 | + default: 5490 |
120 | + description: Default multicast port number that will be used to communicate between HA Cluster nodes. |
121 | + # Network configuration options |
122 | + # by default all access is over 'private-address' |
123 | + access-network: |
124 | + type: string |
125 | + description: | |
126 | + The IP address and netmask of the 'access' network (e.g., 192.168.0.0/24) |
127 | + . |
128 | + This network will be used for access to database services. |
129 | + |
130 | |
131 | === modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py' |
132 | --- hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-03-07 10:20:46 +0000 |
133 | +++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-07-24 15:25:17 +0000 |
134 | @@ -146,12 +146,12 @@ |
135 | Obtains all relevant configuration from charm configuration required |
136 | for initiating a relation to hacluster: |
137 | |
138 | - ha-bindiface, ha-mcastport, vip, vip_iface, vip_cidr |
139 | + ha-bindiface, ha-mcastport, vip |
140 | |
141 | returns: dict: A dict containing settings keyed by setting name. |
142 | raises: HAIncompleteConfig if settings are missing. |
143 | ''' |
144 | - settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'vip_iface', 'vip_cidr'] |
145 | + settings = ['ha-bindiface', 'ha-mcastport', 'vip'] |
146 | conf = {} |
147 | for setting in settings: |
148 | conf[setting] = config_get(setting) |
149 | @@ -170,6 +170,7 @@ |
150 | |
151 | :configs : OSTemplateRenderer: A config tempating object to inspect for |
152 | a complete https context. |
153 | + |
154 | :vip_setting: str: Setting in charm config that specifies |
155 | VIP address. |
156 | ''' |
157 | |
158 | === added directory 'hooks/charmhelpers/contrib/network' |
159 | === added file 'hooks/charmhelpers/contrib/network/__init__.py' |
160 | === added file 'hooks/charmhelpers/contrib/network/ip.py' |
161 | --- hooks/charmhelpers/contrib/network/ip.py 1970-01-01 00:00:00 +0000 |
162 | +++ hooks/charmhelpers/contrib/network/ip.py 2014-07-24 15:25:17 +0000 |
163 | @@ -0,0 +1,156 @@ |
164 | +import sys |
165 | + |
166 | +from functools import partial |
167 | + |
168 | +from charmhelpers.fetch import apt_install |
169 | +from charmhelpers.core.hookenv import ( |
170 | + ERROR, log, |
171 | +) |
172 | + |
173 | +try: |
174 | + import netifaces |
175 | +except ImportError: |
176 | + apt_install('python-netifaces') |
177 | + import netifaces |
178 | + |
179 | +try: |
180 | + import netaddr |
181 | +except ImportError: |
182 | + apt_install('python-netaddr') |
183 | + import netaddr |
184 | + |
185 | + |
186 | +def _validate_cidr(network): |
187 | + try: |
188 | + netaddr.IPNetwork(network) |
189 | + except (netaddr.core.AddrFormatError, ValueError): |
190 | + raise ValueError("Network (%s) is not in CIDR presentation format" % |
191 | + network) |
192 | + |
193 | + |
194 | +def get_address_in_network(network, fallback=None, fatal=False): |
195 | + """ |
196 | + Get an IPv4 or IPv6 address within the network from the host. |
197 | + |
198 | + :param network (str): CIDR presentation format. For example, |
199 | + '192.168.1.0/24'. |
200 | + :param fallback (str): If no address is found, return fallback. |
201 | + :param fatal (boolean): If no address is found, fallback is not |
202 | + set and fatal is True then exit(1). |
203 | + |
204 | + """ |
205 | + |
206 | + def not_found_error_out(): |
207 | + log("No IP address found in network: %s" % network, |
208 | + level=ERROR) |
209 | + sys.exit(1) |
210 | + |
211 | + if network is None: |
212 | + if fallback is not None: |
213 | + return fallback |
214 | + else: |
215 | + if fatal: |
216 | + not_found_error_out() |
217 | + |
218 | + _validate_cidr(network) |
219 | + network = netaddr.IPNetwork(network) |
220 | + for iface in netifaces.interfaces(): |
221 | + addresses = netifaces.ifaddresses(iface) |
222 | + if network.version == 4 and netifaces.AF_INET in addresses: |
223 | + addr = addresses[netifaces.AF_INET][0]['addr'] |
224 | + netmask = addresses[netifaces.AF_INET][0]['netmask'] |
225 | + cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask)) |
226 | + if cidr in network: |
227 | + return str(cidr.ip) |
228 | + if network.version == 6 and netifaces.AF_INET6 in addresses: |
229 | + for addr in addresses[netifaces.AF_INET6]: |
230 | + if not addr['addr'].startswith('fe80'): |
231 | + cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'], |
232 | + addr['netmask'])) |
233 | + if cidr in network: |
234 | + return str(cidr.ip) |
235 | + |
236 | + if fallback is not None: |
237 | + return fallback |
238 | + |
239 | + if fatal: |
240 | + not_found_error_out() |
241 | + |
242 | + return None |
243 | + |
244 | + |
245 | +def is_ipv6(address): |
246 | + '''Determine whether provided address is IPv6 or not''' |
247 | + try: |
248 | + address = netaddr.IPAddress(address) |
249 | + except netaddr.AddrFormatError: |
250 | + # probably a hostname - so not an address at all! |
251 | + return False |
252 | + else: |
253 | + return address.version == 6 |
254 | + |
255 | + |
256 | +def is_address_in_network(network, address): |
257 | + """ |
258 | + Determine whether the provided address is within a network range. |
259 | + |
260 | + :param network (str): CIDR presentation format. For example, |
261 | + '192.168.1.0/24'. |
262 | + :param address: An individual IPv4 or IPv6 address without a net |
263 | + mask or subnet prefix. For example, '192.168.1.1'. |
264 | + :returns boolean: Flag indicating whether address is in network. |
265 | + """ |
266 | + try: |
267 | + network = netaddr.IPNetwork(network) |
268 | + except (netaddr.core.AddrFormatError, ValueError): |
269 | + raise ValueError("Network (%s) is not in CIDR presentation format" % |
270 | + network) |
271 | + try: |
272 | + address = netaddr.IPAddress(address) |
273 | + except (netaddr.core.AddrFormatError, ValueError): |
274 | + raise ValueError("Address (%s) is not in correct presentation format" % |
275 | + address) |
276 | + if address in network: |
277 | + return True |
278 | + else: |
279 | + return False |
280 | + |
281 | + |
282 | +def _get_for_address(address, key): |
283 | + """Retrieve an attribute of or the physical interface that |
284 | + the IP address provided could be bound to. |
285 | + |
286 | + :param address (str): An individual IPv4 or IPv6 address without a net |
287 | + mask or subnet prefix. For example, '192.168.1.1'. |
288 | + :param key: 'iface' for the physical interface name or an attribute |
289 | + of the configured interface, for example 'netmask'. |
290 | + :returns str: Requested attribute or None if address is not bindable. |
291 | + """ |
292 | + address = netaddr.IPAddress(address) |
293 | + for iface in netifaces.interfaces(): |
294 | + addresses = netifaces.ifaddresses(iface) |
295 | + if address.version == 4 and netifaces.AF_INET in addresses: |
296 | + addr = addresses[netifaces.AF_INET][0]['addr'] |
297 | + netmask = addresses[netifaces.AF_INET][0]['netmask'] |
298 | + cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask)) |
299 | + if address in cidr: |
300 | + if key == 'iface': |
301 | + return iface |
302 | + else: |
303 | + return addresses[netifaces.AF_INET][0][key] |
304 | + if address.version == 6 and netifaces.AF_INET6 in addresses: |
305 | + for addr in addresses[netifaces.AF_INET6]: |
306 | + if not addr['addr'].startswith('fe80'): |
307 | + cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'], |
308 | + addr['netmask'])) |
309 | + if address in cidr: |
310 | + if key == 'iface': |
311 | + return iface |
312 | + else: |
313 | + return addr[key] |
314 | + return None |
315 | + |
316 | + |
317 | +get_iface_for_address = partial(_get_for_address, key='iface') |
318 | + |
319 | +get_netmask_for_address = partial(_get_for_address, key='netmask') |
320 | |
321 | === modified file 'hooks/charmhelpers/core/hookenv.py' |
322 | --- hooks/charmhelpers/core/hookenv.py 2014-06-23 09:47:35 +0000 |
323 | +++ hooks/charmhelpers/core/hookenv.py 2014-07-24 15:25:17 +0000 |
324 | @@ -25,7 +25,7 @@ |
325 | def cached(func): |
326 | """Cache return values for multiple executions of func + args |
327 | |
328 | - For example: |
329 | + For example:: |
330 | |
331 | @cached |
332 | def unit_get(attribute): |
333 | @@ -445,18 +445,19 @@ |
334 | class Hooks(object): |
335 | """A convenient handler for hook functions. |
336 | |
337 | - Example: |
338 | + Example:: |
339 | + |
340 | hooks = Hooks() |
341 | |
342 | # register a hook, taking its name from the function name |
343 | @hooks.hook() |
344 | def install(): |
345 | - ... |
346 | + pass # your code here |
347 | |
348 | # register a hook, providing a custom hook name |
349 | @hooks.hook("config-changed") |
350 | def config_changed(): |
351 | - ... |
352 | + pass # your code here |
353 | |
354 | if __name__ == "__main__": |
355 | # execute a hook based on the name the program is called by |
356 | |
357 | === modified file 'hooks/charmhelpers/core/host.py' |
358 | --- hooks/charmhelpers/core/host.py 2014-06-23 09:47:35 +0000 |
359 | +++ hooks/charmhelpers/core/host.py 2014-07-24 15:25:17 +0000 |
360 | @@ -211,13 +211,13 @@ |
361 | def restart_on_change(restart_map, stopstart=False): |
362 | """Restart services based on configuration files changing |
363 | |
364 | - This function is used a decorator, for example |
365 | + This function is used a decorator, for example:: |
366 | |
367 | @restart_on_change({ |
368 | '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] |
369 | }) |
370 | def ceph_client_changed(): |
371 | - ... |
372 | + pass # your code here |
373 | |
374 | In this example, the cinder-api and cinder-volume services |
375 | would be restarted if /etc/ceph/ceph.conf is changed by the |
376 | @@ -313,13 +313,19 @@ |
377 | |
378 | def cmp_pkgrevno(package, revno, pkgcache=None): |
379 | '''Compare supplied revno with the revno of the installed package |
380 | - 1 => Installed revno is greater than supplied arg |
381 | - 0 => Installed revno is the same as supplied arg |
382 | - -1 => Installed revno is less than supplied arg |
383 | + |
384 | + * 1 => Installed revno is greater than supplied arg |
385 | + * 0 => Installed revno is the same as supplied arg |
386 | + * -1 => Installed revno is less than supplied arg |
387 | + |
388 | ''' |
389 | import apt_pkg |
390 | if not pkgcache: |
391 | apt_pkg.init() |
392 | + # Force Apt to build its cache in memory. That way we avoid race |
393 | + # conditions with other applications building the cache in the same |
394 | + # place. |
395 | + apt_pkg.config.set("Dir::Cache::pkgcache", "") |
396 | pkgcache = apt_pkg.Cache() |
397 | pkg = pkgcache[package] |
398 | return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) |
399 | |
400 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
401 | --- hooks/charmhelpers/fetch/__init__.py 2014-06-23 09:47:35 +0000 |
402 | +++ hooks/charmhelpers/fetch/__init__.py 2014-07-24 15:25:17 +0000 |
403 | @@ -235,31 +235,39 @@ |
404 | sources_var='install_sources', |
405 | keys_var='install_keys'): |
406 | """ |
407 | - Configure multiple sources from charm configuration |
408 | + Configure multiple sources from charm configuration. |
409 | + |
410 | + The lists are encoded as yaml fragments in the configuration. |
411 | + The frament needs to be included as a string. |
412 | |
413 | Example config: |
414 | - install_sources: |
415 | + install_sources: | |
416 | - "ppa:foo" |
417 | - "http://example.com/repo precise main" |
418 | - install_keys: |
419 | + install_keys: | |
420 | - null |
421 | - "a1b2c3d4" |
422 | |
423 | Note that 'null' (a.k.a. None) should not be quoted. |
424 | """ |
425 | - sources = safe_load(config(sources_var)) |
426 | - keys = config(keys_var) |
427 | - if keys is not None: |
428 | - keys = safe_load(keys) |
429 | - if isinstance(sources, basestring) and ( |
430 | - keys is None or isinstance(keys, basestring)): |
431 | - add_source(sources, keys) |
432 | + sources = safe_load((config(sources_var) or '').strip()) or [] |
433 | + keys = safe_load((config(keys_var) or '').strip()) or None |
434 | + |
435 | + if isinstance(sources, basestring): |
436 | + sources = [sources] |
437 | + |
438 | + if keys is None: |
439 | + for source in sources: |
440 | + add_source(source, None) |
441 | else: |
442 | - if not len(sources) == len(keys): |
443 | - msg = 'Install sources and keys lists are different lengths' |
444 | - raise SourceConfigError(msg) |
445 | - for src_num in range(len(sources)): |
446 | - add_source(sources[src_num], keys[src_num]) |
447 | + if isinstance(keys, basestring): |
448 | + keys = [keys] |
449 | + |
450 | + if len(sources) != len(keys): |
451 | + raise SourceConfigError( |
452 | + 'Install sources and keys lists are different lengths') |
453 | + for source, key in zip(sources, keys): |
454 | + add_source(source, key) |
455 | if update: |
456 | apt_update(fatal=True) |
457 | |
458 | |
459 | === modified file 'hooks/percona_hooks.py' |
460 | --- hooks/percona_hooks.py 2014-06-23 09:46:08 +0000 |
461 | +++ hooks/percona_hooks.py 2014-07-24 15:25:17 +0000 |
462 | @@ -54,6 +54,10 @@ |
463 | ) |
464 | from mysql import configure_db |
465 | from charmhelpers.payload.execd import execd_preinstall |
466 | +from charmhelpers.contrib.network.ip import ( |
467 | + is_address_in_network, |
468 | + get_address_in_network, |
469 | +) |
470 | |
471 | hooks = Hooks() |
472 | |
473 | @@ -106,6 +110,10 @@ |
474 | elif not clustered: |
475 | # Restart with new configuration |
476 | service_restart('mysql') |
477 | + # Notify any changes to the access network |
478 | + for r_id in relation_ids('shared-db'): |
479 | + for unit in related_units(r_id): |
480 | + shared_db_changed(r_id, unit) |
481 | |
482 | |
483 | @hooks.hook('cluster-relation-changed') |
484 | @@ -142,11 +150,15 @@ |
485 | database_name, |
486 | username, |
487 | admin=admin) |
488 | + |
489 | relation_set(relation_id=relation_id, |
490 | - database=database_name, |
491 | - user=username, |
492 | - password=password, |
493 | - host=db_host) |
494 | + relation_settings={ |
495 | + 'user': username, |
496 | + 'password': password, |
497 | + 'host': db_host, |
498 | + 'database': database_name, |
499 | + } |
500 | + ) |
501 | |
502 | |
503 | # TODO: This could be a hook common between mysql and percona-cluster |
504 | @@ -164,6 +176,9 @@ |
505 | db_host = config('vip') |
506 | else: |
507 | db_host = unit_get('private-address') |
508 | + |
509 | + access_network = config('access-network') |
510 | + |
511 | singleset = set([ |
512 | 'database', |
513 | 'username', |
514 | @@ -175,6 +190,10 @@ |
515 | password = configure_db(settings['hostname'], |
516 | settings['database'], |
517 | settings['username']) |
518 | + if (access_network is not None and |
519 | + is_address_in_network(access_network, |
520 | + get_host_ip(settings['hostname']))): |
521 | + db_host = get_address_in_network(access_network) |
522 | relation_set(relation_id=relation_id, |
523 | db_host=db_host, |
524 | password=password) |
525 | @@ -211,11 +230,18 @@ |
526 | configure_db(databases[db]['hostname'], |
527 | databases[db]['database'], |
528 | databases[db]['username']) |
529 | + if (access_network is not None and |
530 | + is_address_in_network( |
531 | + access_network, |
532 | + get_host_ip(databases[db]['hostname']))): |
533 | + db_host = get_address_in_network(access_network) |
534 | if len(return_data) > 0: |
535 | relation_set(relation_id=relation_id, |
536 | **return_data) |
537 | relation_set(relation_id=relation_id, |
538 | db_host=db_host) |
539 | + relation_set(relation_id=relation_id, |
540 | + relation_settings={'access-network': access_network}) |
541 | |
542 | |
543 | @hooks.hook('ha-relation-joined') |
544 | |
545 | === modified file 'hooks/percona_utils.py' |
546 | --- hooks/percona_utils.py 2014-03-07 11:04:44 +0000 |
547 | +++ hooks/percona_utils.py 2014-07-24 15:25:17 +0000 |
548 | @@ -93,7 +93,7 @@ |
549 | for relid in relation_ids('cluster'): |
550 | for unit in related_units(relid): |
551 | hosts.append(get_host_ip(relation_get('private-address', |
552 | - unit, relid))) |
553 | + unit, relid))) |
554 | return hosts |
555 | |
556 | SQL_SST_USER_SETUP = "GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.*" \ |
See inline comment