Merge lp:~james-page/charms/precise/hacluster/redux into lp:charms/hacluster

Proposed by James Page
Status: Merged
Merged at revision: 16
Proposed branch: lp:~james-page/charms/precise/hacluster/redux
Merge into: lp:charms/hacluster
Diff against target: 1217 lines (+647/-387)
10 files modified
.project (+17/-0)
.pydevproject (+8/-0)
config.yaml (+9/-4)
copyright (+5/-0)
hooks/hacluster.py (+8/-235)
hooks/hooks.py (+136/-146)
hooks/lib/cluster_utils.py (+130/-0)
hooks/lib/utils.py (+332/-0)
hooks/maas.py (+1/-1)
hooks/pcmk.py (+1/-1)
To merge this branch: bzr merge lp:~james-page/charms/precise/hacluster/redux
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+155237@code.launchpad.net

Description of the change

Redux ready for final submission (hopefully)

1) Refactored to use HA openstack-charm-helpers reducing charm specific code

2) 'ready' barrier on peer relation added

Each service-unit set a 'ready' flag on the peer relationship when its received enough information to configure corosync and pacemaker; service units are only counted as valid once they have set this.

This ensures that nodes that don't have enough config info don't get counted when assessing clusterable size

3) oldest peer configures services

This is to avoid a split brain when initially forming the cluster; the oldest peer will most likely be busy dealing with other service relations, whilst the other service units start getting ready to be clustered during early service archive

By ensuring that the oldest peer configures the actual services, we smooth the transition from peering->clustered and avoid one of the newer service units trying to take control before the cluster is fully formed.

4) configurable initial cluster size

Default to 2 units; but should be set to the target size for the initial cluster.

5) default to corosync/pacemake v1 integration

This is as recommended upstream and splits the control of the corosync and pacemaker daemons.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

The branch looks good to me. However, I believe we need to make sure the pacemaker initscript is being set to defaults and that starts after corosync since it is disabled in packaging.

Revision history for this message
Andres Rodriguez (andreserl) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.project'
2--- .project 1970-01-01 00:00:00 +0000
3+++ .project 2013-03-25 14:21:21 +0000
4@@ -0,0 +1,17 @@
5+<?xml version="1.0" encoding="UTF-8"?>
6+<projectDescription>
7+ <name>hacluster</name>
8+ <comment></comment>
9+ <projects>
10+ </projects>
11+ <buildSpec>
12+ <buildCommand>
13+ <name>org.python.pydev.PyDevBuilder</name>
14+ <arguments>
15+ </arguments>
16+ </buildCommand>
17+ </buildSpec>
18+ <natures>
19+ <nature>org.python.pydev.pythonNature</nature>
20+ </natures>
21+</projectDescription>
22
23=== added file '.pydevproject'
24--- .pydevproject 1970-01-01 00:00:00 +0000
25+++ .pydevproject 2013-03-25 14:21:21 +0000
26@@ -0,0 +1,8 @@
27+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
28+<?eclipse-pydev version="1.0"?><pydev_project>
29+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
30+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
31+<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
32+<path>/hacluster/hooks</path>
33+</pydev_pathproperty>
34+</pydev_project>
35
36=== modified file 'config.yaml'
37--- config.yaml 2013-01-24 23:39:45 +0000
38+++ config.yaml 2013-03-25 14:21:21 +0000
39@@ -7,22 +7,23 @@
40 If multiple clusters are on the same bindnetaddr network, this value
41 can be changed.
42 corosync_pcmk_ver:
43- default: 0
44+ default: 1
45 type: int
46 description: |
47 Service version for the Pacemaker service version. This will tell
48 Corosync how to start pacemaker
49 corosync_key:
50 type: string
51- default: corosync-key
52+ default: "64RxJNcCkwo8EJYBsaacitUvbQp5AW4YolJi5/2urYZYp2jfLxY+3IUCOaAUJHPle4Yqfy+WBXO0I/6ASSAjj9jaiHVNaxmVhhjcmyBqy2vtPf+m+0VxVjUXlkTyYsODwobeDdO3SIkbIABGfjLTu29yqPTsfbvSYr6skRb9ne0="
53 description: |
54 This value will become the Corosync authentication key. To generate
55 a suitable value use:
56 .
57- corosync-keygen
58+ sudo corosync-keygen
59+ sudo cat /etc/corosync/authkey | base64 -w 0
60 .
61 This configuration element is mandatory and the service will fail on
62- install if it is not provided.
63+ install if it is not provided. The value must be base64 encoded.
64 stonith_enabled:
65 type: string
66 default: 'False'
67@@ -36,3 +37,7 @@
68 maas_credentials:
69 type: string
70 description: MAAS credentials (required for STONITH).
71+ cluster_count:
72+ type: int
73+ default: 2
74+ description: Number of peer units required to bootstrap cluster services.
75
76=== modified file 'copyright'
77--- copyright 2012-11-20 20:06:11 +0000
78+++ copyright 2013-03-25 14:21:21 +0000
79@@ -15,3 +15,8 @@
80 .
81 You should have received a copy of the GNU General Public License
82 along with this program. If not, see <http://www.gnu.org/licenses/>.
83+
84+ Files: ocf/ceph/*
85+ Copyright: 2012 Florian Haas, hastexo
86+ License: LGPL-2.1
87+ On Debian based systems, see /usr/share/common-licenses/LGPL-2.1.
88\ No newline at end of file
89
90=== removed symlink 'hooks/ha-relation-departed'
91=== target was u'hooks.py'
92=== renamed file 'hooks/utils.py' => 'hooks/hacluster.py'
93--- hooks/utils.py 2013-02-20 10:10:42 +0000
94+++ hooks/hacluster.py 2013-03-25 14:21:21 +0000
95@@ -10,243 +10,16 @@
96 import os
97 import subprocess
98 import socket
99-import sys
100 import fcntl
101 import struct
102-
103-
104-def do_hooks(hooks):
105- hook = os.path.basename(sys.argv[0])
106-
107- try:
108- hooks[hook]()
109- except KeyError:
110- juju_log('INFO',
111- "This charm doesn't know how to handle '{}'.".format(hook))
112-
113-
114-def install(*pkgs):
115- cmd = [
116- 'apt-get',
117- '-y',
118- 'install'
119- ]
120- for pkg in pkgs:
121- cmd.append(pkg)
122- subprocess.check_call(cmd)
123-
124-TEMPLATES_DIR = 'templates'
125-
126-try:
127- import jinja2
128-except ImportError:
129- install('python-jinja2')
130- import jinja2
131-
132-try:
133- from netaddr import IPNetwork
134-except ImportError:
135- install('python-netaddr')
136- from netaddr import IPNetwork
137-
138-try:
139- import dns.resolver
140-except ImportError:
141- install('python-dnspython')
142- import dns.resolver
143-
144-
145-def render_template(template_name, context, template_dir=TEMPLATES_DIR):
146- templates = jinja2.Environment(
147- loader=jinja2.FileSystemLoader(template_dir)
148- )
149- template = templates.get_template(template_name)
150- return template.render(context)
151-
152-
153-CLOUD_ARCHIVE = \
154-""" # Ubuntu Cloud Archive
155-deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
156-"""
157-
158-CLOUD_ARCHIVE_POCKETS = {
159- 'precise-folsom': 'precise-updates/folsom',
160- 'precise-folsom/updates': 'precise-updates/folsom',
161- 'precise-folsom/proposed': 'precise-proposed/folsom',
162- 'precise-grizzly': 'precise-updates/grizzly',
163- 'precise-grizzly/updates': 'precise-updates/grizzly',
164- 'precise-grizzly/proposed': 'precise-proposed/grizzly'
165- }
166-
167-
168-def configure_source():
169- source = str(config_get('openstack-origin'))
170- if not source:
171- return
172- if source.startswith('ppa:'):
173- cmd = [
174- 'add-apt-repository',
175- source
176- ]
177- subprocess.check_call(cmd)
178- if source.startswith('cloud:'):
179- install('ubuntu-cloud-keyring')
180- pocket = source.split(':')[1]
181- with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
182- apt.write(CLOUD_ARCHIVE.format(CLOUD_ARCHIVE_POCKETS[pocket]))
183- if source.startswith('deb'):
184- l = len(source.split('|'))
185- if l == 2:
186- (apt_line, key) = source.split('|')
187- cmd = [
188- 'apt-key',
189- 'adv', '--keyserver keyserver.ubuntu.com',
190- '--recv-keys', key
191- ]
192- subprocess.check_call(cmd)
193- elif l == 1:
194- apt_line = source
195-
196- with open('/etc/apt/sources.list.d/quantum.list', 'w') as apt:
197- apt.write(apt_line + "\n")
198- cmd = [
199- 'apt-get',
200- 'update'
201- ]
202- subprocess.check_call(cmd)
203-
204-# Protocols
205-TCP = 'TCP'
206-UDP = 'UDP'
207-
208-
209-def expose(port, protocol='TCP'):
210- cmd = [
211- 'open-port',
212- '{}/{}'.format(port, protocol)
213- ]
214- subprocess.check_call(cmd)
215-
216-
217-def juju_log(severity, message):
218- cmd = [
219- 'juju-log',
220- '--log-level', severity,
221- message
222- ]
223- subprocess.check_call(cmd)
224-
225-
226-def relation_ids(relation):
227- cmd = [
228- 'relation-ids',
229- relation
230- ]
231- return subprocess.check_output(cmd).split() # IGNORE:E1103
232-
233-
234-def relation_list(rid):
235- cmd = [
236- 'relation-list',
237- '-r', rid,
238- ]
239- return subprocess.check_output(cmd).split() # IGNORE:E1103
240-
241-
242-def relation_get(attribute, unit=None, rid=None):
243- cmd = [
244- 'relation-get',
245- ]
246- if rid:
247- cmd.append('-r')
248- cmd.append(rid)
249- cmd.append(attribute)
250- if unit:
251- cmd.append(unit)
252- value = subprocess.check_output(cmd).strip() # IGNORE:E1103
253- if value == "":
254- return None
255- else:
256- return value
257-
258-
259-def relation_set(**kwargs):
260- cmd = [
261- 'relation-set'
262- ]
263- args = []
264- for k, v in kwargs.items():
265- if k == 'rid':
266- cmd.append('-r')
267- cmd.append(v)
268- else:
269- args.append('{}={}'.format(k, v))
270- cmd += args
271- subprocess.check_call(cmd)
272-
273-
274-def unit_get(attribute):
275- cmd = [
276- 'unit-get',
277- attribute
278- ]
279- return subprocess.check_output(cmd).strip() # IGNORE:E1103
280-
281-
282-def config_get(attribute):
283- cmd = [
284- 'config-get',
285- attribute
286- ]
287- return subprocess.check_output(cmd).strip() # IGNORE:E1103
288-
289-
290-def get_unit_hostname():
291- return socket.gethostname()
292-
293-
294-def get_host_ip(hostname=unit_get('private-address')):
295- try:
296- # Test to see if already an IPv4 address
297- socket.inet_aton(hostname)
298- return hostname
299- except socket.error:
300- pass
301- try:
302- answers = dns.resolver.query(hostname, 'A')
303- if answers:
304- return answers[0].address
305- except dns.resolver.NXDOMAIN:
306- pass
307- return None
308-
309-
310-def restart(*services):
311- for service in services:
312- subprocess.check_call(['service', service, 'restart'])
313-
314-
315-def stop(*services):
316- for service in services:
317- subprocess.check_call(['service', service, 'stop'])
318-
319-
320-def start(*services):
321- for service in services:
322- subprocess.check_call(['service', service, 'start'])
323-
324-
325-def running(service):
326- try:
327- output = subprocess.check_output(['service', service, 'status'])
328- except subprocess.CalledProcessError:
329- return False
330- else:
331- if ("start/running" in output or
332- "is running" in output):
333- return True
334- else:
335- return False
336+import lib.utils as utils
337+
338+
339+try:
340+ from netaddr import IPNetwork
341+except ImportError:
342+ utils.install('python-netaddr')
343+ from netaddr import IPNetwork
344
345
346 def disable_upstart_services(*services):
347
348=== added symlink 'hooks/hanode-relation-changed'
349=== target is u'hooks.py'
350=== modified file 'hooks/hooks.py'
351--- hooks/hooks.py 2013-03-07 09:37:28 +0000
352+++ hooks/hooks.py 2013-03-25 14:21:21 +0000
353@@ -11,10 +11,13 @@
354 import sys
355 import time
356 import os
357+from base64 import b64decode
358
359 import maas as MAAS
360-import utils
361+import lib.utils as utils
362+import lib.cluster_utils as cluster
363 import pcmk
364+import hacluster
365
366
367 def install():
368@@ -36,12 +39,12 @@
369 for unit in utils.relation_list(relid):
370 conf = {
371 'corosync_bindnetaddr':
372- utils.get_network_address(
373+ hacluster.get_network_address(
374 utils.relation_get('corosync_bindiface',
375 unit, relid)
376 ),
377 'corosync_mcastport': utils.relation_get('corosync_mcastport',
378- unit, relid),
379+ unit, relid),
380 'corosync_mcastaddr': utils.config_get('corosync_mcastaddr'),
381 'corosync_pcmk_ver': utils.config_get('corosync_pcmk_ver'),
382 }
383@@ -68,27 +71,27 @@
384 with open('/etc/default/corosync', 'w') as corosync_default:
385 corosync_default.write(utils.render_template('corosync',
386 corosync_default_context))
387-
388- # write the authkey
389 corosync_key = utils.config_get('corosync_key')
390- with open('/etc/corosync/authkey', 'w') as corosync_key_file:
391- corosync_key_file.write(corosync_key)
392- os.chmod = ('/etc/corosync/authkey', 0400)
393+ if corosync_key:
394+ # write the authkey
395+ with open('/etc/corosync/authkey', 'w') as corosync_key_file:
396+ corosync_key_file.write(b64decode(corosync_key))
397+ os.chmod = ('/etc/corosync/authkey', 0400)
398
399
400 def config_changed():
401 utils.juju_log('INFO', 'Begin config-changed hook.')
402
403 corosync_key = utils.config_get('corosync_key')
404- if corosync_key == '':
405+ if not corosync_key:
406 utils.juju_log('CRITICAL',
407 'No Corosync key supplied, cannot proceed')
408 sys.exit(1)
409
410 if int(utils.config_get('corosync_pcmk_ver')) == 1:
411- utils.enable_lsb_services('pacemaker')
412+ hacluster.enable_lsb_services('pacemaker')
413 else:
414- utils.disable_lsb_services('pacemaker')
415+ hacluster.disable_lsb_services('pacemaker')
416
417 # Create a new config file
418 emit_base_conf()
419@@ -109,14 +112,6 @@
420 utils.juju_log('INFO', 'End upgrade-charm hook.')
421
422
423-def start():
424- pass
425-
426-
427-def stop():
428- pass
429-
430-
431 def restart_corosync():
432 if int(utils.config_get('corosync_pcmk_ver')) == 1:
433 if utils.running("pacemaker"):
434@@ -136,17 +131,23 @@
435 utils.juju_log('INFO',
436 'HA already configured, not reconfiguring')
437 return
438- # Check that there's enough nodes in order to perform the
439- # configuration of the HA cluster
440- if len(get_cluster_nodes()) < 2:
441- utils.juju_log('WARNING', 'Not enough nodes in cluster, bailing')
442- return
443 # Check that we are related to a principle and that
444 # it has already provided the required corosync configuration
445 if not get_corosync_conf():
446 utils.juju_log('WARNING',
447 'Unable to configure corosync right now, bailing')
448 return
449+ else:
450+ utils.juju_log('INFO',
451+ 'Ready to form cluster - informing peers')
452+ utils.relation_set(ready=True,
453+ rid=utils.relation_ids('hanode')[0])
454+ # Check that there's enough nodes in order to perform the
455+ # configuration of the HA cluster
456+ if (len(get_cluster_nodes()) <
457+ int(utils.config_get('cluster_count'))):
458+ utils.juju_log('WARNING', 'Not enough nodes in cluster, bailing')
459+ return
460
461 relids = utils.relation_ids('ha')
462 if len(relids) == 1: # Should only ever be one of these
463@@ -225,98 +226,101 @@
464 ' resource-stickiness="100"'
465 pcmk.commit(cmd)
466
467- utils.juju_log('INFO', 'Configuring Resources')
468- utils.juju_log('INFO', str(resources))
469-
470- for res_name, res_type in resources.iteritems():
471- # disable the service we are going to put in HA
472- if res_type.split(':')[0] == "lsb":
473- utils.disable_lsb_services(res_type.split(':')[1])
474- if utils.running(res_type.split(':')[1]):
475- utils.stop(res_type.split(':')[1])
476- elif (len(init_services) != 0 and
477- res_name in init_services and
478- init_services[res_name]):
479- utils.disable_upstart_services(init_services[res_name])
480- if utils.running(init_services[res_name]):
481- utils.stop(init_services[res_name])
482- # Put the services in HA, if not already done so
483- #if not pcmk.is_resource_present(res_name):
484- if not pcmk.crm_opt_exists(res_name):
485- if not res_name in resource_params:
486- cmd = 'crm -F configure primitive %s %s' % (res_name, res_type)
487- else:
488- cmd = 'crm -F configure primitive %s %s %s' % \
489- (res_name,
490- res_type,
491- resource_params[res_name])
492- pcmk.commit(cmd)
493- utils.juju_log('INFO', '%s' % cmd)
494-
495- utils.juju_log('INFO', 'Configuring Groups')
496- utils.juju_log('INFO', str(groups))
497- for grp_name, grp_params in groups.iteritems():
498- if not pcmk.crm_opt_exists(grp_name):
499- cmd = 'crm -F configure group %s %s' % (grp_name, grp_params)
500- pcmk.commit(cmd)
501- utils.juju_log('INFO', '%s' % cmd)
502-
503- utils.juju_log('INFO', 'Configuring Master/Slave (ms)')
504- utils.juju_log('INFO', str(ms))
505- for ms_name, ms_params in ms.iteritems():
506- if not pcmk.crm_opt_exists(ms_name):
507- cmd = 'crm -F configure ms %s %s' % (ms_name, ms_params)
508- pcmk.commit(cmd)
509- utils.juju_log('INFO', '%s' % cmd)
510-
511- utils.juju_log('INFO', 'Configuring Orders')
512- utils.juju_log('INFO', str(orders))
513- for ord_name, ord_params in orders.iteritems():
514- if not pcmk.crm_opt_exists(ord_name):
515- cmd = 'crm -F configure order %s %s' % (ord_name, ord_params)
516- pcmk.commit(cmd)
517- utils.juju_log('INFO', '%s' % cmd)
518-
519- utils.juju_log('INFO', 'Configuring Colocations')
520- utils.juju_log('INFO', str(colocations))
521- for col_name, col_params in colocations.iteritems():
522- if not pcmk.crm_opt_exists(col_name):
523- cmd = 'crm -F configure colocation %s %s' % (col_name, col_params)
524- pcmk.commit(cmd)
525- utils.juju_log('INFO', '%s' % cmd)
526-
527- utils.juju_log('INFO', 'Configuring Clones')
528- utils.juju_log('INFO', str(clones))
529- for cln_name, cln_params in clones.iteritems():
530- if not pcmk.crm_opt_exists(cln_name):
531- cmd = 'crm -F configure clone %s %s' % (cln_name, cln_params)
532- pcmk.commit(cmd)
533- utils.juju_log('INFO', '%s' % cmd)
534-
535- for res_name, res_type in resources.iteritems():
536- if len(init_services) != 0 and res_name in init_services:
537- # Checks that the resources are running and started.
538- # Ensure that clones are excluded as the resource is
539- # not directly controllable (dealt with below)
540- # Ensure that groups are cleaned up as a whole rather
541- # than as individual resources.
542- if (res_name not in clones.values() and
543- res_name not in groups.values() and
544- not pcmk.crm_res_running(res_name)):
545- # Just in case, cleanup the resources to ensure they get
546- # started in case they failed for some unrelated reason.
547- cmd = 'crm resource cleanup %s' % res_name
548- pcmk.commit(cmd)
549-
550- for cl_name in clones:
551- # Always cleanup clones
552- cmd = 'crm resource cleanup %s' % cl_name
553- pcmk.commit(cmd)
554-
555- for grp_name in groups:
556- # Always cleanup groups
557- cmd = 'crm resource cleanup %s' % grp_name
558- pcmk.commit(cmd)
559+ # Only configure the cluster resources
560+ # from the oldest peer unit.
561+ if cluster.oldest_peer(cluster.peer_units()):
562+ utils.juju_log('INFO', 'Configuring Resources')
563+ utils.juju_log('INFO', str(resources))
564+ for res_name, res_type in resources.iteritems():
565+ # disable the service we are going to put in HA
566+ if res_type.split(':')[0] == "lsb":
567+ hacluster.disable_lsb_services(res_type.split(':')[1])
568+ if utils.running(res_type.split(':')[1]):
569+ utils.stop(res_type.split(':')[1])
570+ elif (len(init_services) != 0 and
571+ res_name in init_services and
572+ init_services[res_name]):
573+ hacluster.disable_upstart_services(init_services[res_name])
574+ if utils.running(init_services[res_name]):
575+ utils.stop(init_services[res_name])
576+ # Put the services in HA, if not already done so
577+ #if not pcmk.is_resource_present(res_name):
578+ if not pcmk.crm_opt_exists(res_name):
579+ if not res_name in resource_params:
580+ cmd = 'crm -F configure primitive %s %s' % (res_name,
581+ res_type)
582+ else:
583+ cmd = 'crm -F configure primitive %s %s %s' % \
584+ (res_name,
585+ res_type,
586+ resource_params[res_name])
587+ pcmk.commit(cmd)
588+ utils.juju_log('INFO', '%s' % cmd)
589+
590+ utils.juju_log('INFO', 'Configuring Groups')
591+ utils.juju_log('INFO', str(groups))
592+ for grp_name, grp_params in groups.iteritems():
593+ if not pcmk.crm_opt_exists(grp_name):
594+ cmd = 'crm -F configure group %s %s' % (grp_name, grp_params)
595+ pcmk.commit(cmd)
596+ utils.juju_log('INFO', '%s' % cmd)
597+
598+ utils.juju_log('INFO', 'Configuring Master/Slave (ms)')
599+ utils.juju_log('INFO', str(ms))
600+ for ms_name, ms_params in ms.iteritems():
601+ if not pcmk.crm_opt_exists(ms_name):
602+ cmd = 'crm -F configure ms %s %s' % (ms_name, ms_params)
603+ pcmk.commit(cmd)
604+ utils.juju_log('INFO', '%s' % cmd)
605+
606+ utils.juju_log('INFO', 'Configuring Orders')
607+ utils.juju_log('INFO', str(orders))
608+ for ord_name, ord_params in orders.iteritems():
609+ if not pcmk.crm_opt_exists(ord_name):
610+ cmd = 'crm -F configure order %s %s' % (ord_name, ord_params)
611+ pcmk.commit(cmd)
612+ utils.juju_log('INFO', '%s' % cmd)
613+
614+ utils.juju_log('INFO', 'Configuring Colocations')
615+ utils.juju_log('INFO', str(colocations))
616+ for col_name, col_params in colocations.iteritems():
617+ if not pcmk.crm_opt_exists(col_name):
618+ cmd = 'crm -F configure colocation %s %s' % (col_name, col_params)
619+ pcmk.commit(cmd)
620+ utils.juju_log('INFO', '%s' % cmd)
621+
622+ utils.juju_log('INFO', 'Configuring Clones')
623+ utils.juju_log('INFO', str(clones))
624+ for cln_name, cln_params in clones.iteritems():
625+ if not pcmk.crm_opt_exists(cln_name):
626+ cmd = 'crm -F configure clone %s %s' % (cln_name, cln_params)
627+ pcmk.commit(cmd)
628+ utils.juju_log('INFO', '%s' % cmd)
629+
630+ for res_name, res_type in resources.iteritems():
631+ if len(init_services) != 0 and res_name in init_services:
632+ # Checks that the resources are running and started.
633+ # Ensure that clones are excluded as the resource is
634+ # not directly controllable (dealt with below)
635+ # Ensure that groups are cleaned up as a whole rather
636+ # than as individual resources.
637+ if (res_name not in clones.values() and
638+ res_name not in groups.values() and
639+ not pcmk.crm_res_running(res_name)):
640+ # Just in case, cleanup the resources to ensure they get
641+ # started in case they failed for some unrelated reason.
642+ cmd = 'crm resource cleanup %s' % res_name
643+ pcmk.commit(cmd)
644+
645+ for cl_name in clones:
646+ # Always cleanup clones
647+ cmd = 'crm resource cleanup %s' % cl_name
648+ pcmk.commit(cmd)
649+
650+ for grp_name in groups:
651+ # Always cleanup groups
652+ cmd = 'crm resource cleanup %s' % grp_name
653+ pcmk.commit(cmd)
654
655 for rel_id in utils.relation_ids('ha'):
656 utils.relation_set(rid=rel_id,
657@@ -382,42 +386,28 @@
658 pcmk.commit(cmd)
659
660
661-def ha_relation_departed():
662- # TODO: Fin out which node is departing and put it in standby mode.
663- # If this happens, and a new relation is created in the same machine
664- # (which already has node), then check whether it is standby and put it
665- # in online mode. This should be done in ha_relation_joined.
666- pcmk.standby(utils.get_unit_hostname())
667-
668-
669 def get_cluster_nodes():
670 hosts = []
671- hosts.append('{}:6789'.format(utils.get_host_ip()))
672-
673+ hosts.append(utils.unit_get('private-address'))
674 for relid in utils.relation_ids('hanode'):
675 for unit in utils.relation_list(relid):
676- hosts.append(
677- '{}:6789'.format(utils.get_host_ip(
678- utils.relation_get('private-address',
679- unit, relid)))
680- )
681-
682+ if utils.relation_get('ready',
683+ rid=relid,
684+ unit=unit):
685+ hosts.append(utils.relation_get('private-address',
686+ unit, relid))
687 hosts.sort()
688 return hosts
689
690
691-utils.do_hooks({
692- 'install': install,
693- 'config-changed': config_changed,
694- 'start': start,
695- 'stop': stop,
696- 'upgrade-charm': upgrade_charm,
697- 'ha-relation-joined': configure_cluster,
698- 'ha-relation-changed': configure_cluster,
699- 'ha-relation-departed': ha_relation_departed,
700- 'hanode-relation-joined': configure_cluster,
701- #'hanode-relation-departed': hanode_relation_departed,
702- # TODO: should probably remove nodes from the cluster
703- })
704+hooks = {
705+ 'install': install,
706+ 'config-changed': config_changed,
707+ 'upgrade-charm': upgrade_charm,
708+ 'ha-relation-joined': configure_cluster,
709+ 'ha-relation-changed': configure_cluster,
710+ 'hanode-relation-joined': configure_cluster,
711+ 'hanode-relation-changed': configure_cluster,
712+ }
713
714-sys.exit(0)
715+utils.do_hooks(hooks)
716
717=== added directory 'hooks/lib'
718=== added file 'hooks/lib/__init__.py'
719=== added file 'hooks/lib/cluster_utils.py'
720--- hooks/lib/cluster_utils.py 1970-01-01 00:00:00 +0000
721+++ hooks/lib/cluster_utils.py 2013-03-25 14:21:21 +0000
722@@ -0,0 +1,130 @@
723+#
724+# Copyright 2012 Canonical Ltd.
725+#
726+# This file is sourced from lp:openstack-charm-helpers
727+#
728+# Authors:
729+# James Page <james.page@ubuntu.com>
730+# Adam Gandelman <adamg@ubuntu.com>
731+#
732+
733+from lib.utils import (
734+ juju_log,
735+ relation_ids,
736+ relation_list,
737+ relation_get,
738+ get_unit_hostname,
739+ config_get
740+ )
741+import subprocess
742+import os
743+
744+
745+def is_clustered():
746+ for r_id in (relation_ids('ha') or []):
747+ for unit in (relation_list(r_id) or []):
748+ clustered = relation_get('clustered',
749+ rid=r_id,
750+ unit=unit)
751+ if clustered:
752+ return True
753+ return False
754+
755+
756+def is_leader(resource):
757+ cmd = [
758+ "crm", "resource",
759+ "show", resource
760+ ]
761+ try:
762+ status = subprocess.check_output(cmd)
763+ except subprocess.CalledProcessError:
764+ return False
765+ else:
766+ if get_unit_hostname() in status:
767+ return True
768+ else:
769+ return False
770+
771+
772+def peer_units():
773+ peers = []
774+ for r_id in (relation_ids('cluster') or []):
775+ for unit in (relation_list(r_id) or []):
776+ peers.append(unit)
777+ return peers
778+
779+
780+def oldest_peer(peers):
781+ local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
782+ for peer in peers:
783+ remote_unit_no = int(peer.split('/')[1])
784+ if remote_unit_no < local_unit_no:
785+ return False
786+ return True
787+
788+
789+def eligible_leader(resource):
790+ if is_clustered():
791+ if not is_leader(resource):
792+ juju_log('INFO', 'Deferring action to CRM leader.')
793+ return False
794+ else:
795+ peers = peer_units()
796+ if peers and not oldest_peer(peers):
797+ juju_log('INFO', 'Deferring action to oldest service unit.')
798+ return False
799+ return True
800+
801+
802+def https():
803+ '''
804+ Determines whether enough data has been provided in configuration
805+ or relation data to configure HTTPS
806+ .
807+ returns: boolean
808+ '''
809+ if config_get('use-https') == "yes":
810+ return True
811+ if config_get('ssl_cert') and config_get('ssl_key'):
812+ return True
813+ for r_id in relation_ids('identity-service'):
814+ for unit in relation_list(r_id):
815+ if (relation_get('https_keystone', rid=r_id, unit=unit) and
816+ relation_get('ssl_cert', rid=r_id, unit=unit) and
817+ relation_get('ssl_key', rid=r_id, unit=unit) and
818+ relation_get('ca_cert', rid=r_id, unit=unit)):
819+ return True
820+ return False
821+
822+
823+def determine_api_port(public_port):
824+ '''
825+ Determine correct API server listening port based on
826+ existence of HTTPS reverse proxy and/or haproxy.
827+
828+ public_port: int: standard public port for given service
829+
830+ returns: int: the correct listening port for the API service
831+ '''
832+ i = 0
833+ if len(peer_units()) > 0 or is_clustered():
834+ i += 1
835+ if https():
836+ i += 1
837+ return public_port - (i * 10)
838+
839+
840+def determine_haproxy_port(public_port):
841+ '''
842+ Description: Determine correct proxy listening port based on public IP +
843+ existence of HTTPS reverse proxy.
844+
845+ public_port: int: standard public port for given service
846+
847+ returns: int: the correct listening port for the HAProxy service
848+ '''
849+ i = 0
850+ if https():
851+ i += 1
852+ return public_port - (i * 10)
853
854=== added file 'hooks/lib/utils.py'
855--- hooks/lib/utils.py 1970-01-01 00:00:00 +0000
856+++ hooks/lib/utils.py 2013-03-25 14:21:21 +0000
857@@ -0,0 +1,332 @@
858+#
859+# Copyright 2012 Canonical Ltd.
860+#
861+# This file is sourced from lp:openstack-charm-helpers
862+#
863+# Authors:
864+# James Page <james.page@ubuntu.com>
865+# Paul Collins <paul.collins@canonical.com>
866+# Adam Gandelman <adamg@ubuntu.com>
867+#
868+
869+import json
870+import os
871+import subprocess
872+import socket
873+import sys
874+
875+
876+def do_hooks(hooks):
877+ hook = os.path.basename(sys.argv[0])
878+
879+ try:
880+ hook_func = hooks[hook]
881+ except KeyError:
882+ juju_log('INFO',
883+ "This charm doesn't know how to handle '{}'.".format(hook))
884+ else:
885+ hook_func()
886+
887+
888+def install(*pkgs):
889+ cmd = [
890+ 'apt-get',
891+ '-y',
892+ 'install'
893+ ]
894+ for pkg in pkgs:
895+ cmd.append(pkg)
896+ subprocess.check_call(cmd)
897+
898+TEMPLATES_DIR = 'templates'
899+
900+try:
901+ import jinja2
902+except ImportError:
903+ install('python-jinja2')
904+ import jinja2
905+
906+try:
907+ import dns.resolver
908+except ImportError:
909+ install('python-dnspython')
910+ import dns.resolver
911+
912+
913+def render_template(template_name, context, template_dir=TEMPLATES_DIR):
914+ templates = jinja2.Environment(
915+ loader=jinja2.FileSystemLoader(template_dir)
916+ )
917+ template = templates.get_template(template_name)
918+ return template.render(context)
919+
920+CLOUD_ARCHIVE = \
921+""" # Ubuntu Cloud Archive
922+deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
923+"""
924+
925+CLOUD_ARCHIVE_POCKETS = {
926+ 'folsom': 'precise-updates/folsom',
927+ 'folsom/updates': 'precise-updates/folsom',
928+ 'folsom/proposed': 'precise-proposed/folsom',
929+ 'grizzly': 'precise-updates/grizzly',
930+ 'grizzly/updates': 'precise-updates/grizzly',
931+ 'grizzly/proposed': 'precise-proposed/grizzly'
932+ }
933+
934+
935+def configure_source():
936+ source = str(config_get('openstack-origin'))
937+ if not source:
938+ return
939+ if source.startswith('ppa:'):
940+ cmd = [
941+ 'add-apt-repository',
942+ source
943+ ]
944+ subprocess.check_call(cmd)
945+ if source.startswith('cloud:'):
946+ # CA values should be formatted as cloud:ubuntu-openstack/pocket, eg:
947+ # cloud:precise-folsom/updates or cloud:precise-folsom/proposed
948+ install('ubuntu-cloud-keyring')
949+ pocket = source.split(':')[1]
950+ pocket = pocket.split('-')[1]
951+ with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
952+ apt.write(CLOUD_ARCHIVE.format(CLOUD_ARCHIVE_POCKETS[pocket]))
953+ if source.startswith('deb'):
954+ l = len(source.split('|'))
955+ if l == 2:
956+ (apt_line, key) = source.split('|')
957+ cmd = [
958+ 'apt-key',
959+ 'adv', '--keyserver keyserver.ubuntu.com',
960+ '--recv-keys', key
961+ ]
962+ subprocess.check_call(cmd)
963+ elif l == 1:
964+ apt_line = source
965+
966+ with open('/etc/apt/sources.list.d/quantum.list', 'w') as apt:
967+ apt.write(apt_line + "\n")
968+ cmd = [
969+ 'apt-get',
970+ 'update'
971+ ]
972+ subprocess.check_call(cmd)
973+
974+# Protocols
975+TCP = 'TCP'
976+UDP = 'UDP'
977+
978+
979+def expose(port, protocol='TCP'):
980+ cmd = [
981+ 'open-port',
982+ '{}/{}'.format(port, protocol)
983+ ]
984+ subprocess.check_call(cmd)
985+
986+
987+def juju_log(severity, message):
988+ cmd = [
989+ 'juju-log',
990+ '--log-level', severity,
991+ message
992+ ]
993+ subprocess.check_call(cmd)
994+
995+
996+cache = {}
997+
998+
999+def cached(func):
1000+ def wrapper(*args, **kwargs):
1001+ global cache
1002+ key = str((func, args, kwargs))
1003+ try:
1004+ return cache[key]
1005+ except KeyError:
1006+ res = func(*args, **kwargs)
1007+ cache[key] = res
1008+ return res
1009+ return wrapper
1010+
1011+
1012+@cached
1013+def relation_ids(relation):
1014+ cmd = [
1015+ 'relation-ids',
1016+ relation
1017+ ]
1018+ result = str(subprocess.check_output(cmd)).split()
1019+ if result == "":
1020+ return None
1021+ else:
1022+ return result
1023+
1024+
1025+@cached
1026+def relation_list(rid):
1027+ cmd = [
1028+ 'relation-list',
1029+ '-r', rid,
1030+ ]
1031+ result = str(subprocess.check_output(cmd)).split()
1032+ if result == "":
1033+ return None
1034+ else:
1035+ return result
1036+
1037+
1038+@cached
1039+def relation_get(attribute, unit=None, rid=None):
1040+ cmd = [
1041+ 'relation-get',
1042+ ]
1043+ if rid:
1044+ cmd.append('-r')
1045+ cmd.append(rid)
1046+ cmd.append(attribute)
1047+ if unit:
1048+ cmd.append(unit)
1049+ value = subprocess.check_output(cmd).strip() # IGNORE:E1103
1050+ if value == "":
1051+ return None
1052+ else:
1053+ return value
1054+
1055+
1056+@cached
1057+def relation_get_dict(relation_id=None, remote_unit=None):
1058+ """Obtain all relation data as dict by way of JSON"""
1059+ cmd = [
1060+ 'relation-get', '--format=json'
1061+ ]
1062+ if relation_id:
1063+ cmd.append('-r')
1064+ cmd.append(relation_id)
1065+ if remote_unit:
1066+ remote_unit_orig = os.getenv('JUJU_REMOTE_UNIT', None)
1067+ os.environ['JUJU_REMOTE_UNIT'] = remote_unit
1068+ j = subprocess.check_output(cmd)
1069+ if remote_unit and remote_unit_orig:
1070+ os.environ['JUJU_REMOTE_UNIT'] = remote_unit_orig
1071+ d = json.loads(j)
1072+ settings = {}
1073+ # convert unicode to strings
1074+ for k, v in d.iteritems():
1075+ settings[str(k)] = str(v)
1076+ return settings
1077+
1078+
1079+def relation_set(**kwargs):
1080+ cmd = [
1081+ 'relation-set'
1082+ ]
1083+ args = []
1084+ for k, v in kwargs.items():
1085+ if k == 'rid':
1086+ if v:
1087+ cmd.append('-r')
1088+ cmd.append(v)
1089+ else:
1090+ args.append('{}={}'.format(k, v))
1091+ cmd += args
1092+ subprocess.check_call(cmd)
1093+
1094+
1095+@cached
1096+def unit_get(attribute):
1097+ cmd = [
1098+ 'unit-get',
1099+ attribute
1100+ ]
1101+ value = subprocess.check_output(cmd).strip() # IGNORE:E1103
1102+ if value == "":
1103+ return None
1104+ else:
1105+ return value
1106+
1107+
1108+@cached
1109+def config_get(attribute):
1110+ cmd = [
1111+ 'config-get',
1112+ '--format',
1113+ 'json',
1114+ ]
1115+ out = subprocess.check_output(cmd).strip() # IGNORE:E1103
1116+ cfg = json.loads(out)
1117+
1118+ try:
1119+ return cfg[attribute]
1120+ except KeyError:
1121+ return None
1122+
1123+
1124+@cached
1125+def get_unit_hostname():
1126+ return socket.gethostname()
1127+
1128+
1129+@cached
1130+def get_host_ip(hostname=unit_get('private-address')):
1131+ try:
1132+ # Test to see if already an IPv4 address
1133+ socket.inet_aton(hostname)
1134+ return hostname
1135+ except socket.error:
1136+ answers = dns.resolver.query(hostname, 'A')
1137+ if answers:
1138+ return answers[0].address
1139+ return None
1140+
1141+
1142+def _svc_control(service, action):
1143+ subprocess.check_call(['service', service, action])
1144+
1145+
1146+def restart(*services):
1147+ for service in services:
1148+ _svc_control(service, 'restart')
1149+
1150+
1151+def stop(*services):
1152+ for service in services:
1153+ _svc_control(service, 'stop')
1154+
1155+
1156+def start(*services):
1157+ for service in services:
1158+ _svc_control(service, 'start')
1159+
1160+
1161+def reload(*services):
1162+ for service in services:
1163+ try:
1164+ _svc_control(service, 'reload')
1165+ except subprocess.CalledProcessError:
1166+ # Reload failed - either service does not support reload
1167+ # or it was not running - restart will fixup most things
1168+ _svc_control(service, 'restart')
1169+
1170+
1171+def running(service):
1172+ try:
1173+ output = subprocess.check_output(['service', service, 'status'])
1174+ except subprocess.CalledProcessError:
1175+ return False
1176+ else:
1177+ if ("start/running" in output or
1178+ "is running" in output):
1179+ return True
1180+ else:
1181+ return False
1182+
1183+
1184+def is_relation_made(relation, key='private-address'):
1185+ for r_id in (relation_ids(relation) or []):
1186+ for unit in (relation_list(r_id) or []):
1187+ if relation_get(key, rid=r_id, unit=unit):
1188+ return True
1189+ return False
1190
1191=== modified file 'hooks/maas.py'
1192--- hooks/maas.py 2013-02-20 10:10:42 +0000
1193+++ hooks/maas.py 2013-03-25 14:21:21 +0000
1194@@ -3,7 +3,7 @@
1195 import json
1196 import subprocess
1197
1198-import utils
1199+import lib.utils as utils
1200
1201 MAAS_STABLE_PPA = 'ppa:maas-maintainers/stable '
1202 MAAS_PROFILE_NAME = 'maas-juju-hacluster'
1203
1204=== modified file 'hooks/pcmk.py'
1205--- hooks/pcmk.py 2013-02-20 10:10:42 +0000
1206+++ hooks/pcmk.py 2013-03-25 14:21:21 +0000
1207@@ -1,4 +1,4 @@
1208-import utils
1209+import lib.utils as utils
1210 import commands
1211 import subprocess
1212
1213
1214=== removed symlink 'hooks/start'
1215=== target was u'hooks.py'
1216=== removed symlink 'hooks/stop'
1217=== target was u'hooks.py'

Subscribers

People subscribed via source and target branches

to all changes: