Merge lp:~james-page/charms/trusty/percona-cluster/network-splits into lp:~openstack-charmers-archive/charms/trusty/percona-cluster/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
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
James Page Needs Resubmitting
Review via email: mp+228151@code.launchpad.net

Description of the change

Add support for separate 'access-network' configuration.

To post a comment you must log in.
Revision history for this message
Liam Young (gnuoy) wrote :

See inline comment

review: Needs Fixing
Revision history for this message
James Page (james-page) :
review: Needs Resubmitting
Revision history for this message
Liam Young (gnuoy) wrote :

LGTM

review: Approve

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 *.*" \

Subscribers

People subscribed via source and target branches

to status/vote changes: