Merge lp:~gnuoy/charms/trusty/cinder-ceph/workloadstatus into lp:~openstack-charmers-archive/charms/trusty/cinder-ceph/next

Proposed by Liam Young
Status: Merged
Merged at revision: 45
Proposed branch: lp:~gnuoy/charms/trusty/cinder-ceph/workloadstatus
Merge into: lp:~openstack-charmers-archive/charms/trusty/cinder-ceph/next
Diff against target: 1778 lines (+1035/-98)
22 files modified
charm-helpers-hooks.yaml (+1/-0)
hooks/charmhelpers/contrib/network/ip.py (+5/-3)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+23/-9)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+359/-0)
hooks/charmhelpers/contrib/openstack/context.py (+92/-20)
hooks/charmhelpers/contrib/openstack/neutron.py (+17/-3)
hooks/charmhelpers/contrib/openstack/templates/ceph.conf (+6/-0)
hooks/charmhelpers/contrib/openstack/templating.py (+30/-2)
hooks/charmhelpers/contrib/openstack/utils.py (+232/-2)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+2/-11)
hooks/charmhelpers/core/hookenv.py (+32/-0)
hooks/charmhelpers/core/host.py (+32/-16)
hooks/charmhelpers/core/hugepage.py (+8/-1)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/strutils.py (+30/-0)
hooks/cinder_hooks.py (+10/-1)
hooks/cinder_utils.py (+16/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+4/-2)
tests/charmhelpers/contrib/amulet/utils.py (+56/-16)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+9/-10)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+1/-1)
unit_tests/test_cinder_hooks.py (+2/-1)
To merge this branch: bzr merge lp:~gnuoy/charms/trusty/cinder-ceph/workloadstatus
Reviewer Review Type Date Requested Status
OpenStack Charmers Pending
Review via email: mp+273861@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 #11544 cinder-ceph-next for gnuoy mp273861
    LINT OK: passed

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

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

charm_unit_test #10736 cinder-ceph-next for gnuoy mp273861
    UNIT OK: passed

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

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

charm_amulet_test #7245 cinder-ceph-next for gnuoy mp273861
    AMULET OK: passed

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

Revision history for this message
James Page (james-page) wrote :

I merged this with a tweak to drop the optional function from set_os_workload_status; as this charm does not have any optional interfaces, it was really not required.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charm-helpers-hooks.yaml'
2--- charm-helpers-hooks.yaml 2015-07-31 13:11:44 +0000
3+++ charm-helpers-hooks.yaml 2015-10-08 15:47:11 +0000
4@@ -5,6 +5,7 @@
5 - cli
6 - fetch
7 - contrib.openstack|inc=*
8+ - contrib.openstack.utils
9 - contrib.storage
10 - contrib.hahelpers
11 - contrib.network.ip
12
13=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
14--- hooks/charmhelpers/contrib/network/ip.py 2015-09-03 09:44:48 +0000
15+++ hooks/charmhelpers/contrib/network/ip.py 2015-10-08 15:47:11 +0000
16@@ -23,7 +23,7 @@
17 from functools import partial
18
19 from charmhelpers.core.hookenv import unit_get
20-from charmhelpers.fetch import apt_install
21+from charmhelpers.fetch import apt_install, apt_update
22 from charmhelpers.core.hookenv import (
23 log,
24 WARNING,
25@@ -32,13 +32,15 @@
26 try:
27 import netifaces
28 except ImportError:
29- apt_install('python-netifaces')
30+ apt_update(fatal=True)
31+ apt_install('python-netifaces', fatal=True)
32 import netifaces
33
34 try:
35 import netaddr
36 except ImportError:
37- apt_install('python-netaddr')
38+ apt_update(fatal=True)
39+ apt_install('python-netaddr', fatal=True)
40 import netaddr
41
42
43
44=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
45--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-18 17:34:34 +0000
46+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-10-08 15:47:11 +0000
47@@ -44,20 +44,31 @@
48 Determine if the local branch being tested is derived from its
49 stable or next (dev) branch, and based on this, use the corresonding
50 stable or next branches for the other_services."""
51+
52+ # Charms outside the lp:~openstack-charmers namespace
53 base_charms = ['mysql', 'mongodb', 'nrpe']
54
55+ # Force these charms to current series even when using an older series.
56+ # ie. Use trusty/nrpe even when series is precise, as the P charm
57+ # does not possess the necessary external master config and hooks.
58+ force_series_current = ['nrpe']
59+
60 if self.series in ['precise', 'trusty']:
61 base_series = self.series
62 else:
63 base_series = self.current_next
64
65- if self.stable:
66- for svc in other_services:
67+ for svc in other_services:
68+ if svc['name'] in force_series_current:
69+ base_series = self.current_next
70+ # If a location has been explicitly set, use it
71+ if svc.get('location'):
72+ continue
73+ if self.stable:
74 temp = 'lp:charms/{}/{}'
75 svc['location'] = temp.format(base_series,
76 svc['name'])
77- else:
78- for svc in other_services:
79+ else:
80 if svc['name'] in base_charms:
81 temp = 'lp:charms/{}/{}'
82 svc['location'] = temp.format(base_series,
83@@ -66,6 +77,7 @@
84 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
85 svc['location'] = temp.format(self.current_next,
86 svc['name'])
87+
88 return other_services
89
90 def _add_services(self, this_service, other_services):
91@@ -77,21 +89,23 @@
92
93 services = other_services
94 services.append(this_service)
95+
96+ # Charms which should use the source config option
97 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
98 'ceph-osd', 'ceph-radosgw']
99- # Most OpenStack subordinate charms do not expose an origin option
100- # as that is controlled by the principle.
101- ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
102+
103+ # Charms which can not use openstack-origin, ie. many subordinates
104+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
105
106 if self.openstack:
107 for svc in services:
108- if svc['name'] not in use_source + ignore:
109+ if svc['name'] not in use_source + no_origin:
110 config = {'openstack-origin': self.openstack}
111 self.d.configure(svc['name'], config)
112
113 if self.source:
114 for svc in services:
115- if svc['name'] in use_source and svc['name'] not in ignore:
116+ if svc['name'] in use_source and svc['name'] not in no_origin:
117 config = {'source': self.source}
118 self.d.configure(svc['name'], config)
119
120
121=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
122--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-29 10:50:52 +0000
123+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-10-08 15:47:11 +0000
124@@ -27,6 +27,7 @@
125 import heatclient.v1.client as heat_client
126 import keystoneclient.v2_0 as keystone_client
127 import novaclient.v1_1.client as nova_client
128+import pika
129 import swiftclient
130
131 from charmhelpers.contrib.amulet.utils import (
132@@ -602,3 +603,361 @@
133 self.log.debug('Ceph {} samples (OK): '
134 '{}'.format(sample_type, samples))
135 return None
136+
137+# rabbitmq/amqp specific helpers:
138+ def add_rmq_test_user(self, sentry_units,
139+ username="testuser1", password="changeme"):
140+ """Add a test user via the first rmq juju unit, check connection as
141+ the new user against all sentry units.
142+
143+ :param sentry_units: list of sentry unit pointers
144+ :param username: amqp user name, default to testuser1
145+ :param password: amqp user password
146+ :returns: None if successful. Raise on error.
147+ """
148+ self.log.debug('Adding rmq user ({})...'.format(username))
149+
150+ # Check that user does not already exist
151+ cmd_user_list = 'rabbitmqctl list_users'
152+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
153+ if username in output:
154+ self.log.warning('User ({}) already exists, returning '
155+ 'gracefully.'.format(username))
156+ return
157+
158+ perms = '".*" ".*" ".*"'
159+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
160+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
161+
162+ # Add user via first unit
163+ for cmd in cmds:
164+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
165+
166+ # Check connection against the other sentry_units
167+ self.log.debug('Checking user connect against units...')
168+ for sentry_unit in sentry_units:
169+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
170+ username=username,
171+ password=password)
172+ connection.close()
173+
174+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
175+ """Delete a rabbitmq user via the first rmq juju unit.
176+
177+ :param sentry_units: list of sentry unit pointers
178+ :param username: amqp user name, default to testuser1
179+ :param password: amqp user password
180+ :returns: None if successful or no such user.
181+ """
182+ self.log.debug('Deleting rmq user ({})...'.format(username))
183+
184+ # Check that the user exists
185+ cmd_user_list = 'rabbitmqctl list_users'
186+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
187+
188+ if username not in output:
189+ self.log.warning('User ({}) does not exist, returning '
190+ 'gracefully.'.format(username))
191+ return
192+
193+ # Delete the user
194+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
195+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
196+
197+ def get_rmq_cluster_status(self, sentry_unit):
198+ """Execute rabbitmq cluster status command on a unit and return
199+ the full output.
200+
201+ :param unit: sentry unit
202+ :returns: String containing console output of cluster status command
203+ """
204+ cmd = 'rabbitmqctl cluster_status'
205+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
206+ self.log.debug('{} cluster_status:\n{}'.format(
207+ sentry_unit.info['unit_name'], output))
208+ return str(output)
209+
210+ def get_rmq_cluster_running_nodes(self, sentry_unit):
211+ """Parse rabbitmqctl cluster_status output string, return list of
212+ running rabbitmq cluster nodes.
213+
214+ :param unit: sentry unit
215+ :returns: List containing node names of running nodes
216+ """
217+ # NOTE(beisner): rabbitmqctl cluster_status output is not
218+ # json-parsable, do string chop foo, then json.loads that.
219+ str_stat = self.get_rmq_cluster_status(sentry_unit)
220+ if 'running_nodes' in str_stat:
221+ pos_start = str_stat.find("{running_nodes,") + 15
222+ pos_end = str_stat.find("]},", pos_start) + 1
223+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
224+ run_nodes = json.loads(str_run_nodes)
225+ return run_nodes
226+ else:
227+ return []
228+
229+ def validate_rmq_cluster_running_nodes(self, sentry_units):
230+ """Check that all rmq unit hostnames are represented in the
231+ cluster_status output of all units.
232+
233+ :param host_names: dict of juju unit names to host names
234+ :param units: list of sentry unit pointers (all rmq units)
235+ :returns: None if successful, otherwise return error message
236+ """
237+ host_names = self.get_unit_hostnames(sentry_units)
238+ errors = []
239+
240+ # Query every unit for cluster_status running nodes
241+ for query_unit in sentry_units:
242+ query_unit_name = query_unit.info['unit_name']
243+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
244+
245+ # Confirm that every unit is represented in the queried unit's
246+ # cluster_status running nodes output.
247+ for validate_unit in sentry_units:
248+ val_host_name = host_names[validate_unit.info['unit_name']]
249+ val_node_name = 'rabbit@{}'.format(val_host_name)
250+
251+ if val_node_name not in running_nodes:
252+ errors.append('Cluster member check failed on {}: {} not '
253+ 'in {}\n'.format(query_unit_name,
254+ val_node_name,
255+ running_nodes))
256+ if errors:
257+ return ''.join(errors)
258+
259+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
260+ """Check a single juju rmq unit for ssl and port in the config file."""
261+ host = sentry_unit.info['public-address']
262+ unit_name = sentry_unit.info['unit_name']
263+
264+ conf_file = '/etc/rabbitmq/rabbitmq.config'
265+ conf_contents = str(self.file_contents_safe(sentry_unit,
266+ conf_file, max_wait=16))
267+ # Checks
268+ conf_ssl = 'ssl' in conf_contents
269+ conf_port = str(port) in conf_contents
270+
271+ # Port explicitly checked in config
272+ if port and conf_port and conf_ssl:
273+ self.log.debug('SSL is enabled @{}:{} '
274+ '({})'.format(host, port, unit_name))
275+ return True
276+ elif port and not conf_port and conf_ssl:
277+ self.log.debug('SSL is enabled @{} but not on port {} '
278+ '({})'.format(host, port, unit_name))
279+ return False
280+ # Port not checked (useful when checking that ssl is disabled)
281+ elif not port and conf_ssl:
282+ self.log.debug('SSL is enabled @{}:{} '
283+ '({})'.format(host, port, unit_name))
284+ return True
285+ elif not conf_ssl:
286+ self.log.debug('SSL not enabled @{}:{} '
287+ '({})'.format(host, port, unit_name))
288+ return False
289+ else:
290+ msg = ('Unknown condition when checking SSL status @{}:{} '
291+ '({})'.format(host, port, unit_name))
292+ amulet.raise_status(amulet.FAIL, msg)
293+
294+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
295+ """Check that ssl is enabled on rmq juju sentry units.
296+
297+ :param sentry_units: list of all rmq sentry units
298+ :param port: optional ssl port override to validate
299+ :returns: None if successful, otherwise return error message
300+ """
301+ for sentry_unit in sentry_units:
302+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
303+ return ('Unexpected condition: ssl is disabled on unit '
304+ '({})'.format(sentry_unit.info['unit_name']))
305+ return None
306+
307+ def validate_rmq_ssl_disabled_units(self, sentry_units):
308+ """Check that ssl is enabled on listed rmq juju sentry units.
309+
310+ :param sentry_units: list of all rmq sentry units
311+ :returns: True if successful. Raise on error.
312+ """
313+ for sentry_unit in sentry_units:
314+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
315+ return ('Unexpected condition: ssl is enabled on unit '
316+ '({})'.format(sentry_unit.info['unit_name']))
317+ return None
318+
319+ def configure_rmq_ssl_on(self, sentry_units, deployment,
320+ port=None, max_wait=60):
321+ """Turn ssl charm config option on, with optional non-default
322+ ssl port specification. Confirm that it is enabled on every
323+ unit.
324+
325+ :param sentry_units: list of sentry units
326+ :param deployment: amulet deployment object pointer
327+ :param port: amqp port, use defaults if None
328+ :param max_wait: maximum time to wait in seconds to confirm
329+ :returns: None if successful. Raise on error.
330+ """
331+ self.log.debug('Setting ssl charm config option: on')
332+
333+ # Enable RMQ SSL
334+ config = {'ssl': 'on'}
335+ if port:
336+ config['ssl_port'] = port
337+
338+ deployment.configure('rabbitmq-server', config)
339+
340+ # Confirm
341+ tries = 0
342+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
343+ while ret and tries < (max_wait / 4):
344+ time.sleep(4)
345+ self.log.debug('Attempt {}: {}'.format(tries, ret))
346+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
347+ tries += 1
348+
349+ if ret:
350+ amulet.raise_status(amulet.FAIL, ret)
351+
352+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
353+ """Turn ssl charm config option off, confirm that it is disabled
354+ on every unit.
355+
356+ :param sentry_units: list of sentry units
357+ :param deployment: amulet deployment object pointer
358+ :param max_wait: maximum time to wait in seconds to confirm
359+ :returns: None if successful. Raise on error.
360+ """
361+ self.log.debug('Setting ssl charm config option: off')
362+
363+ # Disable RMQ SSL
364+ config = {'ssl': 'off'}
365+ deployment.configure('rabbitmq-server', config)
366+
367+ # Confirm
368+ tries = 0
369+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
370+ while ret and tries < (max_wait / 4):
371+ time.sleep(4)
372+ self.log.debug('Attempt {}: {}'.format(tries, ret))
373+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
374+ tries += 1
375+
376+ if ret:
377+ amulet.raise_status(amulet.FAIL, ret)
378+
379+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
380+ port=None, fatal=True,
381+ username="testuser1", password="changeme"):
382+ """Establish and return a pika amqp connection to the rabbitmq service
383+ running on a rmq juju unit.
384+
385+ :param sentry_unit: sentry unit pointer
386+ :param ssl: boolean, default to False
387+ :param port: amqp port, use defaults if None
388+ :param fatal: boolean, default to True (raises on connect error)
389+ :param username: amqp user name, default to testuser1
390+ :param password: amqp user password
391+ :returns: pika amqp connection pointer or None if failed and non-fatal
392+ """
393+ host = sentry_unit.info['public-address']
394+ unit_name = sentry_unit.info['unit_name']
395+
396+ # Default port logic if port is not specified
397+ if ssl and not port:
398+ port = 5671
399+ elif not ssl and not port:
400+ port = 5672
401+
402+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
403+ '{}...'.format(host, port, unit_name, username))
404+
405+ try:
406+ credentials = pika.PlainCredentials(username, password)
407+ parameters = pika.ConnectionParameters(host=host, port=port,
408+ credentials=credentials,
409+ ssl=ssl,
410+ connection_attempts=3,
411+ retry_delay=5,
412+ socket_timeout=1)
413+ connection = pika.BlockingConnection(parameters)
414+ assert connection.server_properties['product'] == 'RabbitMQ'
415+ self.log.debug('Connect OK')
416+ return connection
417+ except Exception as e:
418+ msg = ('amqp connection failed to {}:{} as '
419+ '{} ({})'.format(host, port, username, str(e)))
420+ if fatal:
421+ amulet.raise_status(amulet.FAIL, msg)
422+ else:
423+ self.log.warn(msg)
424+ return None
425+
426+ def publish_amqp_message_by_unit(self, sentry_unit, message,
427+ queue="test", ssl=False,
428+ username="testuser1",
429+ password="changeme",
430+ port=None):
431+ """Publish an amqp message to a rmq juju unit.
432+
433+ :param sentry_unit: sentry unit pointer
434+ :param message: amqp message string
435+ :param queue: message queue, default to test
436+ :param username: amqp user name, default to testuser1
437+ :param password: amqp user password
438+ :param ssl: boolean, default to False
439+ :param port: amqp port, use defaults if None
440+ :returns: None. Raises exception if publish failed.
441+ """
442+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
443+ message))
444+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
445+ port=port,
446+ username=username,
447+ password=password)
448+
449+ # NOTE(beisner): extra debug here re: pika hang potential:
450+ # https://github.com/pika/pika/issues/297
451+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
452+ self.log.debug('Defining channel...')
453+ channel = connection.channel()
454+ self.log.debug('Declaring queue...')
455+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
456+ self.log.debug('Publishing message...')
457+ channel.basic_publish(exchange='', routing_key=queue, body=message)
458+ self.log.debug('Closing channel...')
459+ channel.close()
460+ self.log.debug('Closing connection...')
461+ connection.close()
462+
463+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
464+ username="testuser1",
465+ password="changeme",
466+ ssl=False, port=None):
467+ """Get an amqp message from a rmq juju unit.
468+
469+ :param sentry_unit: sentry unit pointer
470+ :param queue: message queue, default to test
471+ :param username: amqp user name, default to testuser1
472+ :param password: amqp user password
473+ :param ssl: boolean, default to False
474+ :param port: amqp port, use defaults if None
475+ :returns: amqp message body as string. Raise if get fails.
476+ """
477+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
478+ port=port,
479+ username=username,
480+ password=password)
481+ channel = connection.channel()
482+ method_frame, _, body = channel.basic_get(queue)
483+
484+ if method_frame:
485+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
486+ body))
487+ channel.basic_ack(method_frame.delivery_tag)
488+ channel.close()
489+ connection.close()
490+ return body
491+ else:
492+ msg = 'No message retrieved.'
493+ amulet.raise_status(amulet.FAIL, msg)
494
495=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
496--- hooks/charmhelpers/contrib/openstack/context.py 2015-09-16 01:07:56 +0000
497+++ hooks/charmhelpers/contrib/openstack/context.py 2015-10-08 15:47:11 +0000
498@@ -14,6 +14,7 @@
499 # You should have received a copy of the GNU Lesser General Public License
500 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
501
502+import glob
503 import json
504 import os
505 import re
506@@ -194,10 +195,50 @@
507 class OSContextGenerator(object):
508 """Base class for all context generators."""
509 interfaces = []
510+ related = False
511+ complete = False
512+ missing_data = []
513
514 def __call__(self):
515 raise NotImplementedError
516
517+ def context_complete(self, ctxt):
518+ """Check for missing data for the required context data.
519+ Set self.missing_data if it exists and return False.
520+ Set self.complete if no missing data and return True.
521+ """
522+ # Fresh start
523+ self.complete = False
524+ self.missing_data = []
525+ for k, v in six.iteritems(ctxt):
526+ if v is None or v == '':
527+ if k not in self.missing_data:
528+ self.missing_data.append(k)
529+
530+ if self.missing_data:
531+ self.complete = False
532+ log('Missing required data: %s' % ' '.join(self.missing_data), level=INFO)
533+ else:
534+ self.complete = True
535+ return self.complete
536+
537+ def get_related(self):
538+ """Check if any of the context interfaces have relation ids.
539+ Set self.related and return True if one of the interfaces
540+ has relation ids.
541+ """
542+ # Fresh start
543+ self.related = False
544+ try:
545+ for interface in self.interfaces:
546+ if relation_ids(interface):
547+ self.related = True
548+ return self.related
549+ except AttributeError as e:
550+ log("{} {}"
551+ "".format(self, e), 'INFO')
552+ return self.related
553+
554
555 class SharedDBContext(OSContextGenerator):
556 interfaces = ['shared-db']
557@@ -213,6 +254,7 @@
558 self.database = database
559 self.user = user
560 self.ssl_dir = ssl_dir
561+ self.rel_name = self.interfaces[0]
562
563 def __call__(self):
564 self.database = self.database or config('database')
565@@ -246,6 +288,7 @@
566 password_setting = self.relation_prefix + '_password'
567
568 for rid in relation_ids(self.interfaces[0]):
569+ self.related = True
570 for unit in related_units(rid):
571 rdata = relation_get(rid=rid, unit=unit)
572 host = rdata.get('db_host')
573@@ -257,7 +300,7 @@
574 'database_password': rdata.get(password_setting),
575 'database_type': 'mysql'
576 }
577- if context_complete(ctxt):
578+ if self.context_complete(ctxt):
579 db_ssl(rdata, ctxt, self.ssl_dir)
580 return ctxt
581 return {}
582@@ -278,6 +321,7 @@
583
584 ctxt = {}
585 for rid in relation_ids(self.interfaces[0]):
586+ self.related = True
587 for unit in related_units(rid):
588 rel_host = relation_get('host', rid=rid, unit=unit)
589 rel_user = relation_get('user', rid=rid, unit=unit)
590@@ -287,7 +331,7 @@
591 'database_user': rel_user,
592 'database_password': rel_passwd,
593 'database_type': 'postgresql'}
594- if context_complete(ctxt):
595+ if self.context_complete(ctxt):
596 return ctxt
597
598 return {}
599@@ -348,6 +392,7 @@
600 ctxt['signing_dir'] = cachedir
601
602 for rid in relation_ids(self.rel_name):
603+ self.related = True
604 for unit in related_units(rid):
605 rdata = relation_get(rid=rid, unit=unit)
606 serv_host = rdata.get('service_host')
607@@ -366,7 +411,7 @@
608 'service_protocol': svc_protocol,
609 'auth_protocol': auth_protocol})
610
611- if context_complete(ctxt):
612+ if self.context_complete(ctxt):
613 # NOTE(jamespage) this is required for >= icehouse
614 # so a missing value just indicates keystone needs
615 # upgrading
616@@ -405,6 +450,7 @@
617 ctxt = {}
618 for rid in relation_ids(self.rel_name):
619 ha_vip_only = False
620+ self.related = True
621 for unit in related_units(rid):
622 if relation_get('clustered', rid=rid, unit=unit):
623 ctxt['clustered'] = True
624@@ -437,7 +483,7 @@
625 ha_vip_only = relation_get('ha-vip-only',
626 rid=rid, unit=unit) is not None
627
628- if context_complete(ctxt):
629+ if self.context_complete(ctxt):
630 if 'rabbit_ssl_ca' in ctxt:
631 if not self.ssl_dir:
632 log("Charm not setup for ssl support but ssl ca "
633@@ -469,7 +515,7 @@
634 ctxt['oslo_messaging_flags'] = config_flags_parser(
635 oslo_messaging_flags)
636
637- if not context_complete(ctxt):
638+ if not self.complete:
639 return {}
640
641 return ctxt
642@@ -507,7 +553,7 @@
643 if not os.path.isdir('/etc/ceph'):
644 os.mkdir('/etc/ceph')
645
646- if not context_complete(ctxt):
647+ if not self.context_complete(ctxt):
648 return {}
649
650 ensure_packages(['ceph-common'])
651@@ -906,6 +952,19 @@
652 'config': config}
653 return ovs_ctxt
654
655+ def midonet_ctxt(self):
656+ driver = neutron_plugin_attribute(self.plugin, 'driver',
657+ self.network_manager)
658+ midonet_config = neutron_plugin_attribute(self.plugin, 'config',
659+ self.network_manager)
660+ mido_ctxt = {'core_plugin': driver,
661+ 'neutron_plugin': 'midonet',
662+ 'neutron_security_groups': self.neutron_security_groups,
663+ 'local_ip': unit_private_ip(),
664+ 'config': midonet_config}
665+
666+ return mido_ctxt
667+
668 def __call__(self):
669 if self.network_manager not in ['quantum', 'neutron']:
670 return {}
671@@ -927,6 +986,8 @@
672 ctxt.update(self.nuage_ctxt())
673 elif self.plugin == 'plumgrid':
674 ctxt.update(self.pg_ctxt())
675+ elif self.plugin == 'midonet':
676+ ctxt.update(self.midonet_ctxt())
677
678 alchemy_flags = config('neutron-alchemy-flags')
679 if alchemy_flags:
680@@ -1059,7 +1120,7 @@
681
682 ctxt = {
683 ... other context ...
684- 'subordinate_config': {
685+ 'subordinate_configuration': {
686 'DEFAULT': {
687 'key1': 'value1',
688 },
689@@ -1100,22 +1161,23 @@
690 try:
691 sub_config = json.loads(sub_config)
692 except:
693- log('Could not parse JSON from subordinate_config '
694- 'setting from %s' % rid, level=ERROR)
695+ log('Could not parse JSON from '
696+ 'subordinate_configuration setting from %s'
697+ % rid, level=ERROR)
698 continue
699
700 for service in self.services:
701 if service not in sub_config:
702- log('Found subordinate_config on %s but it contained'
703- 'nothing for %s service' % (rid, service),
704- level=INFO)
705+ log('Found subordinate_configuration on %s but it '
706+ 'contained nothing for %s service'
707+ % (rid, service), level=INFO)
708 continue
709
710 sub_config = sub_config[service]
711 if self.config_file not in sub_config:
712- log('Found subordinate_config on %s but it contained'
713- 'nothing for %s' % (rid, self.config_file),
714- level=INFO)
715+ log('Found subordinate_configuration on %s but it '
716+ 'contained nothing for %s'
717+ % (rid, self.config_file), level=INFO)
718 continue
719
720 sub_config = sub_config[self.config_file]
721@@ -1318,7 +1380,7 @@
722 normalized.update({port: port for port in resolved
723 if port in ports})
724 if resolved:
725- return {bridge: normalized[port] for port, bridge in
726+ return {normalized[port]: bridge for port, bridge in
727 six.iteritems(portmap) if port in normalized.keys()}
728
729 return None
730@@ -1329,12 +1391,22 @@
731 def __call__(self):
732 ctxt = {}
733 mappings = super(PhyNICMTUContext, self).__call__()
734- if mappings and mappings.values():
735- ports = mappings.values()
736+ if mappings and mappings.keys():
737+ ports = sorted(mappings.keys())
738 napi_settings = NeutronAPIContext()()
739 mtu = napi_settings.get('network_device_mtu')
740+ all_ports = set()
741+ # If any of ports is a vlan device, its underlying device must have
742+ # mtu applied first.
743+ for port in ports:
744+ for lport in glob.glob("/sys/class/net/%s/lower_*" % port):
745+ lport = os.path.basename(lport)
746+ all_ports.add(lport.split('_')[1])
747+
748+ all_ports = list(all_ports)
749+ all_ports.extend(ports)
750 if mtu:
751- ctxt["devs"] = '\\n'.join(ports)
752+ ctxt["devs"] = '\\n'.join(all_ports)
753 ctxt['mtu'] = mtu
754
755 return ctxt
756@@ -1366,6 +1438,6 @@
757 'auth_protocol':
758 rdata.get('auth_protocol') or 'http',
759 }
760- if context_complete(ctxt):
761+ if self.context_complete(ctxt):
762 return ctxt
763 return {}
764
765=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
766--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-09-03 09:44:48 +0000
767+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-10-08 15:47:11 +0000
768@@ -209,6 +209,20 @@
769 'server_packages': ['neutron-server',
770 'neutron-plugin-plumgrid'],
771 'server_services': ['neutron-server']
772+ },
773+ 'midonet': {
774+ 'config': '/etc/neutron/plugins/midonet/midonet.ini',
775+ 'driver': 'midonet.neutron.plugin.MidonetPluginV2',
776+ 'contexts': [
777+ context.SharedDBContext(user=config('neutron-database-user'),
778+ database=config('neutron-database'),
779+ relation_prefix='neutron',
780+ ssl_dir=NEUTRON_CONF_DIR)],
781+ 'services': [],
782+ 'packages': [[headers_package()] + determine_dkms_package()],
783+ 'server_packages': ['neutron-server',
784+ 'python-neutron-plugin-midonet'],
785+ 'server_services': ['neutron-server']
786 }
787 }
788 if release >= 'icehouse':
789@@ -310,10 +324,10 @@
790 def parse_data_port_mappings(mappings, default_bridge='br-data'):
791 """Parse data port mappings.
792
793- Mappings must be a space-delimited list of port:bridge mappings.
794+ Mappings must be a space-delimited list of bridge:port.
795
796- Returns dict of the form {port:bridge} where port may be an mac address or
797- interface name.
798+ Returns dict of the form {port:bridge} where ports may be mac addresses or
799+ interface names.
800 """
801
802 # NOTE(dosaboy): we use rvalue for key to allow multiple values to be
803
804=== modified file 'hooks/charmhelpers/contrib/openstack/templates/ceph.conf'
805--- hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2015-07-29 10:50:52 +0000
806+++ hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2015-10-08 15:47:11 +0000
807@@ -13,3 +13,9 @@
808 err to syslog = {{ use_syslog }}
809 clog to syslog = {{ use_syslog }}
810
811+[client]
812+{% if rbd_client_cache_settings -%}
813+{% for key, value in rbd_client_cache_settings.iteritems() -%}
814+{{ key }} = {{ value }}
815+{% endfor -%}
816+{%- endif %}
817\ No newline at end of file
818
819=== modified file 'hooks/charmhelpers/contrib/openstack/templating.py'
820--- hooks/charmhelpers/contrib/openstack/templating.py 2015-07-29 10:50:52 +0000
821+++ hooks/charmhelpers/contrib/openstack/templating.py 2015-10-08 15:47:11 +0000
822@@ -18,7 +18,7 @@
823
824 import six
825
826-from charmhelpers.fetch import apt_install
827+from charmhelpers.fetch import apt_install, apt_update
828 from charmhelpers.core.hookenv import (
829 log,
830 ERROR,
831@@ -29,6 +29,7 @@
832 try:
833 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
834 except ImportError:
835+ apt_update(fatal=True)
836 apt_install('python-jinja2', fatal=True)
837 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
838
839@@ -112,7 +113,7 @@
840
841 def complete_contexts(self):
842 '''
843- Return a list of interfaces that have atisfied contexts.
844+ Return a list of interfaces that have satisfied contexts.
845 '''
846 if self._complete_contexts:
847 return self._complete_contexts
848@@ -293,3 +294,30 @@
849 [interfaces.extend(i.complete_contexts())
850 for i in six.itervalues(self.templates)]
851 return interfaces
852+
853+ def get_incomplete_context_data(self, interfaces):
854+ '''
855+ Return dictionary of relation status of interfaces and any missing
856+ required context data. Example:
857+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
858+ 'zeromq-configuration': {'related': False}}
859+ '''
860+ incomplete_context_data = {}
861+
862+ for i in six.itervalues(self.templates):
863+ for context in i.contexts:
864+ for interface in interfaces:
865+ related = False
866+ if interface in context.interfaces:
867+ related = context.get_related()
868+ missing_data = context.missing_data
869+ if missing_data:
870+ incomplete_context_data[interface] = {'missing_data': missing_data}
871+ if related:
872+ if incomplete_context_data.get(interface):
873+ incomplete_context_data[interface].update({'related': True})
874+ else:
875+ incomplete_context_data[interface] = {'related': True}
876+ else:
877+ incomplete_context_data[interface] = {'related': False}
878+ return incomplete_context_data
879
880=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
881--- hooks/charmhelpers/contrib/openstack/utils.py 2015-09-03 09:44:48 +0000
882+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-10-08 15:47:11 +0000
883@@ -25,6 +25,7 @@
884 import re
885
886 import six
887+import traceback
888 import yaml
889
890 from charmhelpers.contrib.network import ip
891@@ -34,12 +35,16 @@
892 )
893
894 from charmhelpers.core.hookenv import (
895+ action_fail,
896+ action_set,
897 config,
898 log as juju_log,
899 charm_dir,
900 INFO,
901 relation_ids,
902- relation_set
903+ relation_set,
904+ status_set,
905+ hook_name
906 )
907
908 from charmhelpers.contrib.storage.linux.lvm import (
909@@ -49,7 +54,8 @@
910 )
911
912 from charmhelpers.contrib.network.ip import (
913- get_ipv6_addr
914+ get_ipv6_addr,
915+ is_ipv6,
916 )
917
918 from charmhelpers.contrib.python.packages import (
919@@ -114,6 +120,7 @@
920 ('2.2.1', 'kilo'),
921 ('2.2.2', 'kilo'),
922 ('2.3.0', 'liberty'),
923+ ('2.4.0', 'liberty'),
924 ])
925
926 # >= Liberty version->codename mapping
927@@ -142,6 +149,9 @@
928 'glance-common': OrderedDict([
929 ('11.0.0', 'liberty'),
930 ]),
931+ 'openstack-dashboard': OrderedDict([
932+ ('8.0.0', 'liberty'),
933+ ]),
934 }
935
936 DEFAULT_LOOPBACK_SIZE = '5G'
937@@ -510,6 +520,12 @@
938 relation_prefix=None):
939 hosts = get_ipv6_addr(dynamic_only=False)
940
941+ if config('vip'):
942+ vips = config('vip').split()
943+ for vip in vips:
944+ if vip and is_ipv6(vip):
945+ hosts.append(vip)
946+
947 kwargs = {'database': database,
948 'username': database_user,
949 'hostname': json.dumps(hosts)}
950@@ -745,3 +761,217 @@
951 return projects[key]
952
953 return None
954+
955+
956+def os_workload_status(configs, required_interfaces, charm_func=None):
957+ """
958+ Decorator to set workload status based on complete contexts
959+ """
960+ def wrap(f):
961+ @wraps(f)
962+ def wrapped_f(*args, **kwargs):
963+ # Run the original function first
964+ f(*args, **kwargs)
965+ # Set workload status now that contexts have been
966+ # acted on
967+ set_os_workload_status(configs, required_interfaces, charm_func)
968+ return wrapped_f
969+ return wrap
970+
971+
972+def set_os_workload_status(configs, required_interfaces, charm_func=None):
973+ """
974+ Set workload status based on complete contexts.
975+ status-set missing or incomplete contexts
976+ and juju-log details of missing required data.
977+ charm_func is a charm specific function to run checking
978+ for charm specific requirements such as a VIP setting.
979+ """
980+ incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
981+ state = 'active'
982+ missing_relations = []
983+ incomplete_relations = []
984+ message = None
985+ charm_state = None
986+ charm_message = None
987+
988+ for generic_interface in incomplete_rel_data.keys():
989+ related_interface = None
990+ missing_data = {}
991+ # Related or not?
992+ for interface in incomplete_rel_data[generic_interface]:
993+ if incomplete_rel_data[generic_interface][interface].get('related'):
994+ related_interface = interface
995+ missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
996+ # No relation ID for the generic_interface
997+ if not related_interface:
998+ juju_log("{} relation is missing and must be related for "
999+ "functionality. ".format(generic_interface), 'WARN')
1000+ state = 'blocked'
1001+ if generic_interface not in missing_relations:
1002+ missing_relations.append(generic_interface)
1003+ else:
1004+ # Relation ID exists but no related unit
1005+ if not missing_data:
1006+ # Edge case relation ID exists but departing
1007+ if ('departed' in hook_name() or 'broken' in hook_name()) \
1008+ and related_interface in hook_name():
1009+ state = 'blocked'
1010+ if generic_interface not in missing_relations:
1011+ missing_relations.append(generic_interface)
1012+ juju_log("{} relation's interface, {}, "
1013+ "relationship is departed or broken "
1014+ "and is required for functionality."
1015+ "".format(generic_interface, related_interface), "WARN")
1016+ # Normal case relation ID exists but no related unit
1017+ # (joining)
1018+ else:
1019+ juju_log("{} relations's interface, {}, is related but has "
1020+ "no units in the relation."
1021+ "".format(generic_interface, related_interface), "INFO")
1022+ # Related unit exists and data missing on the relation
1023+ else:
1024+ juju_log("{} relation's interface, {}, is related awaiting "
1025+ "the following data from the relationship: {}. "
1026+ "".format(generic_interface, related_interface,
1027+ ", ".join(missing_data)), "INFO")
1028+ if state != 'blocked':
1029+ state = 'waiting'
1030+ if generic_interface not in incomplete_relations \
1031+ and generic_interface not in missing_relations:
1032+ incomplete_relations.append(generic_interface)
1033+
1034+ if missing_relations:
1035+ message = "Missing relations: {}".format(", ".join(missing_relations))
1036+ if incomplete_relations:
1037+ message += "; incomplete relations: {}" \
1038+ "".format(", ".join(incomplete_relations))
1039+ state = 'blocked'
1040+ elif incomplete_relations:
1041+ message = "Incomplete relations: {}" \
1042+ "".format(", ".join(incomplete_relations))
1043+ state = 'waiting'
1044+
1045+ # Run charm specific checks
1046+ if charm_func:
1047+ charm_state, charm_message = charm_func(configs)
1048+ if charm_state != 'active' and charm_state != 'unknown':
1049+ state = workload_state_compare(state, charm_state)
1050+ if message:
1051+ message = "{} {}".format(message, charm_message)
1052+ else:
1053+ message = charm_message
1054+
1055+ # Set to active if all requirements have been met
1056+ if state == 'active':
1057+ message = "Unit is ready"
1058+ juju_log(message, "INFO")
1059+
1060+ status_set(state, message)
1061+
1062+
1063+def workload_state_compare(current_workload_state, workload_state):
1064+ """ Return highest priority of two states"""
1065+ hierarchy = {'unknown': -1,
1066+ 'active': 0,
1067+ 'maintenance': 1,
1068+ 'waiting': 2,
1069+ 'blocked': 3,
1070+ }
1071+
1072+ if hierarchy.get(workload_state) is None:
1073+ workload_state = 'unknown'
1074+ if hierarchy.get(current_workload_state) is None:
1075+ current_workload_state = 'unknown'
1076+
1077+ # Set workload_state based on hierarchy of statuses
1078+ if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
1079+ return current_workload_state
1080+ else:
1081+ return workload_state
1082+
1083+
1084+def incomplete_relation_data(configs, required_interfaces):
1085+ """
1086+ Check complete contexts against required_interfaces
1087+ Return dictionary of incomplete relation data.
1088+
1089+ configs is an OSConfigRenderer object with configs registered
1090+
1091+ required_interfaces is a dictionary of required general interfaces
1092+ with dictionary values of possible specific interfaces.
1093+ Example:
1094+ required_interfaces = {'database': ['shared-db', 'pgsql-db']}
1095+
1096+ The interface is said to be satisfied if anyone of the interfaces in the
1097+ list has a complete context.
1098+
1099+ Return dictionary of incomplete or missing required contexts with relation
1100+ status of interfaces and any missing data points. Example:
1101+ {'message':
1102+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
1103+ 'zeromq-configuration': {'related': False}},
1104+ 'identity':
1105+ {'identity-service': {'related': False}},
1106+ 'database':
1107+ {'pgsql-db': {'related': False},
1108+ 'shared-db': {'related': True}}}
1109+ """
1110+ complete_ctxts = configs.complete_contexts()
1111+ incomplete_relations = []
1112+ for svc_type in required_interfaces.keys():
1113+ # Avoid duplicates
1114+ found_ctxt = False
1115+ for interface in required_interfaces[svc_type]:
1116+ if interface in complete_ctxts:
1117+ found_ctxt = True
1118+ if not found_ctxt:
1119+ incomplete_relations.append(svc_type)
1120+ incomplete_context_data = {}
1121+ for i in incomplete_relations:
1122+ incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
1123+ return incomplete_context_data
1124+
1125+
1126+def do_action_openstack_upgrade(package, upgrade_callback, configs):
1127+ """Perform action-managed OpenStack upgrade.
1128+
1129+ Upgrades packages to the configured openstack-origin version and sets
1130+ the corresponding action status as a result.
1131+
1132+ If the charm was installed from source we cannot upgrade it.
1133+ For backwards compatibility a config flag (action-managed-upgrade) must
1134+ be set for this code to run, otherwise a full service level upgrade will
1135+ fire on config-changed.
1136+
1137+ @param package: package name for determining if upgrade available
1138+ @param upgrade_callback: function callback to charm's upgrade function
1139+ @param configs: templating object derived from OSConfigRenderer class
1140+
1141+ @return: True if upgrade successful; False if upgrade failed or skipped
1142+ """
1143+ ret = False
1144+
1145+ if git_install_requested():
1146+ action_set({'outcome': 'installed from source, skipped upgrade.'})
1147+ else:
1148+ if openstack_upgrade_available(package):
1149+ if config('action-managed-upgrade'):
1150+ juju_log('Upgrading OpenStack release')
1151+
1152+ try:
1153+ upgrade_callback(configs=configs)
1154+ action_set({'outcome': 'success, upgrade completed.'})
1155+ ret = True
1156+ except:
1157+ action_set({'outcome': 'upgrade failed, see traceback.'})
1158+ action_set({'traceback': traceback.format_exc()})
1159+ action_fail('do_openstack_upgrade resulted in an '
1160+ 'unexpected error')
1161+ else:
1162+ action_set({'outcome': 'action-managed-upgrade config is '
1163+ 'False, skipped upgrade.'})
1164+ else:
1165+ action_set({'outcome': 'no upgrade available.'})
1166+
1167+ return ret
1168
1169=== modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py'
1170--- hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-09-10 09:30:40 +0000
1171+++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-10-08 15:47:11 +0000
1172@@ -59,6 +59,8 @@
1173 apt_install,
1174 )
1175
1176+from charmhelpers.core.kernel import modprobe
1177+
1178 KEYRING = '/etc/ceph/ceph.client.{}.keyring'
1179 KEYFILE = '/etc/ceph/ceph.client.{}.key'
1180
1181@@ -291,17 +293,6 @@
1182 os.chown(data_src_dst, uid, gid)
1183
1184
1185-# TODO: re-use
1186-def modprobe(module):
1187- """Load a kernel module and configure for auto-load on reboot."""
1188- log('Loading kernel module', level=INFO)
1189- cmd = ['modprobe', module]
1190- check_call(cmd)
1191- with open('/etc/modules', 'r+') as modules:
1192- if module not in modules.read():
1193- modules.write(module)
1194-
1195-
1196 def copy_files(src, dst, symlinks=False, ignore=None):
1197 """Copy files from src to dst."""
1198 for item in os.listdir(src):
1199
1200=== modified file 'hooks/charmhelpers/core/hookenv.py'
1201--- hooks/charmhelpers/core/hookenv.py 2015-09-03 09:44:48 +0000
1202+++ hooks/charmhelpers/core/hookenv.py 2015-10-08 15:47:11 +0000
1203@@ -623,6 +623,38 @@
1204 return unit_get('private-address')
1205
1206
1207+@cached
1208+def storage_get(attribute="", storage_id=""):
1209+ """Get storage attributes"""
1210+ _args = ['storage-get', '--format=json']
1211+ if storage_id:
1212+ _args.extend(('-s', storage_id))
1213+ if attribute:
1214+ _args.append(attribute)
1215+ try:
1216+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1217+ except ValueError:
1218+ return None
1219+
1220+
1221+@cached
1222+def storage_list(storage_name=""):
1223+ """List the storage IDs for the unit"""
1224+ _args = ['storage-list', '--format=json']
1225+ if storage_name:
1226+ _args.append(storage_name)
1227+ try:
1228+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1229+ except ValueError:
1230+ return None
1231+ except OSError as e:
1232+ import errno
1233+ if e.errno == errno.ENOENT:
1234+ # storage-list does not exist
1235+ return []
1236+ raise
1237+
1238+
1239 class UnregisteredHookError(Exception):
1240 """Raised when an undefined hook is called"""
1241 pass
1242
1243=== modified file 'hooks/charmhelpers/core/host.py'
1244--- hooks/charmhelpers/core/host.py 2015-08-19 13:53:01 +0000
1245+++ hooks/charmhelpers/core/host.py 2015-10-08 15:47:11 +0000
1246@@ -63,32 +63,48 @@
1247 return service_result
1248
1249
1250-def service_pause(service_name, init_dir=None):
1251+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
1252 """Pause a system service.
1253
1254 Stop it, and prevent it from starting again at boot."""
1255- if init_dir is None:
1256- init_dir = "/etc/init"
1257 stopped = service_stop(service_name)
1258- # XXX: Support systemd too
1259- override_path = os.path.join(
1260- init_dir, '{}.override'.format(service_name))
1261- with open(override_path, 'w') as fh:
1262- fh.write("manual\n")
1263+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1264+ sysv_file = os.path.join(initd_dir, service_name)
1265+ if os.path.exists(upstart_file):
1266+ override_path = os.path.join(
1267+ init_dir, '{}.override'.format(service_name))
1268+ with open(override_path, 'w') as fh:
1269+ fh.write("manual\n")
1270+ elif os.path.exists(sysv_file):
1271+ subprocess.check_call(["update-rc.d", service_name, "disable"])
1272+ else:
1273+ # XXX: Support SystemD too
1274+ raise ValueError(
1275+ "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
1276+ service_name, upstart_file, sysv_file))
1277 return stopped
1278
1279
1280-def service_resume(service_name, init_dir=None):
1281+def service_resume(service_name, init_dir="/etc/init",
1282+ initd_dir="/etc/init.d"):
1283 """Resume a system service.
1284
1285 Reenable starting again at boot. Start the service"""
1286- # XXX: Support systemd too
1287- if init_dir is None:
1288- init_dir = "/etc/init"
1289- override_path = os.path.join(
1290- init_dir, '{}.override'.format(service_name))
1291- if os.path.exists(override_path):
1292- os.unlink(override_path)
1293+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1294+ sysv_file = os.path.join(initd_dir, service_name)
1295+ if os.path.exists(upstart_file):
1296+ override_path = os.path.join(
1297+ init_dir, '{}.override'.format(service_name))
1298+ if os.path.exists(override_path):
1299+ os.unlink(override_path)
1300+ elif os.path.exists(sysv_file):
1301+ subprocess.check_call(["update-rc.d", service_name, "enable"])
1302+ else:
1303+ # XXX: Support SystemD too
1304+ raise ValueError(
1305+ "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
1306+ service_name, upstart_file, sysv_file))
1307+
1308 started = service_start(service_name)
1309 return started
1310
1311
1312=== modified file 'hooks/charmhelpers/core/hugepage.py'
1313--- hooks/charmhelpers/core/hugepage.py 2015-08-19 13:53:01 +0000
1314+++ hooks/charmhelpers/core/hugepage.py 2015-10-08 15:47:11 +0000
1315@@ -25,11 +25,13 @@
1316 fstab_mount,
1317 mkdir,
1318 )
1319+from charmhelpers.core.strutils import bytes_from_string
1320+from subprocess import check_output
1321
1322
1323 def hugepage_support(user, group='hugetlb', nr_hugepages=256,
1324 max_map_count=65536, mnt_point='/run/hugepages/kvm',
1325- pagesize='2MB', mount=True):
1326+ pagesize='2MB', mount=True, set_shmmax=False):
1327 """Enable hugepages on system.
1328
1329 Args:
1330@@ -49,6 +51,11 @@
1331 'vm.max_map_count': max_map_count,
1332 'vm.hugetlb_shm_group': gid,
1333 }
1334+ if set_shmmax:
1335+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
1336+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
1337+ if shmmax_minsize > shmmax_current:
1338+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
1339 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
1340 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
1341 lfstab = fstab.Fstab()
1342
1343=== added file 'hooks/charmhelpers/core/kernel.py'
1344--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
1345+++ hooks/charmhelpers/core/kernel.py 2015-10-08 15:47:11 +0000
1346@@ -0,0 +1,68 @@
1347+#!/usr/bin/env python
1348+# -*- coding: utf-8 -*-
1349+
1350+# Copyright 2014-2015 Canonical Limited.
1351+#
1352+# This file is part of charm-helpers.
1353+#
1354+# charm-helpers is free software: you can redistribute it and/or modify
1355+# it under the terms of the GNU Lesser General Public License version 3 as
1356+# published by the Free Software Foundation.
1357+#
1358+# charm-helpers is distributed in the hope that it will be useful,
1359+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1360+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1361+# GNU Lesser General Public License for more details.
1362+#
1363+# You should have received a copy of the GNU Lesser General Public License
1364+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1365+
1366+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1367+
1368+from charmhelpers.core.hookenv import (
1369+ log,
1370+ INFO
1371+)
1372+
1373+from subprocess import check_call, check_output
1374+import re
1375+
1376+
1377+def modprobe(module, persist=True):
1378+ """Load a kernel module and configure for auto-load on reboot."""
1379+ cmd = ['modprobe', module]
1380+
1381+ log('Loading kernel module %s' % module, level=INFO)
1382+
1383+ check_call(cmd)
1384+ if persist:
1385+ with open('/etc/modules', 'r+') as modules:
1386+ if module not in modules.read():
1387+ modules.write(module)
1388+
1389+
1390+def rmmod(module, force=False):
1391+ """Remove a module from the linux kernel"""
1392+ cmd = ['rmmod']
1393+ if force:
1394+ cmd.append('-f')
1395+ cmd.append(module)
1396+ log('Removing kernel module %s' % module, level=INFO)
1397+ return check_call(cmd)
1398+
1399+
1400+def lsmod():
1401+ """Shows what kernel modules are currently loaded"""
1402+ return check_output(['lsmod'],
1403+ universal_newlines=True)
1404+
1405+
1406+def is_module_loaded(module):
1407+ """Checks if a kernel module is already loaded"""
1408+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
1409+ return len(matches) > 0
1410+
1411+
1412+def update_initramfs(version='all'):
1413+ """Updates an initramfs image"""
1414+ return check_call(["update-initramfs", "-k", version, "-u"])
1415
1416=== modified file 'hooks/charmhelpers/core/strutils.py'
1417--- hooks/charmhelpers/core/strutils.py 2015-04-16 10:29:35 +0000
1418+++ hooks/charmhelpers/core/strutils.py 2015-10-08 15:47:11 +0000
1419@@ -18,6 +18,7 @@
1420 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1421
1422 import six
1423+import re
1424
1425
1426 def bool_from_string(value):
1427@@ -40,3 +41,32 @@
1428
1429 msg = "Unable to interpret string value '%s' as boolean" % (value)
1430 raise ValueError(msg)
1431+
1432+
1433+def bytes_from_string(value):
1434+ """Interpret human readable string value as bytes.
1435+
1436+ Returns int
1437+ """
1438+ BYTE_POWER = {
1439+ 'K': 1,
1440+ 'KB': 1,
1441+ 'M': 2,
1442+ 'MB': 2,
1443+ 'G': 3,
1444+ 'GB': 3,
1445+ 'T': 4,
1446+ 'TB': 4,
1447+ 'P': 5,
1448+ 'PB': 5,
1449+ }
1450+ if isinstance(value, six.string_types):
1451+ value = six.text_type(value)
1452+ else:
1453+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
1454+ raise ValueError(msg)
1455+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
1456+ if not matches:
1457+ msg = "Unable to interpret string value '%s' as bytes" % (value)
1458+ raise ValueError(msg)
1459+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
1460
1461=== modified file 'hooks/cinder_hooks.py'
1462--- hooks/cinder_hooks.py 2015-09-16 01:07:56 +0000
1463+++ hooks/cinder_hooks.py 2015-10-08 15:47:11 +0000
1464@@ -8,7 +8,9 @@
1465 register_configs,
1466 restart_map,
1467 set_ceph_env_variables,
1468- PACKAGES
1469+ PACKAGES,
1470+ REQUIRED_INTERFACES,
1471+ check_optional_relations,
1472 )
1473 from cinder_contexts import CephSubordinateContext
1474
1475@@ -19,6 +21,7 @@
1476 service_name,
1477 relation_set,
1478 relation_ids,
1479+ status_set,
1480 log,
1481 )
1482 from charmhelpers.fetch import apt_install, apt_update
1483@@ -34,6 +37,8 @@
1484 delete_keyring,
1485 )
1486 from charmhelpers.payload.execd import execd_preinstall
1487+from charmhelpers.contrib.openstack.utils import set_os_workload_status
1488+
1489
1490 hooks = Hooks()
1491
1492@@ -42,7 +47,9 @@
1493
1494 @hooks.hook('install')
1495 def install():
1496+ status_set('maintenance', 'Executing pre-install')
1497 execd_preinstall()
1498+ status_set('maintenance', 'Installing apt packages')
1499 apt_update(fatal=True)
1500 apt_install(PACKAGES, fatal=True)
1501
1502@@ -136,3 +143,5 @@
1503 hooks.execute(sys.argv)
1504 except UnregisteredHookError as e:
1505 log('Unknown hook {} - skipping.'.format(e))
1506+ set_os_workload_status(CONFIGS, REQUIRED_INTERFACES,
1507+ charm_func=check_optional_relations)
1508
1509=== modified file 'hooks/cinder_utils.py'
1510--- hooks/cinder_utils.py 2014-11-20 16:29:40 +0000
1511+++ hooks/cinder_utils.py 2015-10-08 15:47:11 +0000
1512@@ -4,6 +4,7 @@
1513 from charmhelpers.core.hookenv import (
1514 relation_ids,
1515 service_name,
1516+ status_get,
1517 )
1518
1519 from charmhelpers.contrib.openstack import (
1520@@ -13,6 +14,7 @@
1521
1522 from charmhelpers.contrib.openstack.utils import (
1523 get_os_codename_package,
1524+ set_os_workload_status,
1525 )
1526
1527 from charmhelpers.contrib.openstack.alternatives import install_alternative
1528@@ -23,6 +25,10 @@
1529 'ceph-common',
1530 ]
1531
1532+REQUIRED_INTERFACES = {
1533+ 'ceph': ['ceph'],
1534+}
1535+
1536 CHARM_CEPH_CONF = '/var/lib/charm/{}/ceph.conf'
1537 CEPH_CONF = '/etc/ceph/ceph.conf'
1538
1539@@ -104,3 +110,13 @@
1540 out.write('CEPH_ARGS="--id %s"\n' % service)
1541 with open('/etc/init/cinder-volume.override', 'w') as out:
1542 out.write('env CEPH_ARGS="--id %s"\n' % service)
1543+
1544+
1545+def check_optional_relations(configs):
1546+ # No optional relations at this time
1547+ required_interfaces = {}
1548+ if required_interfaces:
1549+ set_os_workload_status(configs, required_interfaces)
1550+ return status_get()
1551+ else:
1552+ return 'unknown', 'No optional relations'
1553
1554=== modified file 'tests/charmhelpers/contrib/amulet/deployment.py'
1555--- tests/charmhelpers/contrib/amulet/deployment.py 2015-06-25 16:06:45 +0000
1556+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-10-08 15:47:11 +0000
1557@@ -51,7 +51,8 @@
1558 if 'units' not in this_service:
1559 this_service['units'] = 1
1560
1561- self.d.add(this_service['name'], units=this_service['units'])
1562+ self.d.add(this_service['name'], units=this_service['units'],
1563+ constraints=this_service.get('constraints'))
1564
1565 for svc in other_services:
1566 if 'location' in svc:
1567@@ -64,7 +65,8 @@
1568 if 'units' not in svc:
1569 svc['units'] = 1
1570
1571- self.d.add(svc['name'], charm=branch_location, units=svc['units'])
1572+ self.d.add(svc['name'], charm=branch_location, units=svc['units'],
1573+ constraints=svc.get('constraints'))
1574
1575 def _add_relations(self, relations):
1576 """Add all of the relations for the services."""
1577
1578=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
1579--- tests/charmhelpers/contrib/amulet/utils.py 2015-09-10 09:30:40 +0000
1580+++ tests/charmhelpers/contrib/amulet/utils.py 2015-10-08 15:47:11 +0000
1581@@ -326,7 +326,7 @@
1582
1583 def service_restarted_since(self, sentry_unit, mtime, service,
1584 pgrep_full=None, sleep_time=20,
1585- retry_count=2, retry_sleep_time=30):
1586+ retry_count=30, retry_sleep_time=10):
1587 """Check if service was been started after a given time.
1588
1589 Args:
1590@@ -334,8 +334,9 @@
1591 mtime (float): The epoch time to check against
1592 service (string): service name to look for in process table
1593 pgrep_full: [Deprecated] Use full command line search mode with pgrep
1594- sleep_time (int): Seconds to sleep before looking for process
1595- retry_count (int): If service is not found, how many times to retry
1596+ sleep_time (int): Initial sleep time (s) before looking for file
1597+ retry_sleep_time (int): Time (s) to sleep between retries
1598+ retry_count (int): If file is not found, how many times to retry
1599
1600 Returns:
1601 bool: True if service found and its start time it newer than mtime,
1602@@ -359,11 +360,12 @@
1603 pgrep_full)
1604 self.log.debug('Attempt {} to get {} proc start time on {} '
1605 'OK'.format(tries, service, unit_name))
1606- except IOError:
1607+ except IOError as e:
1608 # NOTE(beisner) - race avoidance, proc may not exist yet.
1609 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1610 self.log.debug('Attempt {} to get {} proc start time on {} '
1611- 'failed'.format(tries, service, unit_name))
1612+ 'failed\n{}'.format(tries, service,
1613+ unit_name, e))
1614 time.sleep(retry_sleep_time)
1615 tries += 1
1616
1617@@ -383,35 +385,62 @@
1618 return False
1619
1620 def config_updated_since(self, sentry_unit, filename, mtime,
1621- sleep_time=20):
1622+ sleep_time=20, retry_count=30,
1623+ retry_sleep_time=10):
1624 """Check if file was modified after a given time.
1625
1626 Args:
1627 sentry_unit (sentry): The sentry unit to check the file mtime on
1628 filename (string): The file to check mtime of
1629 mtime (float): The epoch time to check against
1630- sleep_time (int): Seconds to sleep before looking for process
1631+ sleep_time (int): Initial sleep time (s) before looking for file
1632+ retry_sleep_time (int): Time (s) to sleep between retries
1633+ retry_count (int): If file is not found, how many times to retry
1634
1635 Returns:
1636 bool: True if file was modified more recently than mtime, False if
1637- file was modified before mtime,
1638+ file was modified before mtime, or if file not found.
1639 """
1640- self.log.debug('Checking %s updated since %s' % (filename, mtime))
1641+ unit_name = sentry_unit.info['unit_name']
1642+ self.log.debug('Checking that %s updated since %s on '
1643+ '%s' % (filename, mtime, unit_name))
1644 time.sleep(sleep_time)
1645- file_mtime = self._get_file_mtime(sentry_unit, filename)
1646+ file_mtime = None
1647+ tries = 0
1648+ while tries <= retry_count and not file_mtime:
1649+ try:
1650+ file_mtime = self._get_file_mtime(sentry_unit, filename)
1651+ self.log.debug('Attempt {} to get {} file mtime on {} '
1652+ 'OK'.format(tries, filename, unit_name))
1653+ except IOError as e:
1654+ # NOTE(beisner) - race avoidance, file may not exist yet.
1655+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1656+ self.log.debug('Attempt {} to get {} file mtime on {} '
1657+ 'failed\n{}'.format(tries, filename,
1658+ unit_name, e))
1659+ time.sleep(retry_sleep_time)
1660+ tries += 1
1661+
1662+ if not file_mtime:
1663+ self.log.warn('Could not determine file mtime, assuming '
1664+ 'file does not exist')
1665+ return False
1666+
1667 if file_mtime >= mtime:
1668 self.log.debug('File mtime is newer than provided mtime '
1669- '(%s >= %s)' % (file_mtime, mtime))
1670+ '(%s >= %s) on %s (OK)' % (file_mtime,
1671+ mtime, unit_name))
1672 return True
1673 else:
1674- self.log.warn('File mtime %s is older than provided mtime %s'
1675- % (file_mtime, mtime))
1676+ self.log.warn('File mtime is older than provided mtime'
1677+ '(%s < on %s) on %s' % (file_mtime,
1678+ mtime, unit_name))
1679 return False
1680
1681 def validate_service_config_changed(self, sentry_unit, mtime, service,
1682 filename, pgrep_full=None,
1683- sleep_time=20, retry_count=2,
1684- retry_sleep_time=30):
1685+ sleep_time=20, retry_count=30,
1686+ retry_sleep_time=10):
1687 """Check service and file were updated after mtime
1688
1689 Args:
1690@@ -456,7 +485,9 @@
1691 sentry_unit,
1692 filename,
1693 mtime,
1694- sleep_time=0)
1695+ sleep_time=sleep_time,
1696+ retry_count=retry_count,
1697+ retry_sleep_time=retry_sleep_time)
1698
1699 return service_restart and config_update
1700
1701@@ -776,3 +807,12 @@
1702 output = _check_output(command, universal_newlines=True)
1703 data = json.loads(output)
1704 return data.get(u"status") == "completed"
1705+
1706+ def status_get(self, unit):
1707+ """Return the current service status of this unit."""
1708+ raw_status, return_code = unit.run(
1709+ "status-get --format=json --include-data")
1710+ if return_code != 0:
1711+ return ("unknown", "")
1712+ status = json.loads(raw_status)
1713+ return (status["status"], status["message"])
1714
1715=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
1716--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-10 09:30:40 +0000
1717+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-10-08 15:47:11 +0000
1718@@ -58,19 +58,17 @@
1719 else:
1720 base_series = self.current_next
1721
1722- if self.stable:
1723- for svc in other_services:
1724- if svc['name'] in force_series_current:
1725- base_series = self.current_next
1726-
1727+ for svc in other_services:
1728+ if svc['name'] in force_series_current:
1729+ base_series = self.current_next
1730+ # If a location has been explicitly set, use it
1731+ if svc.get('location'):
1732+ continue
1733+ if self.stable:
1734 temp = 'lp:charms/{}/{}'
1735 svc['location'] = temp.format(base_series,
1736 svc['name'])
1737- else:
1738- for svc in other_services:
1739- if svc['name'] in force_series_current:
1740- base_series = self.current_next
1741-
1742+ else:
1743 if svc['name'] in base_charms:
1744 temp = 'lp:charms/{}/{}'
1745 svc['location'] = temp.format(base_series,
1746@@ -79,6 +77,7 @@
1747 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
1748 svc['location'] = temp.format(self.current_next,
1749 svc['name'])
1750+
1751 return other_services
1752
1753 def _add_services(self, this_service, other_services):
1754
1755=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
1756--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-10 09:30:40 +0000
1757+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-10-08 15:47:11 +0000
1758@@ -752,7 +752,7 @@
1759 self.log.debug('SSL is enabled @{}:{} '
1760 '({})'.format(host, port, unit_name))
1761 return True
1762- elif not port and not conf_ssl:
1763+ elif not conf_ssl:
1764 self.log.debug('SSL not enabled @{}:{} '
1765 '({})'.format(host, port, unit_name))
1766 return False
1767
1768=== modified file 'unit_tests/test_cinder_hooks.py'
1769--- unit_tests/test_cinder_hooks.py 2015-09-16 01:07:56 +0000
1770+++ unit_tests/test_cinder_hooks.py 2015-10-08 15:47:11 +0000
1771@@ -34,7 +34,8 @@
1772 # charmhelpers.contrib.hahelpers.cluster_utils
1773 'execd_preinstall',
1774 'CephSubordinateContext',
1775- 'delete_keyring'
1776+ 'delete_keyring',
1777+ 'status_set'
1778 ]
1779
1780

Subscribers

People subscribed via source and target branches