Merge lp:~corey.bryant/charms/trusty/cinder/action-managed-upgrade into lp:~openstack-charmers-archive/charms/trusty/cinder/next

Proposed by Corey Bryant
Status: Merged
Merged at revision: 124
Proposed branch: lp:~corey.bryant/charms/trusty/cinder/action-managed-upgrade
Merge into: lp:~openstack-charmers-archive/charms/trusty/cinder/next
Diff against target: 945 lines (+584/-164)
9 files modified
actions/openstack_upgrade.py (+13/-40)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+20/-5)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+359/-0)
hooks/charmhelpers/contrib/openstack/utils.py (+51/-0)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+2/-11)
hooks/charmhelpers/core/host.py (+32/-16)
hooks/charmhelpers/core/kernel.py (+68/-0)
tests/charmhelpers/contrib/amulet/utils.py (+9/-0)
unit_tests/test_actions_openstack_upgrade.py (+30/-92)
To merge this branch: bzr merge lp:~corey.bryant/charms/trusty/cinder/action-managed-upgrade
Reviewer Review Type Date Requested Status
Billy Olsen Approve
Review via email: mp+270999@code.launchpad.net
To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #10051 cinder-next for corey.bryant mp270999
    LINT OK: passed

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

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

charm_unit_test #9216 cinder-next for corey.bryant mp270999
    UNIT OK: passed

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

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

charm_amulet_test #6436 cinder-next for corey.bryant mp270999
    AMULET OK: passed

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

134. By Corey Bryant

Update unit tests

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

charm_lint_check #10056 cinder-next for corey.bryant mp270999
    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/12419474/
Build: http://10.245.162.77:8080/job/charm_lint_check/10056/

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

charm_unit_test #9222 cinder-next for corey.bryant mp270999
    UNIT OK: passed

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

135. By Corey Bryant

Fix unit test

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

charm_lint_check #10058 cinder-next for corey.bryant mp270999
    LINT OK: passed

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

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

charm_unit_test #9226 cinder-next for corey.bryant mp270999
    UNIT OK: passed

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

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

charm_amulet_test #6443 cinder-next for corey.bryant mp270999
    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/12420336/
Build: http://10.245.162.77:8080/job/charm_amulet_test/6443/

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

charm_amulet_test #6445 cinder-next for corey.bryant mp270999
    AMULET OK: passed

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

Revision history for this message
Billy Olsen (billy-olsen) wrote :

LGTM. Approved.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'actions/openstack_upgrade.py'
2--- actions/openstack_upgrade.py 2015-09-01 15:13:23 +0000
3+++ actions/openstack_upgrade.py 2015-09-15 18:42:15 +0000
4@@ -1,26 +1,20 @@
5 #!/usr/bin/python
6 import sys
7-import traceback
8 import uuid
9
10 sys.path.append('hooks/')
11
12+from charmhelpers.contrib.openstack.utils import (
13+ do_action_openstack_upgrade,
14+)
15+
16 from charmhelpers.core.hookenv import (
17- action_set,
18- action_fail,
19- config,
20 relation_ids,
21- relation_set
22+ relation_set,
23 )
24
25 from cinder_hooks import config_changed
26
27-from charmhelpers.contrib.openstack.utils import (
28- juju_log,
29- git_install_requested,
30- openstack_upgrade_available
31-)
32-
33 from cinder_utils import (
34 do_openstack_upgrade,
35 register_configs
36@@ -38,35 +32,14 @@
37 code to run, otherwise a full service level upgrade will fire
38 on config-changed."""
39
40- if git_install_requested():
41- action_set({'outcome': 'installed from source, skipped upgrade.'})
42- else:
43- if openstack_upgrade_available('cinder-common'):
44- if config('action-managed-upgrade'):
45- juju_log('Upgrading OpenStack release')
46-
47- try:
48- do_openstack_upgrade(configs=CONFIGS)
49-
50- # NOTE(jamespage) tell any storage-backends we just
51- # upgraded
52- for rid in relation_ids('storage-backend'):
53- relation_set(relation_id=rid,
54- upgrade_nonce=uuid.uuid4())
55-
56- action_set({'outcome': 'success, upgrade completed.'})
57- except:
58- action_set({'outcome': 'upgrade failed, see traceback.'})
59- action_set({'traceback': traceback.format_exc()})
60- action_fail('do_openstack_upgrade resulted in an '
61- 'unexpected error')
62-
63- config_changed()
64- else:
65- action_set({'outcome': 'action-managed-upgrade config is '
66- 'False, skipped upgrade.'})
67- else:
68- action_set({'outcome': 'no upgrade available.'})
69+ if (do_action_openstack_upgrade('cinder-common',
70+ do_openstack_upgrade,
71+ CONFIGS)):
72+ # tell any storage-backends we just upgraded
73+ for rid in relation_ids('storage-backend'):
74+ relation_set(relation_id=rid,
75+ upgrade_nonce=uuid.uuid4())
76+ config_changed()
77
78 if __name__ == '__main__':
79 openstack_upgrade()
80
81=== added directory 'bin'
82=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
83--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-18 17:34:33 +0000
84+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-15 18:42:15 +0000
85@@ -44,8 +44,15 @@
86 Determine if the local branch being tested is derived from its
87 stable or next (dev) branch, and based on this, use the corresonding
88 stable or next branches for the other_services."""
89+
90+ # Charms outside the lp:~openstack-charmers namespace
91 base_charms = ['mysql', 'mongodb', 'nrpe']
92
93+ # Force these charms to current series even when using an older series.
94+ # ie. Use trusty/nrpe even when series is precise, as the P charm
95+ # does not possess the necessary external master config and hooks.
96+ force_series_current = ['nrpe']
97+
98 if self.series in ['precise', 'trusty']:
99 base_series = self.series
100 else:
101@@ -53,11 +60,17 @@
102
103 if self.stable:
104 for svc in other_services:
105+ if svc['name'] in force_series_current:
106+ base_series = self.current_next
107+
108 temp = 'lp:charms/{}/{}'
109 svc['location'] = temp.format(base_series,
110 svc['name'])
111 else:
112 for svc in other_services:
113+ if svc['name'] in force_series_current:
114+ base_series = self.current_next
115+
116 if svc['name'] in base_charms:
117 temp = 'lp:charms/{}/{}'
118 svc['location'] = temp.format(base_series,
119@@ -77,21 +90,23 @@
120
121 services = other_services
122 services.append(this_service)
123+
124+ # Charms which should use the source config option
125 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
126 'ceph-osd', 'ceph-radosgw']
127- # Most OpenStack subordinate charms do not expose an origin option
128- # as that is controlled by the principle.
129- ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
130+
131+ # Charms which can not use openstack-origin, ie. many subordinates
132+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
133
134 if self.openstack:
135 for svc in services:
136- if svc['name'] not in use_source + ignore:
137+ if svc['name'] not in use_source + no_origin:
138 config = {'openstack-origin': self.openstack}
139 self.d.configure(svc['name'], config)
140
141 if self.source:
142 for svc in services:
143- if svc['name'] in use_source and svc['name'] not in ignore:
144+ if svc['name'] in use_source and svc['name'] not in no_origin:
145 config = {'source': self.source}
146 self.d.configure(svc['name'], config)
147
148
149=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
150--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-16 20:17:07 +0000
151+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-15 18:42:15 +0000
152@@ -27,6 +27,7 @@
153 import heatclient.v1.client as heat_client
154 import keystoneclient.v2_0 as keystone_client
155 import novaclient.v1_1.client as nova_client
156+import pika
157 import swiftclient
158
159 from charmhelpers.contrib.amulet.utils import (
160@@ -602,3 +603,361 @@
161 self.log.debug('Ceph {} samples (OK): '
162 '{}'.format(sample_type, samples))
163 return None
164+
165+# rabbitmq/amqp specific helpers:
166+ def add_rmq_test_user(self, sentry_units,
167+ username="testuser1", password="changeme"):
168+ """Add a test user via the first rmq juju unit, check connection as
169+ the new user against all sentry units.
170+
171+ :param sentry_units: list of sentry unit pointers
172+ :param username: amqp user name, default to testuser1
173+ :param password: amqp user password
174+ :returns: None if successful. Raise on error.
175+ """
176+ self.log.debug('Adding rmq user ({})...'.format(username))
177+
178+ # Check that user does not already exist
179+ cmd_user_list = 'rabbitmqctl list_users'
180+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
181+ if username in output:
182+ self.log.warning('User ({}) already exists, returning '
183+ 'gracefully.'.format(username))
184+ return
185+
186+ perms = '".*" ".*" ".*"'
187+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
188+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
189+
190+ # Add user via first unit
191+ for cmd in cmds:
192+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
193+
194+ # Check connection against the other sentry_units
195+ self.log.debug('Checking user connect against units...')
196+ for sentry_unit in sentry_units:
197+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
198+ username=username,
199+ password=password)
200+ connection.close()
201+
202+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
203+ """Delete a rabbitmq user via the first rmq juju unit.
204+
205+ :param sentry_units: list of sentry unit pointers
206+ :param username: amqp user name, default to testuser1
207+ :param password: amqp user password
208+ :returns: None if successful or no such user.
209+ """
210+ self.log.debug('Deleting rmq user ({})...'.format(username))
211+
212+ # Check that the user exists
213+ cmd_user_list = 'rabbitmqctl list_users'
214+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
215+
216+ if username not in output:
217+ self.log.warning('User ({}) does not exist, returning '
218+ 'gracefully.'.format(username))
219+ return
220+
221+ # Delete the user
222+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
223+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
224+
225+ def get_rmq_cluster_status(self, sentry_unit):
226+ """Execute rabbitmq cluster status command on a unit and return
227+ the full output.
228+
229+ :param unit: sentry unit
230+ :returns: String containing console output of cluster status command
231+ """
232+ cmd = 'rabbitmqctl cluster_status'
233+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
234+ self.log.debug('{} cluster_status:\n{}'.format(
235+ sentry_unit.info['unit_name'], output))
236+ return str(output)
237+
238+ def get_rmq_cluster_running_nodes(self, sentry_unit):
239+ """Parse rabbitmqctl cluster_status output string, return list of
240+ running rabbitmq cluster nodes.
241+
242+ :param unit: sentry unit
243+ :returns: List containing node names of running nodes
244+ """
245+ # NOTE(beisner): rabbitmqctl cluster_status output is not
246+ # json-parsable, do string chop foo, then json.loads that.
247+ str_stat = self.get_rmq_cluster_status(sentry_unit)
248+ if 'running_nodes' in str_stat:
249+ pos_start = str_stat.find("{running_nodes,") + 15
250+ pos_end = str_stat.find("]},", pos_start) + 1
251+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
252+ run_nodes = json.loads(str_run_nodes)
253+ return run_nodes
254+ else:
255+ return []
256+
257+ def validate_rmq_cluster_running_nodes(self, sentry_units):
258+ """Check that all rmq unit hostnames are represented in the
259+ cluster_status output of all units.
260+
261+ :param host_names: dict of juju unit names to host names
262+ :param units: list of sentry unit pointers (all rmq units)
263+ :returns: None if successful, otherwise return error message
264+ """
265+ host_names = self.get_unit_hostnames(sentry_units)
266+ errors = []
267+
268+ # Query every unit for cluster_status running nodes
269+ for query_unit in sentry_units:
270+ query_unit_name = query_unit.info['unit_name']
271+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
272+
273+ # Confirm that every unit is represented in the queried unit's
274+ # cluster_status running nodes output.
275+ for validate_unit in sentry_units:
276+ val_host_name = host_names[validate_unit.info['unit_name']]
277+ val_node_name = 'rabbit@{}'.format(val_host_name)
278+
279+ if val_node_name not in running_nodes:
280+ errors.append('Cluster member check failed on {}: {} not '
281+ 'in {}\n'.format(query_unit_name,
282+ val_node_name,
283+ running_nodes))
284+ if errors:
285+ return ''.join(errors)
286+
287+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
288+ """Check a single juju rmq unit for ssl and port in the config file."""
289+ host = sentry_unit.info['public-address']
290+ unit_name = sentry_unit.info['unit_name']
291+
292+ conf_file = '/etc/rabbitmq/rabbitmq.config'
293+ conf_contents = str(self.file_contents_safe(sentry_unit,
294+ conf_file, max_wait=16))
295+ # Checks
296+ conf_ssl = 'ssl' in conf_contents
297+ conf_port = str(port) in conf_contents
298+
299+ # Port explicitly checked in config
300+ if port and conf_port and conf_ssl:
301+ self.log.debug('SSL is enabled @{}:{} '
302+ '({})'.format(host, port, unit_name))
303+ return True
304+ elif port and not conf_port and conf_ssl:
305+ self.log.debug('SSL is enabled @{} but not on port {} '
306+ '({})'.format(host, port, unit_name))
307+ return False
308+ # Port not checked (useful when checking that ssl is disabled)
309+ elif not port and conf_ssl:
310+ self.log.debug('SSL is enabled @{}:{} '
311+ '({})'.format(host, port, unit_name))
312+ return True
313+ elif not port and not conf_ssl:
314+ self.log.debug('SSL not enabled @{}:{} '
315+ '({})'.format(host, port, unit_name))
316+ return False
317+ else:
318+ msg = ('Unknown condition when checking SSL status @{}:{} '
319+ '({})'.format(host, port, unit_name))
320+ amulet.raise_status(amulet.FAIL, msg)
321+
322+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
323+ """Check that ssl is enabled on rmq juju sentry units.
324+
325+ :param sentry_units: list of all rmq sentry units
326+ :param port: optional ssl port override to validate
327+ :returns: None if successful, otherwise return error message
328+ """
329+ for sentry_unit in sentry_units:
330+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
331+ return ('Unexpected condition: ssl is disabled on unit '
332+ '({})'.format(sentry_unit.info['unit_name']))
333+ return None
334+
335+ def validate_rmq_ssl_disabled_units(self, sentry_units):
336+ """Check that ssl is enabled on listed rmq juju sentry units.
337+
338+ :param sentry_units: list of all rmq sentry units
339+ :returns: True if successful. Raise on error.
340+ """
341+ for sentry_unit in sentry_units:
342+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
343+ return ('Unexpected condition: ssl is enabled on unit '
344+ '({})'.format(sentry_unit.info['unit_name']))
345+ return None
346+
347+ def configure_rmq_ssl_on(self, sentry_units, deployment,
348+ port=None, max_wait=60):
349+ """Turn ssl charm config option on, with optional non-default
350+ ssl port specification. Confirm that it is enabled on every
351+ unit.
352+
353+ :param sentry_units: list of sentry units
354+ :param deployment: amulet deployment object pointer
355+ :param port: amqp port, use defaults if None
356+ :param max_wait: maximum time to wait in seconds to confirm
357+ :returns: None if successful. Raise on error.
358+ """
359+ self.log.debug('Setting ssl charm config option: on')
360+
361+ # Enable RMQ SSL
362+ config = {'ssl': 'on'}
363+ if port:
364+ config['ssl_port'] = port
365+
366+ deployment.configure('rabbitmq-server', config)
367+
368+ # Confirm
369+ tries = 0
370+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
371+ while ret and tries < (max_wait / 4):
372+ time.sleep(4)
373+ self.log.debug('Attempt {}: {}'.format(tries, ret))
374+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
375+ tries += 1
376+
377+ if ret:
378+ amulet.raise_status(amulet.FAIL, ret)
379+
380+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
381+ """Turn ssl charm config option off, confirm that it is disabled
382+ on every unit.
383+
384+ :param sentry_units: list of sentry units
385+ :param deployment: amulet deployment object pointer
386+ :param max_wait: maximum time to wait in seconds to confirm
387+ :returns: None if successful. Raise on error.
388+ """
389+ self.log.debug('Setting ssl charm config option: off')
390+
391+ # Disable RMQ SSL
392+ config = {'ssl': 'off'}
393+ deployment.configure('rabbitmq-server', config)
394+
395+ # Confirm
396+ tries = 0
397+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
398+ while ret and tries < (max_wait / 4):
399+ time.sleep(4)
400+ self.log.debug('Attempt {}: {}'.format(tries, ret))
401+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
402+ tries += 1
403+
404+ if ret:
405+ amulet.raise_status(amulet.FAIL, ret)
406+
407+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
408+ port=None, fatal=True,
409+ username="testuser1", password="changeme"):
410+ """Establish and return a pika amqp connection to the rabbitmq service
411+ running on a rmq juju unit.
412+
413+ :param sentry_unit: sentry unit pointer
414+ :param ssl: boolean, default to False
415+ :param port: amqp port, use defaults if None
416+ :param fatal: boolean, default to True (raises on connect error)
417+ :param username: amqp user name, default to testuser1
418+ :param password: amqp user password
419+ :returns: pika amqp connection pointer or None if failed and non-fatal
420+ """
421+ host = sentry_unit.info['public-address']
422+ unit_name = sentry_unit.info['unit_name']
423+
424+ # Default port logic if port is not specified
425+ if ssl and not port:
426+ port = 5671
427+ elif not ssl and not port:
428+ port = 5672
429+
430+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
431+ '{}...'.format(host, port, unit_name, username))
432+
433+ try:
434+ credentials = pika.PlainCredentials(username, password)
435+ parameters = pika.ConnectionParameters(host=host, port=port,
436+ credentials=credentials,
437+ ssl=ssl,
438+ connection_attempts=3,
439+ retry_delay=5,
440+ socket_timeout=1)
441+ connection = pika.BlockingConnection(parameters)
442+ assert connection.server_properties['product'] == 'RabbitMQ'
443+ self.log.debug('Connect OK')
444+ return connection
445+ except Exception as e:
446+ msg = ('amqp connection failed to {}:{} as '
447+ '{} ({})'.format(host, port, username, str(e)))
448+ if fatal:
449+ amulet.raise_status(amulet.FAIL, msg)
450+ else:
451+ self.log.warn(msg)
452+ return None
453+
454+ def publish_amqp_message_by_unit(self, sentry_unit, message,
455+ queue="test", ssl=False,
456+ username="testuser1",
457+ password="changeme",
458+ port=None):
459+ """Publish an amqp message to a rmq juju unit.
460+
461+ :param sentry_unit: sentry unit pointer
462+ :param message: amqp message string
463+ :param queue: message queue, default to test
464+ :param username: amqp user name, default to testuser1
465+ :param password: amqp user password
466+ :param ssl: boolean, default to False
467+ :param port: amqp port, use defaults if None
468+ :returns: None. Raises exception if publish failed.
469+ """
470+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
471+ message))
472+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
473+ port=port,
474+ username=username,
475+ password=password)
476+
477+ # NOTE(beisner): extra debug here re: pika hang potential:
478+ # https://github.com/pika/pika/issues/297
479+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
480+ self.log.debug('Defining channel...')
481+ channel = connection.channel()
482+ self.log.debug('Declaring queue...')
483+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
484+ self.log.debug('Publishing message...')
485+ channel.basic_publish(exchange='', routing_key=queue, body=message)
486+ self.log.debug('Closing channel...')
487+ channel.close()
488+ self.log.debug('Closing connection...')
489+ connection.close()
490+
491+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
492+ username="testuser1",
493+ password="changeme",
494+ ssl=False, port=None):
495+ """Get an amqp message from a rmq juju unit.
496+
497+ :param sentry_unit: sentry unit pointer
498+ :param queue: message queue, default to test
499+ :param username: amqp user name, default to testuser1
500+ :param password: amqp user password
501+ :param ssl: boolean, default to False
502+ :param port: amqp port, use defaults if None
503+ :returns: amqp message body as string. Raise if get fails.
504+ """
505+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
506+ port=port,
507+ username=username,
508+ password=password)
509+ channel = connection.channel()
510+ method_frame, _, body = channel.basic_get(queue)
511+
512+ if method_frame:
513+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
514+ body))
515+ channel.basic_ack(method_frame.delivery_tag)
516+ channel.close()
517+ connection.close()
518+ return body
519+ else:
520+ msg = 'No message retrieved.'
521+ amulet.raise_status(amulet.FAIL, msg)
522
523=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
524--- hooks/charmhelpers/contrib/openstack/utils.py 2015-09-03 09:43:34 +0000
525+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-09-15 18:42:15 +0000
526@@ -25,6 +25,7 @@
527 import re
528
529 import six
530+import traceback
531 import yaml
532
533 from charmhelpers.contrib.network import ip
534@@ -34,6 +35,8 @@
535 )
536
537 from charmhelpers.core.hookenv import (
538+ action_fail,
539+ action_set,
540 config,
541 log as juju_log,
542 charm_dir,
543@@ -114,6 +117,7 @@
544 ('2.2.1', 'kilo'),
545 ('2.2.2', 'kilo'),
546 ('2.3.0', 'liberty'),
547+ ('2.4.0', 'liberty'),
548 ])
549
550 # >= Liberty version->codename mapping
551@@ -142,6 +146,9 @@
552 'glance-common': OrderedDict([
553 ('11.0.0', 'liberty'),
554 ]),
555+ 'openstack-dashboard': OrderedDict([
556+ ('8.0.0', 'liberty'),
557+ ]),
558 }
559
560 DEFAULT_LOOPBACK_SIZE = '5G'
561@@ -745,3 +752,47 @@
562 return projects[key]
563
564 return None
565+
566+
567+def do_action_openstack_upgrade(package, upgrade_callback, configs):
568+ """Perform action-managed OpenStack upgrade.
569+
570+ Upgrades packages to the configured openstack-origin version and sets
571+ the corresponding action status as a result.
572+
573+ If the charm was installed from source we cannot upgrade it.
574+ For backwards compatibility a config flag (action-managed-upgrade) must
575+ be set for this code to run, otherwise a full service level upgrade will
576+ fire on config-changed.
577+
578+ @param package: package name for determining if upgrade available
579+ @param upgrade_callback: function callback to charm's upgrade function
580+ @param configs: templating object derived from OSConfigRenderer class
581+
582+ @return: True if upgrade successful; False if upgrade failed or skipped
583+ """
584+ ret = False
585+
586+ if git_install_requested():
587+ action_set({'outcome': 'installed from source, skipped upgrade.'})
588+ else:
589+ if openstack_upgrade_available(package):
590+ if config('action-managed-upgrade'):
591+ juju_log('Upgrading OpenStack release')
592+
593+ try:
594+ upgrade_callback(configs=configs)
595+ action_set({'outcome': 'success, upgrade completed.'})
596+ ret = True
597+ except:
598+ action_set({'outcome': 'upgrade failed, see traceback.'})
599+ action_set({'traceback': traceback.format_exc()})
600+ action_fail('do_openstack_upgrade resulted in an '
601+ 'unexpected error')
602+ else:
603+ action_set({'outcome': 'action-managed-upgrade config is '
604+ 'False, skipped upgrade.'})
605+ else:
606+ action_set({'outcome': 'no upgrade available.'})
607+
608+ return ret
609
610=== modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py'
611--- hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-09-10 09:30:00 +0000
612+++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-09-15 18:42:15 +0000
613@@ -59,6 +59,8 @@
614 apt_install,
615 )
616
617+from charmhelpers.core.kernel import modprobe
618+
619 KEYRING = '/etc/ceph/ceph.client.{}.keyring'
620 KEYFILE = '/etc/ceph/ceph.client.{}.key'
621
622@@ -291,17 +293,6 @@
623 os.chown(data_src_dst, uid, gid)
624
625
626-# TODO: re-use
627-def modprobe(module):
628- """Load a kernel module and configure for auto-load on reboot."""
629- log('Loading kernel module', level=INFO)
630- cmd = ['modprobe', module]
631- check_call(cmd)
632- with open('/etc/modules', 'r+') as modules:
633- if module not in modules.read():
634- modules.write(module)
635-
636-
637 def copy_files(src, dst, symlinks=False, ignore=None):
638 """Copy files from src to dst."""
639 for item in os.listdir(src):
640
641=== modified file 'hooks/charmhelpers/core/host.py'
642--- hooks/charmhelpers/core/host.py 2015-08-19 13:51:56 +0000
643+++ hooks/charmhelpers/core/host.py 2015-09-15 18:42:15 +0000
644@@ -63,32 +63,48 @@
645 return service_result
646
647
648-def service_pause(service_name, init_dir=None):
649+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
650 """Pause a system service.
651
652 Stop it, and prevent it from starting again at boot."""
653- if init_dir is None:
654- init_dir = "/etc/init"
655 stopped = service_stop(service_name)
656- # XXX: Support systemd too
657- override_path = os.path.join(
658- init_dir, '{}.override'.format(service_name))
659- with open(override_path, 'w') as fh:
660- fh.write("manual\n")
661+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
662+ sysv_file = os.path.join(initd_dir, service_name)
663+ if os.path.exists(upstart_file):
664+ override_path = os.path.join(
665+ init_dir, '{}.override'.format(service_name))
666+ with open(override_path, 'w') as fh:
667+ fh.write("manual\n")
668+ elif os.path.exists(sysv_file):
669+ subprocess.check_call(["update-rc.d", service_name, "disable"])
670+ else:
671+ # XXX: Support SystemD too
672+ raise ValueError(
673+ "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
674+ service_name, upstart_file, sysv_file))
675 return stopped
676
677
678-def service_resume(service_name, init_dir=None):
679+def service_resume(service_name, init_dir="/etc/init",
680+ initd_dir="/etc/init.d"):
681 """Resume a system service.
682
683 Reenable starting again at boot. Start the service"""
684- # XXX: Support systemd too
685- if init_dir is None:
686- init_dir = "/etc/init"
687- override_path = os.path.join(
688- init_dir, '{}.override'.format(service_name))
689- if os.path.exists(override_path):
690- os.unlink(override_path)
691+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
692+ sysv_file = os.path.join(initd_dir, service_name)
693+ if os.path.exists(upstart_file):
694+ override_path = os.path.join(
695+ init_dir, '{}.override'.format(service_name))
696+ if os.path.exists(override_path):
697+ os.unlink(override_path)
698+ elif os.path.exists(sysv_file):
699+ subprocess.check_call(["update-rc.d", service_name, "enable"])
700+ else:
701+ # XXX: Support SystemD too
702+ raise ValueError(
703+ "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
704+ service_name, upstart_file, sysv_file))
705+
706 started = service_start(service_name)
707 return started
708
709
710=== added file 'hooks/charmhelpers/core/kernel.py'
711--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
712+++ hooks/charmhelpers/core/kernel.py 2015-09-15 18:42:15 +0000
713@@ -0,0 +1,68 @@
714+#!/usr/bin/env python
715+# -*- coding: utf-8 -*-
716+
717+# Copyright 2014-2015 Canonical Limited.
718+#
719+# This file is part of charm-helpers.
720+#
721+# charm-helpers is free software: you can redistribute it and/or modify
722+# it under the terms of the GNU Lesser General Public License version 3 as
723+# published by the Free Software Foundation.
724+#
725+# charm-helpers is distributed in the hope that it will be useful,
726+# but WITHOUT ANY WARRANTY; without even the implied warranty of
727+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
728+# GNU Lesser General Public License for more details.
729+#
730+# You should have received a copy of the GNU Lesser General Public License
731+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
732+
733+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
734+
735+from charmhelpers.core.hookenv import (
736+ log,
737+ INFO
738+)
739+
740+from subprocess import check_call, check_output
741+import re
742+
743+
744+def modprobe(module, persist=True):
745+ """Load a kernel module and configure for auto-load on reboot."""
746+ cmd = ['modprobe', module]
747+
748+ log('Loading kernel module %s' % module, level=INFO)
749+
750+ check_call(cmd)
751+ if persist:
752+ with open('/etc/modules', 'r+') as modules:
753+ if module not in modules.read():
754+ modules.write(module)
755+
756+
757+def rmmod(module, force=False):
758+ """Remove a module from the linux kernel"""
759+ cmd = ['rmmod']
760+ if force:
761+ cmd.append('-f')
762+ cmd.append(module)
763+ log('Removing kernel module %s' % module, level=INFO)
764+ return check_call(cmd)
765+
766+
767+def lsmod():
768+ """Shows what kernel modules are currently loaded"""
769+ return check_output(['lsmod'],
770+ universal_newlines=True)
771+
772+
773+def is_module_loaded(module):
774+ """Checks if a kernel module is already loaded"""
775+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
776+ return len(matches) > 0
777+
778+
779+def update_initramfs(version='all'):
780+ """Updates an initramfs image"""
781+ return check_call(["update-initramfs", "-k", version, "-u"])
782
783=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
784--- tests/charmhelpers/contrib/amulet/utils.py 2015-09-10 09:30:00 +0000
785+++ tests/charmhelpers/contrib/amulet/utils.py 2015-09-15 18:42:15 +0000
786@@ -776,3 +776,12 @@
787 output = _check_output(command, universal_newlines=True)
788 data = json.loads(output)
789 return data.get(u"status") == "completed"
790+
791+ def status_get(self, unit):
792+ """Return the current service status of this unit."""
793+ raw_status, return_code = unit.run(
794+ "status-get --format=json --include-data")
795+ if return_code != 0:
796+ return ("unknown", "")
797+ status = json.loads(raw_status)
798+ return (status["status"], status["message"])
799
800=== modified file 'unit_tests/test_actions_openstack_upgrade.py'
801--- unit_tests/test_actions_openstack_upgrade.py 2015-09-01 15:13:23 +0000
802+++ unit_tests/test_actions_openstack_upgrade.py 2015-09-15 18:42:15 +0000
803@@ -11,8 +11,9 @@
804 )
805
806 TO_PATCH = [
807- 'config',
808- 'juju_log',
809+ 'config_changed',
810+ 'do_openstack_upgrade',
811+ 'register_configs',
812 'relation_set',
813 'relation_ids',
814 'uuid'
815@@ -24,103 +25,40 @@
816 def setUp(self):
817 super(TestCinderUpgradeActions, self).setUp(openstack_upgrade,
818 TO_PATCH)
819- self.config.side_effect = self.test_config.get
820
821- @patch.object(openstack_upgrade, 'action_set')
822- @patch.object(openstack_upgrade, 'action_fail')
823- @patch.object(openstack_upgrade, 'do_openstack_upgrade')
824- @patch.object(openstack_upgrade, 'openstack_upgrade_available')
825- @patch.object(openstack_upgrade, 'config_changed')
826 @patch('charmhelpers.contrib.openstack.utils.config')
827- def test_openstack_upgrade(self, _config, config_changed,
828- openstack_upgrade_available,
829- do_openstack_upgrade, action_fail,
830- action_set):
831- _config.return_value = None
832- openstack_upgrade_available.return_value = True
833+ @patch('charmhelpers.contrib.openstack.utils.action_set')
834+ @patch('charmhelpers.contrib.openstack.utils.git_install_requested')
835+ @patch('charmhelpers.contrib.openstack.utils.openstack_upgrade_available')
836+ def test_openstack_upgrade_true(self, upgrade_avail, git_requested,
837+ action_set, config):
838+ git_requested.return_value = False
839+ upgrade_avail.return_value = True
840+ config.return_value = True
841 self.relation_ids.return_value = ['relid1']
842 self.uuid.uuid4.return_value = 12345
843
844- self.test_config.set('action-managed-upgrade', True)
845-
846 openstack_upgrade.openstack_upgrade()
847
848- self.assertTrue(do_openstack_upgrade.called)
849- self.assertTrue(config_changed.called)
850+ self.assertTrue(self.do_openstack_upgrade.called)
851 self.assertTrue(self.relation_ids.called)
852 self.relation_set.assert_called_with(relation_id='relid1',
853 upgrade_nonce=12345)
854- self.assertFalse(action_fail.called)
855-
856- @patch.object(openstack_upgrade, 'action_set')
857- @patch.object(openstack_upgrade, 'do_openstack_upgrade')
858- @patch.object(openstack_upgrade, 'openstack_upgrade_available')
859- @patch.object(openstack_upgrade, 'config_changed')
860- @patch('charmhelpers.contrib.openstack.utils.config')
861- def test_openstack_upgrade_not_configured(self, _config, config_changed,
862- openstack_upgrade_available,
863- do_openstack_upgrade,
864- action_set):
865- _config.return_value = None
866- openstack_upgrade_available.return_value = True
867-
868- openstack_upgrade.openstack_upgrade()
869-
870- msg = ('action-managed-upgrade config is False, skipped upgrade.')
871-
872- action_set.assert_called_with({'outcome': msg})
873- self.assertFalse(do_openstack_upgrade.called)
874-
875- @patch.object(openstack_upgrade, 'action_set')
876- @patch.object(openstack_upgrade, 'do_openstack_upgrade')
877- @patch.object(openstack_upgrade, 'openstack_upgrade_available')
878- @patch.object(openstack_upgrade, 'config_changed')
879- @patch('charmhelpers.contrib.openstack.utils.config')
880- def test_openstack_upgrade_git_install(self, _config, config_changed,
881- openstack_upgrade_available,
882- do_openstack_upgrade,
883- action_set):
884-
885- self.test_config.set('action-managed-upgrade', True)
886- self.test_config.set('openstack-origin-git', True)
887-
888- openstack_upgrade.openstack_upgrade()
889-
890- msg = ('installed from source, skipped upgrade.')
891- action_set.assert_called_with({'outcome': msg})
892- self.assertFalse(do_openstack_upgrade.called)
893-
894- @patch.object(openstack_upgrade, 'action_set')
895- @patch.object(openstack_upgrade, 'action_fail')
896- @patch.object(openstack_upgrade, 'do_openstack_upgrade')
897- @patch.object(openstack_upgrade, 'openstack_upgrade_available')
898- @patch.object(openstack_upgrade, 'config_changed')
899- @patch('traceback.format_exc')
900- @patch('charmhelpers.contrib.openstack.utils.config')
901- def test_openstack_upgrade_exception(self, _config, format_exc,
902- config_changed,
903- openstack_upgrade_available,
904- do_openstack_upgrade,
905- action_fail, action_set):
906- _config.return_value = None
907- self.test_config.set('action-managed-upgrade', True)
908- openstack_upgrade_available.return_value = True
909-
910- e = OSError('something bad happened')
911- do_openstack_upgrade.side_effect = e
912- traceback = (
913- "Traceback (most recent call last):\n"
914- " File \"actions/openstack_upgrade.py\", line 37, in openstack_upgrade\n" # noqa
915- " openstack_upgrade(config(\'openstack-origin-git\'))\n"
916- " File \"/usr/lib/python2.7/dist-packages/mock.py\", line 964, in __call__\n" # noqa
917- " return _mock_self._mock_call(*args, **kwargs)\n"
918- " File \"/usr/lib/python2.7/dist-packages/mock.py\", line 1019, in _mock_call\n" # noqa
919- " raise effect\n"
920- "OSError: something bad happened\n")
921- format_exc.return_value = traceback
922-
923- openstack_upgrade.openstack_upgrade()
924-
925- msg = 'do_openstack_upgrade resulted in an unexpected error'
926- action_fail.assert_called_with(msg)
927- action_set.assert_called_with({'traceback': traceback})
928+ self.assertTrue(self.config_changed.called)
929+
930+ @patch('charmhelpers.contrib.openstack.utils.config')
931+ @patch('charmhelpers.contrib.openstack.utils.action_set')
932+ @patch('charmhelpers.contrib.openstack.utils.git_install_requested')
933+ @patch('charmhelpers.contrib.openstack.utils.openstack_upgrade_available')
934+ def test_openstack_upgrade_false(self, upgrade_avail, git_requested,
935+ action_set, config):
936+ git_requested.return_value = False
937+ upgrade_avail.return_value = True
938+ config.return_value = False
939+
940+ openstack_upgrade.openstack_upgrade()
941+
942+ self.assertFalse(self.do_openstack_upgrade.called)
943+ self.assertFalse(self.relation_ids.called)
944+ self.assertFalse(self.relation_set.called)
945+ self.assertFalse(self.config_changed.called)

Subscribers

People subscribed via source and target branches