Merge lp:~chris.macnaughton/charms/trusty/ceph-osd/trunk into lp:~openstack-charmers-archive/charms/trusty/ceph-osd/next

Proposed by Chris MacNaughton
Status: Needs review
Proposed branch: lp:~chris.macnaughton/charms/trusty/ceph-osd/trunk
Merge into: lp:~openstack-charmers-archive/charms/trusty/ceph-osd/next
Diff against target: 1778 lines (+1103/-119)
18 files modified
config.yaml (+2/-1)
hooks/ceph_hooks.py (+19/-3)
hooks/charmhelpers/cli/__init__.py (+3/-3)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+44/-8)
hooks/charmhelpers/contrib/network/ip.py (+5/-3)
hooks/charmhelpers/core/hookenv.py (+46/-0)
hooks/charmhelpers/core/host.py (+60/-17)
hooks/charmhelpers/core/hugepage.py (+10/-1)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/helpers.py (+5/-2)
hooks/charmhelpers/core/strutils.py (+30/-0)
hooks/charmhelpers/core/templating.py (+13/-6)
hooks/charmhelpers/fetch/__init__.py (+1/-1)
metadata.yaml (+5/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+4/-2)
tests/charmhelpers/contrib/amulet/utils.py (+284/-62)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+123/-10)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+381/-0)
To merge this branch: bzr merge lp:~chris.macnaughton/charms/trusty/ceph-osd/trunk
Reviewer Review Type Date Requested Status
James Page Needs Fixing
Review via email: mp+277135@code.launchpad.net

Description of the change

Add storage hooks support

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #12559 ceph-osd-next for chris.macnaughton mp277135
    UNIT OK: passed

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

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

charm_lint_check #13425 ceph-osd-next for chris.macnaughton mp277135
    LINT FAIL: lint-test failed
    LINT FAIL: charm-proof failed

LINT Results (max last 2 lines):
make: *** [lint] Error 200
ERROR:root:Make target returned non-zero.

Full lint test output: http://paste.ubuntu.com/13216177/
Build: http://10.245.162.77:8080/job/charm_lint_check/13425/

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

charm_amulet_test #7833 ceph-osd-next for chris.macnaughton mp277135
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 124
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/13216928/
Build: http://10.245.162.77:8080/job/charm_amulet_test/7833/

56. By Chris MacNaughton

add-storage-hooks

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

charm_unit_test #13062 ceph-osd-next for chris.macnaughton mp277135
    UNIT OK: passed

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

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

charm_lint_check #14013 ceph-osd-next for chris.macnaughton mp277135
    LINT FAIL: lint-test failed
    LINT FAIL: charm-proof failed

LINT Results (max last 2 lines):
make: *** [lint] Error 200
ERROR:root:Make target returned non-zero.

Full lint test output: http://paste.ubuntu.com/13334730/
Build: http://10.245.162.77:8080/job/charm_lint_check/14013/

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

charm_amulet_test #7940 ceph-osd-next for chris.macnaughton mp277135
    AMULET OK: passed

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

Revision history for this message
Ryan Beisner (1chb1n) wrote :

FYI, the lint fail is https://github.com/juju/charm-tools/issues/41 (charm proof isn't yet storage-aware). Fix-committed upstream, package not yet fix-released.

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

Hi Chris

Please can you update this proposal to add support for the journal device as well - I've just landed that into the ceph charm.

Cheers

James

review: Needs Fixing
Revision history for this message
Chris MacNaughton (chris.macnaughton) wrote :
Revision history for this message
James Page (james-page) wrote :

Chris - no not that one - specifically the changes that landed into ceph:

storage:
  osd-devices:
    type: block
    multiple:
      range: 0-
  osd-journal:
    type: block
    multiple:
      range: 0-1

Unmerged revisions

56. By Chris MacNaughton

add-storage-hooks

55. By Chris MacNaughton

add storage hooks

54. By Chris MacNaughton

update charmhelpers

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'config.yaml'
--- config.yaml 2015-07-10 14:12:01 +0000
+++ config.yaml 2015-11-18 20:55:22 +0000
@@ -6,7 +6,8 @@
6 The devices to format and set up as osd volumes.6 The devices to format and set up as osd volumes.
77
8 These devices are the range of devices that will be checked for and8 These devices are the range of devices that will be checked for and
9 used across all service units.9 used across all service units, in addition to any volumes attached
10 via the --storage flag during deployment
1011
11 For ceph >= 0.56.6 these can also be directories instead of devices - the12 For ceph >= 0.56.6 these can also be directories instead of devices - the
12 charm assumes anything not starting with /dev is a directory instead.13 charm assumes anything not starting with /dev is a directory instead.
1314
=== modified file 'hooks/ceph_hooks.py'
--- hooks/ceph_hooks.py 2015-10-30 02:22:54 +0000
+++ hooks/ceph_hooks.py 2015-11-18 20:55:22 +0000
@@ -24,6 +24,8 @@
24 UnregisteredHookError,24 UnregisteredHookError,
25 service_name,25 service_name,
26 status_set,26 status_set,
27 storage_get,
28 storage_list
27)29)
28from charmhelpers.core.host import (30from charmhelpers.core.host import (
29 umount,31 umount,
@@ -128,7 +130,14 @@
128 ceph.zap_disk(osd_journal)130 ceph.zap_disk(osd_journal)
129 with open(JOURNAL_ZAPPED, 'w') as zapped:131 with open(JOURNAL_ZAPPED, 'w') as zapped:
130 zapped.write('DONE')132 zapped.write('DONE')
131133 storage_changed()
134
135
136@hooks.hook('osd-devices-storage-attached',
137 'osd-fs-storage-attached',
138 'osd-devices-storage-detaching',
139 'osd-fs-storage-detaching')
140def storage_changed():
132 if ceph.is_bootstrapped():141 if ceph.is_bootstrapped():
133 log('ceph bootstrapped, rescanning disks')142 log('ceph bootstrapped, rescanning disks')
134 emit_cephconf()143 emit_cephconf()
@@ -180,9 +189,16 @@
180189
181def get_devices():190def get_devices():
182 if config('osd-devices'):191 if config('osd-devices'):
183 return config('osd-devices').split(' ')192 devices = config('osd-devices').split(' ')
184 else:193 else:
185 return []194 devices = []
195 # List storage instances for the 'osd-devices'
196 # storegdeclared for this charm too, and add
197 # their block device paths to the list.
198 storage_ids = storage_list('osd-devices')
199 storage_ids.extend(storage_list('osd-fs'))
200 devices.extend((storage_get('location', s) for s in storage_ids))
201 return devices
186202
187203
188@hooks.hook('mon-relation-changed',204@hooks.hook('mon-relation-changed',
189205
=== modified file 'hooks/charmhelpers/cli/__init__.py'
--- hooks/charmhelpers/cli/__init__.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/cli/__init__.py 2015-11-18 20:55:22 +0000
@@ -20,7 +20,7 @@
2020
21from six.moves import zip21from six.moves import zip
2222
23from charmhelpers.core import unitdata23import charmhelpers.core.unitdata
2424
2525
26class OutputFormatter(object):26class OutputFormatter(object):
@@ -163,8 +163,8 @@
163 if getattr(arguments.func, '_cli_no_output', False):163 if getattr(arguments.func, '_cli_no_output', False):
164 output = ''164 output = ''
165 self.formatter.format_output(output, arguments.format)165 self.formatter.format_output(output, arguments.format)
166 if unitdata._KV:166 if charmhelpers.core.unitdata._KV:
167 unitdata._KV.flush()167 charmhelpers.core.unitdata._KV.flush()
168168
169169
170cmdline = CommandLine()170cmdline = CommandLine()
171171
=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-06-17 16:59:15 +0000
+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-11-18 20:55:22 +0000
@@ -148,6 +148,13 @@
148 self.description = description148 self.description = description
149 self.check_cmd = self._locate_cmd(check_cmd)149 self.check_cmd = self._locate_cmd(check_cmd)
150150
151 def _get_check_filename(self):
152 return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
153
154 def _get_service_filename(self, hostname):
155 return os.path.join(NRPE.nagios_exportdir,
156 'service__{}_{}.cfg'.format(hostname, self.command))
157
151 def _locate_cmd(self, check_cmd):158 def _locate_cmd(self, check_cmd):
152 search_path = (159 search_path = (
153 '/usr/lib/nagios/plugins',160 '/usr/lib/nagios/plugins',
@@ -163,9 +170,21 @@
163 log('Check command not found: {}'.format(parts[0]))170 log('Check command not found: {}'.format(parts[0]))
164 return ''171 return ''
165172
173 def _remove_service_files(self):
174 if not os.path.exists(NRPE.nagios_exportdir):
175 return
176 for f in os.listdir(NRPE.nagios_exportdir):
177 if f.endswith('_{}.cfg'.format(self.command)):
178 os.remove(os.path.join(NRPE.nagios_exportdir, f))
179
180 def remove(self, hostname):
181 nrpe_check_file = self._get_check_filename()
182 if os.path.exists(nrpe_check_file):
183 os.remove(nrpe_check_file)
184 self._remove_service_files()
185
166 def write(self, nagios_context, hostname, nagios_servicegroups):186 def write(self, nagios_context, hostname, nagios_servicegroups):
167 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(187 nrpe_check_file = self._get_check_filename()
168 self.command)
169 with open(nrpe_check_file, 'w') as nrpe_check_config:188 with open(nrpe_check_file, 'w') as nrpe_check_config:
170 nrpe_check_config.write("# check {}\n".format(self.shortname))189 nrpe_check_config.write("# check {}\n".format(self.shortname))
171 nrpe_check_config.write("command[{}]={}\n".format(190 nrpe_check_config.write("command[{}]={}\n".format(
@@ -180,9 +199,7 @@
180199
181 def write_service_config(self, nagios_context, hostname,200 def write_service_config(self, nagios_context, hostname,
182 nagios_servicegroups):201 nagios_servicegroups):
183 for f in os.listdir(NRPE.nagios_exportdir):202 self._remove_service_files()
184 if re.search('.*{}.cfg'.format(self.command), f):
185 os.remove(os.path.join(NRPE.nagios_exportdir, f))
186203
187 templ_vars = {204 templ_vars = {
188 'nagios_hostname': hostname,205 'nagios_hostname': hostname,
@@ -192,8 +209,7 @@
192 'command': self.command,209 'command': self.command,
193 }210 }
194 nrpe_service_text = Check.service_template.format(**templ_vars)211 nrpe_service_text = Check.service_template.format(**templ_vars)
195 nrpe_service_file = '{}/service__{}_{}.cfg'.format(212 nrpe_service_file = self._get_service_filename(hostname)
196 NRPE.nagios_exportdir, hostname, self.command)
197 with open(nrpe_service_file, 'w') as nrpe_service_config:213 with open(nrpe_service_file, 'w') as nrpe_service_config:
198 nrpe_service_config.write(str(nrpe_service_text))214 nrpe_service_config.write(str(nrpe_service_text))
199215
@@ -218,12 +234,32 @@
218 if hostname:234 if hostname:
219 self.hostname = hostname235 self.hostname = hostname
220 else:236 else:
221 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)237 nagios_hostname = get_nagios_hostname()
238 if nagios_hostname:
239 self.hostname = nagios_hostname
240 else:
241 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
222 self.checks = []242 self.checks = []
223243
224 def add_check(self, *args, **kwargs):244 def add_check(self, *args, **kwargs):
225 self.checks.append(Check(*args, **kwargs))245 self.checks.append(Check(*args, **kwargs))
226246
247 def remove_check(self, *args, **kwargs):
248 if kwargs.get('shortname') is None:
249 raise ValueError('shortname of check must be specified')
250
251 # Use sensible defaults if they're not specified - these are not
252 # actually used during removal, but they're required for constructing
253 # the Check object; check_disk is chosen because it's part of the
254 # nagios-plugins-basic package.
255 if kwargs.get('check_cmd') is None:
256 kwargs['check_cmd'] = 'check_disk'
257 if kwargs.get('description') is None:
258 kwargs['description'] = ''
259
260 check = Check(*args, **kwargs)
261 check.remove(self.hostname)
262
227 def write(self):263 def write(self):
228 try:264 try:
229 nagios_uid = pwd.getpwnam('nagios').pw_uid265 nagios_uid = pwd.getpwnam('nagios').pw_uid
230266
=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 2015-09-03 09:42:18 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2015-11-18 20:55:22 +0000
@@ -23,7 +23,7 @@
23from functools import partial23from functools import partial
2424
25from charmhelpers.core.hookenv import unit_get25from charmhelpers.core.hookenv import unit_get
26from charmhelpers.fetch import apt_install26from charmhelpers.fetch import apt_install, apt_update
27from charmhelpers.core.hookenv import (27from charmhelpers.core.hookenv import (
28 log,28 log,
29 WARNING,29 WARNING,
@@ -32,13 +32,15 @@
32try:32try:
33 import netifaces33 import netifaces
34except ImportError:34except ImportError:
35 apt_install('python-netifaces')35 apt_update(fatal=True)
36 apt_install('python-netifaces', fatal=True)
36 import netifaces37 import netifaces
3738
38try:39try:
39 import netaddr40 import netaddr
40except ImportError:41except ImportError:
41 apt_install('python-netaddr')42 apt_update(fatal=True)
43 apt_install('python-netaddr', fatal=True)
42 import netaddr44 import netaddr
4345
4446
4547
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2015-09-03 09:42:18 +0000
+++ hooks/charmhelpers/core/hookenv.py 2015-11-18 20:55:22 +0000
@@ -491,6 +491,19 @@
491491
492492
493@cached493@cached
494def peer_relation_id():
495 '''Get a peer relation id if a peer relation has been joined, else None.'''
496 md = metadata()
497 section = md.get('peers')
498 if section:
499 for key in section:
500 relids = relation_ids(key)
501 if relids:
502 return relids[0]
503 return None
504
505
506@cached
494def relation_to_interface(relation_name):507def relation_to_interface(relation_name):
495 """508 """
496 Given the name of a relation, return the interface that relation uses.509 Given the name of a relation, return the interface that relation uses.
@@ -623,6 +636,38 @@
623 return unit_get('private-address')636 return unit_get('private-address')
624637
625638
639@cached
640def storage_get(attribute="", storage_id=""):
641 """Get storage attributes"""
642 _args = ['storage-get', '--format=json']
643 if storage_id:
644 _args.extend(('-s', storage_id))
645 if attribute:
646 _args.append(attribute)
647 try:
648 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
649 except ValueError:
650 return None
651
652
653@cached
654def storage_list(storage_name=""):
655 """List the storage IDs for the unit"""
656 _args = ['storage-list', '--format=json']
657 if storage_name:
658 _args.append(storage_name)
659 try:
660 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
661 except ValueError:
662 return None
663 except OSError as e:
664 import errno
665 if e.errno == errno.ENOENT:
666 # storage-list does not exist
667 return []
668 raise
669
670
626class UnregisteredHookError(Exception):671class UnregisteredHookError(Exception):
627 """Raised when an undefined hook is called"""672 """Raised when an undefined hook is called"""
628 pass673 pass
@@ -788,6 +833,7 @@
788833
789def translate_exc(from_exc, to_exc):834def translate_exc(from_exc, to_exc):
790 def inner_translate_exc1(f):835 def inner_translate_exc1(f):
836 @wraps(f)
791 def inner_translate_exc2(*args, **kwargs):837 def inner_translate_exc2(*args, **kwargs):
792 try:838 try:
793 return f(*args, **kwargs)839 return f(*args, **kwargs)
794840
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/core/host.py 2015-11-18 20:55:22 +0000
@@ -63,32 +63,48 @@
63 return service_result63 return service_result
6464
6565
66def service_pause(service_name, init_dir=None):66def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
67 """Pause a system service.67 """Pause a system service.
6868
69 Stop it, and prevent it from starting again at boot."""69 Stop it, and prevent it from starting again at boot."""
70 if init_dir is None:
71 init_dir = "/etc/init"
72 stopped = service_stop(service_name)70 stopped = service_stop(service_name)
73 # XXX: Support systemd too71 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
74 override_path = os.path.join(72 sysv_file = os.path.join(initd_dir, service_name)
75 init_dir, '{}.override'.format(service_name))73 if os.path.exists(upstart_file):
76 with open(override_path, 'w') as fh:74 override_path = os.path.join(
77 fh.write("manual\n")75 init_dir, '{}.override'.format(service_name))
76 with open(override_path, 'w') as fh:
77 fh.write("manual\n")
78 elif os.path.exists(sysv_file):
79 subprocess.check_call(["update-rc.d", service_name, "disable"])
80 else:
81 # XXX: Support SystemD too
82 raise ValueError(
83 "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
84 service_name, upstart_file, sysv_file))
78 return stopped85 return stopped
7986
8087
81def service_resume(service_name, init_dir=None):88def service_resume(service_name, init_dir="/etc/init",
89 initd_dir="/etc/init.d"):
82 """Resume a system service.90 """Resume a system service.
8391
84 Reenable starting again at boot. Start the service"""92 Reenable starting again at boot. Start the service"""
85 # XXX: Support systemd too93 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
86 if init_dir is None:94 sysv_file = os.path.join(initd_dir, service_name)
87 init_dir = "/etc/init"95 if os.path.exists(upstart_file):
88 override_path = os.path.join(96 override_path = os.path.join(
89 init_dir, '{}.override'.format(service_name))97 init_dir, '{}.override'.format(service_name))
90 if os.path.exists(override_path):98 if os.path.exists(override_path):
91 os.unlink(override_path)99 os.unlink(override_path)
100 elif os.path.exists(sysv_file):
101 subprocess.check_call(["update-rc.d", service_name, "enable"])
102 else:
103 # XXX: Support SystemD too
104 raise ValueError(
105 "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
106 service_name, upstart_file, sysv_file))
107
92 started = service_start(service_name)108 started = service_start(service_name)
93 return started109 return started
94110
@@ -550,7 +566,14 @@
550 os.chdir(cur)566 os.chdir(cur)
551567
552568
553def chownr(path, owner, group, follow_links=True):569def chownr(path, owner, group, follow_links=True, chowntopdir=False):
570 """
571 Recursively change user and group ownership of files and directories
572 in given path. Doesn't chown path itself by default, only its children.
573
574 :param bool follow_links: Also Chown links if True
575 :param bool chowntopdir: Also chown path itself if True
576 """
554 uid = pwd.getpwnam(owner).pw_uid577 uid = pwd.getpwnam(owner).pw_uid
555 gid = grp.getgrnam(group).gr_gid578 gid = grp.getgrnam(group).gr_gid
556 if follow_links:579 if follow_links:
@@ -558,6 +581,10 @@
558 else:581 else:
559 chown = os.lchown582 chown = os.lchown
560583
584 if chowntopdir:
585 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
586 if not broken_symlink:
587 chown(path, uid, gid)
561 for root, dirs, files in os.walk(path):588 for root, dirs, files in os.walk(path):
562 for name in dirs + files:589 for name in dirs + files:
563 full = os.path.join(root, name)590 full = os.path.join(root, name)
@@ -568,3 +595,19 @@
568595
569def lchownr(path, owner, group):596def lchownr(path, owner, group):
570 chownr(path, owner, group, follow_links=False)597 chownr(path, owner, group, follow_links=False)
598
599
600def get_total_ram():
601 '''The total amount of system RAM in bytes.
602
603 This is what is reported by the OS, and may be overcommitted when
604 there are multiple containers hosted on the same machine.
605 '''
606 with open('/proc/meminfo', 'r') as f:
607 for line in f.readlines():
608 if line:
609 key, value, unit = line.split()
610 if key == 'MemTotal:':
611 assert unit == 'kB', 'Unknown unit'
612 return int(value) * 1024 # Classic, not KiB.
613 raise NotImplementedError()
571614
=== modified file 'hooks/charmhelpers/core/hugepage.py'
--- hooks/charmhelpers/core/hugepage.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/core/hugepage.py 2015-11-18 20:55:22 +0000
@@ -25,11 +25,13 @@
25 fstab_mount,25 fstab_mount,
26 mkdir,26 mkdir,
27)27)
28from charmhelpers.core.strutils import bytes_from_string
29from subprocess import check_output
2830
2931
30def hugepage_support(user, group='hugetlb', nr_hugepages=256,32def hugepage_support(user, group='hugetlb', nr_hugepages=256,
31 max_map_count=65536, mnt_point='/run/hugepages/kvm',33 max_map_count=65536, mnt_point='/run/hugepages/kvm',
32 pagesize='2MB', mount=True):34 pagesize='2MB', mount=True, set_shmmax=False):
33 """Enable hugepages on system.35 """Enable hugepages on system.
3436
35 Args:37 Args:
@@ -44,11 +46,18 @@
44 group_info = add_group(group)46 group_info = add_group(group)
45 gid = group_info.gr_gid47 gid = group_info.gr_gid
46 add_user_to_group(user, group)48 add_user_to_group(user, group)
49 if max_map_count < 2 * nr_hugepages:
50 max_map_count = 2 * nr_hugepages
47 sysctl_settings = {51 sysctl_settings = {
48 'vm.nr_hugepages': nr_hugepages,52 'vm.nr_hugepages': nr_hugepages,
49 'vm.max_map_count': max_map_count,53 'vm.max_map_count': max_map_count,
50 'vm.hugetlb_shm_group': gid,54 'vm.hugetlb_shm_group': gid,
51 }55 }
56 if set_shmmax:
57 shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
58 shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
59 if shmmax_minsize > shmmax_current:
60 sysctl_settings['kernel.shmmax'] = shmmax_minsize
52 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')61 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
53 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)62 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
54 lfstab = fstab.Fstab()63 lfstab = fstab.Fstab()
5564
=== added file 'hooks/charmhelpers/core/kernel.py'
--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/kernel.py 2015-11-18 20:55:22 +0000
@@ -0,0 +1,68 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# This file is part of charm-helpers.
7#
8# charm-helpers is free software: you can redistribute it and/or modify
9# it under the terms of the GNU Lesser General Public License version 3 as
10# published by the Free Software Foundation.
11#
12# charm-helpers is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
20__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
21
22from charmhelpers.core.hookenv import (
23 log,
24 INFO
25)
26
27from subprocess import check_call, check_output
28import re
29
30
31def modprobe(module, persist=True):
32 """Load a kernel module and configure for auto-load on reboot."""
33 cmd = ['modprobe', module]
34
35 log('Loading kernel module %s' % module, level=INFO)
36
37 check_call(cmd)
38 if persist:
39 with open('/etc/modules', 'r+') as modules:
40 if module not in modules.read():
41 modules.write(module)
42
43
44def rmmod(module, force=False):
45 """Remove a module from the linux kernel"""
46 cmd = ['rmmod']
47 if force:
48 cmd.append('-f')
49 cmd.append(module)
50 log('Removing kernel module %s' % module, level=INFO)
51 return check_call(cmd)
52
53
54def lsmod():
55 """Shows what kernel modules are currently loaded"""
56 return check_output(['lsmod'],
57 universal_newlines=True)
58
59
60def is_module_loaded(module):
61 """Checks if a kernel module is already loaded"""
62 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
63 return len(matches) > 0
64
65
66def update_initramfs(version='all'):
67 """Updates an initramfs image"""
68 return check_call(["update-initramfs", "-k", version, "-u"])
069
=== modified file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2015-11-18 20:55:22 +0000
@@ -249,16 +249,18 @@
249 :param int perms: The permissions of the rendered file249 :param int perms: The permissions of the rendered file
250 :param partial on_change_action: functools partial to be executed when250 :param partial on_change_action: functools partial to be executed when
251 rendered file changes251 rendered file changes
252 :param jinja2 loader template_loader: A jinja2 template loader
252 """253 """
253 def __init__(self, source, target,254 def __init__(self, source, target,
254 owner='root', group='root', perms=0o444,255 owner='root', group='root', perms=0o444,
255 on_change_action=None):256 on_change_action=None, template_loader=None):
256 self.source = source257 self.source = source
257 self.target = target258 self.target = target
258 self.owner = owner259 self.owner = owner
259 self.group = group260 self.group = group
260 self.perms = perms261 self.perms = perms
261 self.on_change_action = on_change_action262 self.on_change_action = on_change_action
263 self.template_loader = template_loader
262264
263 def __call__(self, manager, service_name, event_name):265 def __call__(self, manager, service_name, event_name):
264 pre_checksum = ''266 pre_checksum = ''
@@ -269,7 +271,8 @@
269 for ctx in service.get('required_data', []):271 for ctx in service.get('required_data', []):
270 context.update(ctx)272 context.update(ctx)
271 templating.render(self.source, self.target, context,273 templating.render(self.source, self.target, context,
272 self.owner, self.group, self.perms)274 self.owner, self.group, self.perms,
275 template_loader=self.template_loader)
273 if self.on_change_action:276 if self.on_change_action:
274 if pre_checksum == host.file_hash(self.target):277 if pre_checksum == host.file_hash(self.target):
275 hookenv.log(278 hookenv.log(
276279
=== modified file 'hooks/charmhelpers/core/strutils.py'
--- hooks/charmhelpers/core/strutils.py 2015-04-16 21:32:48 +0000
+++ hooks/charmhelpers/core/strutils.py 2015-11-18 20:55:22 +0000
@@ -18,6 +18,7 @@
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1919
20import six20import six
21import re
2122
2223
23def bool_from_string(value):24def bool_from_string(value):
@@ -40,3 +41,32 @@
4041
41 msg = "Unable to interpret string value '%s' as boolean" % (value)42 msg = "Unable to interpret string value '%s' as boolean" % (value)
42 raise ValueError(msg)43 raise ValueError(msg)
44
45
46def bytes_from_string(value):
47 """Interpret human readable string value as bytes.
48
49 Returns int
50 """
51 BYTE_POWER = {
52 'K': 1,
53 'KB': 1,
54 'M': 2,
55 'MB': 2,
56 'G': 3,
57 'GB': 3,
58 'T': 4,
59 'TB': 4,
60 'P': 5,
61 'PB': 5,
62 }
63 if isinstance(value, six.string_types):
64 value = six.text_type(value)
65 else:
66 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
67 raise ValueError(msg)
68 matches = re.match("([0-9]+)([a-zA-Z]+)", value)
69 if not matches:
70 msg = "Unable to interpret string value '%s' as bytes" % (value)
71 raise ValueError(msg)
72 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
4373
=== modified file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 2015-02-26 13:37:18 +0000
+++ hooks/charmhelpers/core/templating.py 2015-11-18 20:55:22 +0000
@@ -21,7 +21,7 @@
2121
2222
23def render(source, target, context, owner='root', group='root',23def render(source, target, context, owner='root', group='root',
24 perms=0o444, templates_dir=None, encoding='UTF-8'):24 perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
25 """25 """
26 Render a template.26 Render a template.
2727
@@ -52,17 +52,24 @@
52 apt_install('python-jinja2', fatal=True)52 apt_install('python-jinja2', fatal=True)
53 from jinja2 import FileSystemLoader, Environment, exceptions53 from jinja2 import FileSystemLoader, Environment, exceptions
5454
55 if templates_dir is None:55 if template_loader:
56 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')56 template_env = Environment(loader=template_loader)
57 loader = Environment(loader=FileSystemLoader(templates_dir))57 else:
58 if templates_dir is None:
59 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
60 template_env = Environment(loader=FileSystemLoader(templates_dir))
58 try:61 try:
59 source = source62 source = source
60 template = loader.get_template(source)63 template = template_env.get_template(source)
61 except exceptions.TemplateNotFound as e:64 except exceptions.TemplateNotFound as e:
62 hookenv.log('Could not load template %s from %s.' %65 hookenv.log('Could not load template %s from %s.' %
63 (source, templates_dir),66 (source, templates_dir),
64 level=hookenv.ERROR)67 level=hookenv.ERROR)
65 raise e68 raise e
66 content = template.render(context)69 content = template.render(context)
67 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)70 target_dir = os.path.dirname(target)
71 if not os.path.exists(target_dir):
72 # This is a terrible default directory permission, as the file
73 # or its siblings will often contain secrets.
74 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
68 host.write_file(target, content.encode(encoding), owner, group, perms)75 host.write_file(target, content.encode(encoding), owner, group, perms)
6976
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2015-08-19 13:50:42 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2015-11-18 20:55:22 +0000
@@ -225,12 +225,12 @@
225225
226def apt_mark(packages, mark, fatal=False):226def apt_mark(packages, mark, fatal=False):
227 """Flag one or more packages using apt-mark"""227 """Flag one or more packages using apt-mark"""
228 log("Marking {} as {}".format(packages, mark))
228 cmd = ['apt-mark', mark]229 cmd = ['apt-mark', mark]
229 if isinstance(packages, six.string_types):230 if isinstance(packages, six.string_types):
230 cmd.append(packages)231 cmd.append(packages)
231 else:232 else:
232 cmd.extend(packages)233 cmd.extend(packages)
233 log("Holding {}".format(packages))
234234
235 if fatal:235 if fatal:
236 subprocess.check_call(cmd, universal_newlines=True)236 subprocess.check_call(cmd, universal_newlines=True)
237237
=== added symlink 'hooks/osd-devices-storage-attached'
=== target is u'./ceph_hooks.py'
=== added symlink 'hooks/osd-devices-storage-detaching'
=== target is u'./ceph_hooks.py'
=== modified file 'metadata.yaml'
--- metadata.yaml 2015-11-18 10:30:34 +0000
+++ metadata.yaml 2015-11-18 20:55:22 +0000
@@ -19,3 +19,8 @@
19requires:19requires:
20 mon:20 mon:
21 interface: ceph-osd21 interface: ceph-osd
22storage:
23 osd-devices:
24 type: block
25 multiple:
26 range: 0+
22\ No newline at end of file27\ No newline at end of file
2328
=== modified file 'tests/charmhelpers/contrib/amulet/deployment.py'
--- tests/charmhelpers/contrib/amulet/deployment.py 2015-01-26 11:51:28 +0000
+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-11-18 20:55:22 +0000
@@ -51,7 +51,8 @@
51 if 'units' not in this_service:51 if 'units' not in this_service:
52 this_service['units'] = 152 this_service['units'] = 1
5353
54 self.d.add(this_service['name'], units=this_service['units'])54 self.d.add(this_service['name'], units=this_service['units'],
55 constraints=this_service.get('constraints'))
5556
56 for svc in other_services:57 for svc in other_services:
57 if 'location' in svc:58 if 'location' in svc:
@@ -64,7 +65,8 @@
64 if 'units' not in svc:65 if 'units' not in svc:
65 svc['units'] = 166 svc['units'] = 1
6667
67 self.d.add(svc['name'], charm=branch_location, units=svc['units'])68 self.d.add(svc['name'], charm=branch_location, units=svc['units'],
69 constraints=svc.get('constraints'))
6870
69 def _add_relations(self, relations):71 def _add_relations(self, relations):
70 """Add all of the relations for the services."""72 """Add all of the relations for the services."""
7173
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2015-08-19 13:50:42 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2015-11-18 20:55:22 +0000
@@ -19,9 +19,11 @@
19import logging19import logging
20import os20import os
21import re21import re
22import socket
22import subprocess23import subprocess
23import sys24import sys
24import time25import time
26import uuid
2527
26import amulet28import amulet
27import distro_info29import distro_info
@@ -114,7 +116,7 @@
114 # /!\ DEPRECATION WARNING (beisner):116 # /!\ DEPRECATION WARNING (beisner):
115 # New and existing tests should be rewritten to use117 # New and existing tests should be rewritten to use
116 # validate_services_by_name() as it is aware of init systems.118 # validate_services_by_name() as it is aware of init systems.
117 self.log.warn('/!\\ DEPRECATION WARNING: use '119 self.log.warn('DEPRECATION WARNING: use '
118 'validate_services_by_name instead of validate_services '120 'validate_services_by_name instead of validate_services '
119 'due to init system differences.')121 'due to init system differences.')
120122
@@ -269,33 +271,52 @@
269 """Get last modification time of directory."""271 """Get last modification time of directory."""
270 return sentry_unit.directory_stat(directory)['mtime']272 return sentry_unit.directory_stat(directory)['mtime']
271273
272 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):274 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
273 """Get process' start time.275 """Get start time of a process based on the last modification time
274276 of the /proc/pid directory.
275 Determine start time of the process based on the last modification277
276 time of the /proc/pid directory. If pgrep_full is True, the process278 :sentry_unit: The sentry unit to check for the service on
277 name is matched against the full command line.279 :service: service name to look for in process table
278 """280 :pgrep_full: [Deprecated] Use full command line search mode with pgrep
279 if pgrep_full:281 :returns: epoch time of service process start
280 cmd = 'pgrep -o -f {}'.format(service)282 :param commands: list of bash commands
281 else:283 :param sentry_units: list of sentry unit pointers
282 cmd = 'pgrep -o {}'.format(service)284 :returns: None if successful; Failure message otherwise
283 cmd = cmd + ' | grep -v pgrep || exit 0'285 """
284 cmd_out = sentry_unit.run(cmd)286 if pgrep_full is not None:
285 self.log.debug('CMDout: ' + str(cmd_out))287 # /!\ DEPRECATION WARNING (beisner):
286 if cmd_out[0]:288 # No longer implemented, as pidof is now used instead of pgrep.
287 self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))289 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
288 proc_dir = '/proc/{}'.format(cmd_out[0].strip())290 self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
289 return self._get_dir_mtime(sentry_unit, proc_dir)291 'longer implemented re: lp 1474030.')
292
293 pid_list = self.get_process_id_list(sentry_unit, service)
294 pid = pid_list[0]
295 proc_dir = '/proc/{}'.format(pid)
296 self.log.debug('Pid for {} on {}: {}'.format(
297 service, sentry_unit.info['unit_name'], pid))
298
299 return self._get_dir_mtime(sentry_unit, proc_dir)
290300
291 def service_restarted(self, sentry_unit, service, filename,301 def service_restarted(self, sentry_unit, service, filename,
292 pgrep_full=False, sleep_time=20):302 pgrep_full=None, sleep_time=20):
293 """Check if service was restarted.303 """Check if service was restarted.
294304
295 Compare a service's start time vs a file's last modification time305 Compare a service's start time vs a file's last modification time
296 (such as a config file for that service) to determine if the service306 (such as a config file for that service) to determine if the service
297 has been restarted.307 has been restarted.
298 """308 """
309 # /!\ DEPRECATION WARNING (beisner):
310 # This method is prone to races in that no before-time is known.
311 # Use validate_service_config_changed instead.
312
313 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
314 # used instead of pgrep. pgrep_full is still passed through to ensure
315 # deprecation WARNS. lp1474030
316 self.log.warn('DEPRECATION WARNING: use '
317 'validate_service_config_changed instead of '
318 'service_restarted due to known races.')
319
299 time.sleep(sleep_time)320 time.sleep(sleep_time)
300 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=321 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
301 self._get_file_mtime(sentry_unit, filename)):322 self._get_file_mtime(sentry_unit, filename)):
@@ -304,78 +325,122 @@
304 return False325 return False
305326
306 def service_restarted_since(self, sentry_unit, mtime, service,327 def service_restarted_since(self, sentry_unit, mtime, service,
307 pgrep_full=False, sleep_time=20,328 pgrep_full=None, sleep_time=20,
308 retry_count=2):329 retry_count=30, retry_sleep_time=10):
309 """Check if service was been started after a given time.330 """Check if service was been started after a given time.
310331
311 Args:332 Args:
312 sentry_unit (sentry): The sentry unit to check for the service on333 sentry_unit (sentry): The sentry unit to check for the service on
313 mtime (float): The epoch time to check against334 mtime (float): The epoch time to check against
314 service (string): service name to look for in process table335 service (string): service name to look for in process table
315 pgrep_full (boolean): Use full command line search mode with pgrep336 pgrep_full: [Deprecated] Use full command line search mode with pgrep
316 sleep_time (int): Seconds to sleep before looking for process337 sleep_time (int): Initial sleep time (s) before looking for file
317 retry_count (int): If service is not found, how many times to retry338 retry_sleep_time (int): Time (s) to sleep between retries
339 retry_count (int): If file is not found, how many times to retry
318340
319 Returns:341 Returns:
320 bool: True if service found and its start time it newer than mtime,342 bool: True if service found and its start time it newer than mtime,
321 False if service is older than mtime or if service was343 False if service is older than mtime or if service was
322 not found.344 not found.
323 """345 """
324 self.log.debug('Checking %s restarted since %s' % (service, mtime))346 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
347 # used instead of pgrep. pgrep_full is still passed through to ensure
348 # deprecation WARNS. lp1474030
349
350 unit_name = sentry_unit.info['unit_name']
351 self.log.debug('Checking that %s service restarted since %s on '
352 '%s' % (service, mtime, unit_name))
325 time.sleep(sleep_time)353 time.sleep(sleep_time)
326 proc_start_time = self._get_proc_start_time(sentry_unit, service,354 proc_start_time = None
327 pgrep_full)355 tries = 0
328 while retry_count > 0 and not proc_start_time:356 while tries <= retry_count and not proc_start_time:
329 self.log.debug('No pid file found for service %s, will retry %i '357 try:
330 'more times' % (service, retry_count))358 proc_start_time = self._get_proc_start_time(sentry_unit,
331 time.sleep(30)359 service,
332 proc_start_time = self._get_proc_start_time(sentry_unit, service,360 pgrep_full)
333 pgrep_full)361 self.log.debug('Attempt {} to get {} proc start time on {} '
334 retry_count = retry_count - 1362 'OK'.format(tries, service, unit_name))
363 except IOError as e:
364 # NOTE(beisner) - race avoidance, proc may not exist yet.
365 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
366 self.log.debug('Attempt {} to get {} proc start time on {} '
367 'failed\n{}'.format(tries, service,
368 unit_name, e))
369 time.sleep(retry_sleep_time)
370 tries += 1
335371
336 if not proc_start_time:372 if not proc_start_time:
337 self.log.warn('No proc start time found, assuming service did '373 self.log.warn('No proc start time found, assuming service did '
338 'not start')374 'not start')
339 return False375 return False
340 if proc_start_time >= mtime:376 if proc_start_time >= mtime:
341 self.log.debug('proc start time is newer than provided mtime'377 self.log.debug('Proc start time is newer than provided mtime'
342 '(%s >= %s)' % (proc_start_time, mtime))378 '(%s >= %s) on %s (OK)' % (proc_start_time,
379 mtime, unit_name))
343 return True380 return True
344 else:381 else:
345 self.log.warn('proc start time (%s) is older than provided mtime '382 self.log.warn('Proc start time (%s) is older than provided mtime '
346 '(%s), service did not restart' % (proc_start_time,383 '(%s) on %s, service did not '
347 mtime))384 'restart' % (proc_start_time, mtime, unit_name))
348 return False385 return False
349386
350 def config_updated_since(self, sentry_unit, filename, mtime,387 def config_updated_since(self, sentry_unit, filename, mtime,
351 sleep_time=20):388 sleep_time=20, retry_count=30,
389 retry_sleep_time=10):
352 """Check if file was modified after a given time.390 """Check if file was modified after a given time.
353391
354 Args:392 Args:
355 sentry_unit (sentry): The sentry unit to check the file mtime on393 sentry_unit (sentry): The sentry unit to check the file mtime on
356 filename (string): The file to check mtime of394 filename (string): The file to check mtime of
357 mtime (float): The epoch time to check against395 mtime (float): The epoch time to check against
358 sleep_time (int): Seconds to sleep before looking for process396 sleep_time (int): Initial sleep time (s) before looking for file
397 retry_sleep_time (int): Time (s) to sleep between retries
398 retry_count (int): If file is not found, how many times to retry
359399
360 Returns:400 Returns:
361 bool: True if file was modified more recently than mtime, False if401 bool: True if file was modified more recently than mtime, False if
362 file was modified before mtime,402 file was modified before mtime, or if file not found.
363 """403 """
364 self.log.debug('Checking %s updated since %s' % (filename, mtime))404 unit_name = sentry_unit.info['unit_name']
405 self.log.debug('Checking that %s updated since %s on '
406 '%s' % (filename, mtime, unit_name))
365 time.sleep(sleep_time)407 time.sleep(sleep_time)
366 file_mtime = self._get_file_mtime(sentry_unit, filename)408 file_mtime = None
409 tries = 0
410 while tries <= retry_count and not file_mtime:
411 try:
412 file_mtime = self._get_file_mtime(sentry_unit, filename)
413 self.log.debug('Attempt {} to get {} file mtime on {} '
414 'OK'.format(tries, filename, unit_name))
415 except IOError as e:
416 # NOTE(beisner) - race avoidance, file may not exist yet.
417 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
418 self.log.debug('Attempt {} to get {} file mtime on {} '
419 'failed\n{}'.format(tries, filename,
420 unit_name, e))
421 time.sleep(retry_sleep_time)
422 tries += 1
423
424 if not file_mtime:
425 self.log.warn('Could not determine file mtime, assuming '
426 'file does not exist')
427 return False
428
367 if file_mtime >= mtime:429 if file_mtime >= mtime:
368 self.log.debug('File mtime is newer than provided mtime '430 self.log.debug('File mtime is newer than provided mtime '
369 '(%s >= %s)' % (file_mtime, mtime))431 '(%s >= %s) on %s (OK)' % (file_mtime,
432 mtime, unit_name))
370 return True433 return True
371 else:434 else:
372 self.log.warn('File mtime %s is older than provided mtime %s'435 self.log.warn('File mtime is older than provided mtime'
373 % (file_mtime, mtime))436 '(%s < on %s) on %s' % (file_mtime,
437 mtime, unit_name))
374 return False438 return False
375439
376 def validate_service_config_changed(self, sentry_unit, mtime, service,440 def validate_service_config_changed(self, sentry_unit, mtime, service,
377 filename, pgrep_full=False,441 filename, pgrep_full=None,
378 sleep_time=20, retry_count=2):442 sleep_time=20, retry_count=30,
443 retry_sleep_time=10):
379 """Check service and file were updated after mtime444 """Check service and file were updated after mtime
380445
381 Args:446 Args:
@@ -383,9 +448,10 @@
383 mtime (float): The epoch time to check against448 mtime (float): The epoch time to check against
384 service (string): service name to look for in process table449 service (string): service name to look for in process table
385 filename (string): The file to check mtime of450 filename (string): The file to check mtime of
386 pgrep_full (boolean): Use full command line search mode with pgrep451 pgrep_full: [Deprecated] Use full command line search mode with pgrep
387 sleep_time (int): Seconds to sleep before looking for process452 sleep_time (int): Initial sleep in seconds to pass to test helpers
388 retry_count (int): If service is not found, how many times to retry453 retry_count (int): If service is not found, how many times to retry
454 retry_sleep_time (int): Time in seconds to wait between retries
389455
390 Typical Usage:456 Typical Usage:
391 u = OpenStackAmuletUtils(ERROR)457 u = OpenStackAmuletUtils(ERROR)
@@ -402,15 +468,27 @@
402 mtime, False if service is older than mtime or if service was468 mtime, False if service is older than mtime or if service was
403 not found or if filename was modified before mtime.469 not found or if filename was modified before mtime.
404 """470 """
405 self.log.debug('Checking %s restarted since %s' % (service, mtime))471
406 time.sleep(sleep_time)472 # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
407 service_restart = self.service_restarted_since(sentry_unit, mtime,473 # used instead of pgrep. pgrep_full is still passed through to ensure
408 service,474 # deprecation WARNS. lp1474030
409 pgrep_full=pgrep_full,475
410 sleep_time=0,476 service_restart = self.service_restarted_since(
411 retry_count=retry_count)477 sentry_unit, mtime,
412 config_update = self.config_updated_since(sentry_unit, filename, mtime,478 service,
413 sleep_time=0)479 pgrep_full=pgrep_full,
480 sleep_time=sleep_time,
481 retry_count=retry_count,
482 retry_sleep_time=retry_sleep_time)
483
484 config_update = self.config_updated_since(
485 sentry_unit,
486 filename,
487 mtime,
488 sleep_time=sleep_time,
489 retry_count=retry_count,
490 retry_sleep_time=retry_sleep_time)
491
414 return service_restart and config_update492 return service_restart and config_update
415493
416 def get_sentry_time(self, sentry_unit):494 def get_sentry_time(self, sentry_unit):
@@ -428,7 +506,6 @@
428 """Return a list of all Ubuntu releases in order of release."""506 """Return a list of all Ubuntu releases in order of release."""
429 _d = distro_info.UbuntuDistroInfo()507 _d = distro_info.UbuntuDistroInfo()
430 _release_list = _d.all508 _release_list = _d.all
431 self.log.debug('Ubuntu release list: {}'.format(_release_list))
432 return _release_list509 return _release_list
433510
434 def file_to_url(self, file_rel_path):511 def file_to_url(self, file_rel_path):
@@ -568,6 +645,142 @@
568645
569 return None646 return None
570647
648 def validate_sectionless_conf(self, file_contents, expected):
649 """A crude conf parser. Useful to inspect configuration files which
650 do not have section headers (as would be necessary in order to use
651 the configparser). Such as openstack-dashboard or rabbitmq confs."""
652 for line in file_contents.split('\n'):
653 if '=' in line:
654 args = line.split('=')
655 if len(args) <= 1:
656 continue
657 key = args[0].strip()
658 value = args[1].strip()
659 if key in expected.keys():
660 if expected[key] != value:
661 msg = ('Config mismatch. Expected, actual: {}, '
662 '{}'.format(expected[key], value))
663 amulet.raise_status(amulet.FAIL, msg=msg)
664
665 def get_unit_hostnames(self, units):
666 """Return a dict of juju unit names to hostnames."""
667 host_names = {}
668 for unit in units:
669 host_names[unit.info['unit_name']] = \
670 str(unit.file_contents('/etc/hostname').strip())
671 self.log.debug('Unit host names: {}'.format(host_names))
672 return host_names
673
674 def run_cmd_unit(self, sentry_unit, cmd):
675 """Run a command on a unit, return the output and exit code."""
676 output, code = sentry_unit.run(cmd)
677 if code == 0:
678 self.log.debug('{} `{}` command returned {} '
679 '(OK)'.format(sentry_unit.info['unit_name'],
680 cmd, code))
681 else:
682 msg = ('{} `{}` command returned {} '
683 '{}'.format(sentry_unit.info['unit_name'],
684 cmd, code, output))
685 amulet.raise_status(amulet.FAIL, msg=msg)
686 return str(output), code
687
688 def file_exists_on_unit(self, sentry_unit, file_name):
689 """Check if a file exists on a unit."""
690 try:
691 sentry_unit.file_stat(file_name)
692 return True
693 except IOError:
694 return False
695 except Exception as e:
696 msg = 'Error checking file {}: {}'.format(file_name, e)
697 amulet.raise_status(amulet.FAIL, msg=msg)
698
699 def file_contents_safe(self, sentry_unit, file_name,
700 max_wait=60, fatal=False):
701 """Get file contents from a sentry unit. Wrap amulet file_contents
702 with retry logic to address races where a file checks as existing,
703 but no longer exists by the time file_contents is called.
704 Return None if file not found. Optionally raise if fatal is True."""
705 unit_name = sentry_unit.info['unit_name']
706 file_contents = False
707 tries = 0
708 while not file_contents and tries < (max_wait / 4):
709 try:
710 file_contents = sentry_unit.file_contents(file_name)
711 except IOError:
712 self.log.debug('Attempt {} to open file {} from {} '
713 'failed'.format(tries, file_name,
714 unit_name))
715 time.sleep(4)
716 tries += 1
717
718 if file_contents:
719 return file_contents
720 elif not fatal:
721 return None
722 elif fatal:
723 msg = 'Failed to get file contents from unit.'
724 amulet.raise_status(amulet.FAIL, msg)
725
726 def port_knock_tcp(self, host="localhost", port=22, timeout=15):
727 """Open a TCP socket to check for a listening sevice on a host.
728
729 :param host: host name or IP address, default to localhost
730 :param port: TCP port number, default to 22
731 :param timeout: Connect timeout, default to 15 seconds
732 :returns: True if successful, False if connect failed
733 """
734
735 # Resolve host name if possible
736 try:
737 connect_host = socket.gethostbyname(host)
738 host_human = "{} ({})".format(connect_host, host)
739 except socket.error as e:
740 self.log.warn('Unable to resolve address: '
741 '{} ({}) Trying anyway!'.format(host, e))
742 connect_host = host
743 host_human = connect_host
744
745 # Attempt socket connection
746 try:
747 knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
748 knock.settimeout(timeout)
749 knock.connect((connect_host, port))
750 knock.close()
751 self.log.debug('Socket connect OK for host '
752 '{} on port {}.'.format(host_human, port))
753 return True
754 except socket.error as e:
755 self.log.debug('Socket connect FAIL for'
756 ' {} port {} ({})'.format(host_human, port, e))
757 return False
758
759 def port_knock_units(self, sentry_units, port=22,
760 timeout=15, expect_success=True):
761 """Open a TCP socket to check for a listening sevice on each
762 listed juju unit.
763
764 :param sentry_units: list of sentry unit pointers
765 :param port: TCP port number, default to 22
766 :param timeout: Connect timeout, default to 15 seconds
767 :expect_success: True by default, set False to invert logic
768 :returns: None if successful, Failure message otherwise
769 """
770 for unit in sentry_units:
771 host = unit.info['public-address']
772 connected = self.port_knock_tcp(host, port, timeout)
773 if not connected and expect_success:
774 return 'Socket connect failed.'
775 elif connected and not expect_success:
776 return 'Socket connected unexpectedly.'
777
778 def get_uuid_epoch_stamp(self):
779 """Returns a stamp string based on uuid4 and epoch time. Useful in
780 generating test messages which need to be unique-ish."""
781 return '[{}-{}]'.format(uuid.uuid4(), time.time())
782
783# amulet juju action helpers:
571 def run_action(self, unit_sentry, action,784 def run_action(self, unit_sentry, action,
572 _check_output=subprocess.check_output):785 _check_output=subprocess.check_output):
573 """Run the named action on a given unit sentry.786 """Run the named action on a given unit sentry.
@@ -594,3 +807,12 @@
594 output = _check_output(command, universal_newlines=True)807 output = _check_output(command, universal_newlines=True)
595 data = json.loads(output)808 data = json.loads(output)
596 return data.get(u"status") == "completed"809 return data.get(u"status") == "completed"
810
811 def status_get(self, unit):
812 """Return the current service status of this unit."""
813 raw_status, return_code = unit.run(
814 "status-get --format=json --include-data")
815 if return_code != 0:
816 return ("unknown", "")
817 status = json.loads(raw_status)
818 return (status["status"], status["message"])
597819
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-19 13:50:42 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-11-18 20:55:22 +0000
@@ -14,12 +14,18 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1616
17import logging
18import re
19import sys
17import six20import six
18from collections import OrderedDict21from collections import OrderedDict
19from charmhelpers.contrib.amulet.deployment import (22from charmhelpers.contrib.amulet.deployment import (
20 AmuletDeployment23 AmuletDeployment
21)24)
2225
26DEBUG = logging.DEBUG
27ERROR = logging.ERROR
28
2329
24class OpenStackAmuletDeployment(AmuletDeployment):30class OpenStackAmuletDeployment(AmuletDeployment):
25 """OpenStack amulet deployment.31 """OpenStack amulet deployment.
@@ -28,9 +34,12 @@
28 that is specifically for use by OpenStack charms.34 that is specifically for use by OpenStack charms.
29 """35 """
3036
31 def __init__(self, series=None, openstack=None, source=None, stable=True):37 def __init__(self, series=None, openstack=None, source=None,
38 stable=True, log_level=DEBUG):
32 """Initialize the deployment environment."""39 """Initialize the deployment environment."""
33 super(OpenStackAmuletDeployment, self).__init__(series)40 super(OpenStackAmuletDeployment, self).__init__(series)
41 self.log = self.get_logger(level=log_level)
42 self.log.info('OpenStackAmuletDeployment: init')
34 self.openstack = openstack43 self.openstack = openstack
35 self.source = source44 self.source = source
36 self.stable = stable45 self.stable = stable
@@ -38,26 +47,55 @@
38 # out.47 # out.
39 self.current_next = "trusty"48 self.current_next = "trusty"
4049
50 def get_logger(self, name="deployment-logger", level=logging.DEBUG):
51 """Get a logger object that will log to stdout."""
52 log = logging
53 logger = log.getLogger(name)
54 fmt = log.Formatter("%(asctime)s %(funcName)s "
55 "%(levelname)s: %(message)s")
56
57 handler = log.StreamHandler(stream=sys.stdout)
58 handler.setLevel(level)
59 handler.setFormatter(fmt)
60
61 logger.addHandler(handler)
62 logger.setLevel(level)
63
64 return logger
65
41 def _determine_branch_locations(self, other_services):66 def _determine_branch_locations(self, other_services):
42 """Determine the branch locations for the other services.67 """Determine the branch locations for the other services.
4368
44 Determine if the local branch being tested is derived from its69 Determine if the local branch being tested is derived from its
45 stable or next (dev) branch, and based on this, use the corresonding70 stable or next (dev) branch, and based on this, use the corresonding
46 stable or next branches for the other_services."""71 stable or next branches for the other_services."""
72
73 self.log.info('OpenStackAmuletDeployment: determine branch locations')
74
75 # Charms outside the lp:~openstack-charmers namespace
47 base_charms = ['mysql', 'mongodb', 'nrpe']76 base_charms = ['mysql', 'mongodb', 'nrpe']
4877
78 # Force these charms to current series even when using an older series.
79 # ie. Use trusty/nrpe even when series is precise, as the P charm
80 # does not possess the necessary external master config and hooks.
81 force_series_current = ['nrpe']
82
49 if self.series in ['precise', 'trusty']:83 if self.series in ['precise', 'trusty']:
50 base_series = self.series84 base_series = self.series
51 else:85 else:
52 base_series = self.current_next86 base_series = self.current_next
5387
54 if self.stable:88 for svc in other_services:
55 for svc in other_services:89 if svc['name'] in force_series_current:
90 base_series = self.current_next
91 # If a location has been explicitly set, use it
92 if svc.get('location'):
93 continue
94 if self.stable:
56 temp = 'lp:charms/{}/{}'95 temp = 'lp:charms/{}/{}'
57 svc['location'] = temp.format(base_series,96 svc['location'] = temp.format(base_series,
58 svc['name'])97 svc['name'])
59 else:98 else:
60 for svc in other_services:
61 if svc['name'] in base_charms:99 if svc['name'] in base_charms:
62 temp = 'lp:charms/{}/{}'100 temp = 'lp:charms/{}/{}'
63 svc['location'] = temp.format(base_series,101 svc['location'] = temp.format(base_series,
@@ -66,10 +104,13 @@
66 temp = 'lp:~openstack-charmers/charms/{}/{}/next'104 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
67 svc['location'] = temp.format(self.current_next,105 svc['location'] = temp.format(self.current_next,
68 svc['name'])106 svc['name'])
107
69 return other_services108 return other_services
70109
71 def _add_services(self, this_service, other_services):110 def _add_services(self, this_service, other_services):
72 """Add services to the deployment and set openstack-origin/source."""111 """Add services to the deployment and set openstack-origin/source."""
112 self.log.info('OpenStackAmuletDeployment: adding services')
113
73 other_services = self._determine_branch_locations(other_services)114 other_services = self._determine_branch_locations(other_services)
74115
75 super(OpenStackAmuletDeployment, self)._add_services(this_service,116 super(OpenStackAmuletDeployment, self)._add_services(this_service,
@@ -77,29 +118,101 @@
77118
78 services = other_services119 services = other_services
79 services.append(this_service)120 services.append(this_service)
121
122 # Charms which should use the source config option
80 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',123 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
81 'ceph-osd', 'ceph-radosgw']124 'ceph-osd', 'ceph-radosgw']
82 # Most OpenStack subordinate charms do not expose an origin option125
83 # as that is controlled by the principle.126 # Charms which can not use openstack-origin, ie. many subordinates
84 ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']127 no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
85128
86 if self.openstack:129 if self.openstack:
87 for svc in services:130 for svc in services:
88 if svc['name'] not in use_source + ignore:131 if svc['name'] not in use_source + no_origin:
89 config = {'openstack-origin': self.openstack}132 config = {'openstack-origin': self.openstack}
90 self.d.configure(svc['name'], config)133 self.d.configure(svc['name'], config)
91134
92 if self.source:135 if self.source:
93 for svc in services:136 for svc in services:
94 if svc['name'] in use_source and svc['name'] not in ignore:137 if svc['name'] in use_source and svc['name'] not in no_origin:
95 config = {'source': self.source}138 config = {'source': self.source}
96 self.d.configure(svc['name'], config)139 self.d.configure(svc['name'], config)
97140
98 def _configure_services(self, configs):141 def _configure_services(self, configs):
99 """Configure all of the services."""142 """Configure all of the services."""
143 self.log.info('OpenStackAmuletDeployment: configure services')
100 for service, config in six.iteritems(configs):144 for service, config in six.iteritems(configs):
101 self.d.configure(service, config)145 self.d.configure(service, config)
102146
147 def _auto_wait_for_status(self, message=None, exclude_services=None,
148 include_only=None, timeout=1800):
149 """Wait for all units to have a specific extended status, except
150 for any defined as excluded. Unless specified via message, any
151 status containing any case of 'ready' will be considered a match.
152
153 Examples of message usage:
154
155 Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
156 message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
157
158 Wait for all units to reach this status (exact match):
159 message = re.compile('^Unit is ready and clustered$')
160
161 Wait for all units to reach any one of these (exact match):
162 message = re.compile('Unit is ready|OK|Ready')
163
164 Wait for at least one unit to reach this status (exact match):
165 message = {'ready'}
166
167 See Amulet's sentry.wait_for_messages() for message usage detail.
168 https://github.com/juju/amulet/blob/master/amulet/sentry.py
169
170 :param message: Expected status match
171 :param exclude_services: List of juju service names to ignore,
172 not to be used in conjuction with include_only.
173 :param include_only: List of juju service names to exclusively check,
174 not to be used in conjuction with exclude_services.
175 :param timeout: Maximum time in seconds to wait for status match
176 :returns: None. Raises if timeout is hit.
177 """
178 self.log.info('Waiting for extended status on units...')
179
180 all_services = self.d.services.keys()
181
182 if exclude_services and include_only:
183 raise ValueError('exclude_services can not be used '
184 'with include_only')
185
186 if message:
187 if isinstance(message, re._pattern_type):
188 match = message.pattern
189 else:
190 match = message
191
192 self.log.debug('Custom extended status wait match: '
193 '{}'.format(match))
194 else:
195 self.log.debug('Default extended status wait match: contains '
196 'READY (case-insensitive)')
197 message = re.compile('.*ready.*', re.IGNORECASE)
198
199 if exclude_services:
200 self.log.debug('Excluding services from extended status match: '
201 '{}'.format(exclude_services))
202 else:
203 exclude_services = []
204
205 if include_only:
206 services = include_only
207 else:
208 services = list(set(all_services) - set(exclude_services))
209
210 self.log.debug('Waiting up to {}s for extended status on services: '
211 '{}'.format(timeout, services))
212 service_messages = {service: message for service in services}
213 self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
214 self.log.info('OK')
215
103 def _get_openstack_release(self):216 def _get_openstack_release(self):
104 """Get openstack release.217 """Get openstack release.
105218
106219
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-29 14:24:05 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-11-18 20:55:22 +0000
@@ -18,6 +18,7 @@
18import json18import json
19import logging19import logging
20import os20import os
21import re
21import six22import six
22import time23import time
23import urllib24import urllib
@@ -27,6 +28,7 @@
27import heatclient.v1.client as heat_client28import heatclient.v1.client as heat_client
28import keystoneclient.v2_0 as keystone_client29import keystoneclient.v2_0 as keystone_client
29import novaclient.v1_1.client as nova_client30import novaclient.v1_1.client as nova_client
31import pika
30import swiftclient32import swiftclient
3133
32from charmhelpers.contrib.amulet.utils import (34from charmhelpers.contrib.amulet.utils import (
@@ -602,3 +604,382 @@
602 self.log.debug('Ceph {} samples (OK): '604 self.log.debug('Ceph {} samples (OK): '
603 '{}'.format(sample_type, samples))605 '{}'.format(sample_type, samples))
604 return None606 return None
607
608 # rabbitmq/amqp specific helpers:
609
610 def rmq_wait_for_cluster(self, deployment, init_sleep=15, timeout=1200):
611 """Wait for rmq units extended status to show cluster readiness,
612 after an optional initial sleep period. Initial sleep is likely
613 necessary to be effective following a config change, as status
614 message may not instantly update to non-ready."""
615
616 if init_sleep:
617 time.sleep(init_sleep)
618
619 message = re.compile('^Unit is ready and clustered$')
620 deployment._auto_wait_for_status(message=message,
621 timeout=timeout,
622 include_only=['rabbitmq-server'])
623
624 def add_rmq_test_user(self, sentry_units,
625 username="testuser1", password="changeme"):
626 """Add a test user via the first rmq juju unit, check connection as
627 the new user against all sentry units.
628
629 :param sentry_units: list of sentry unit pointers
630 :param username: amqp user name, default to testuser1
631 :param password: amqp user password
632 :returns: None if successful. Raise on error.
633 """
634 self.log.debug('Adding rmq user ({})...'.format(username))
635
636 # Check that user does not already exist
637 cmd_user_list = 'rabbitmqctl list_users'
638 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
639 if username in output:
640 self.log.warning('User ({}) already exists, returning '
641 'gracefully.'.format(username))
642 return
643
644 perms = '".*" ".*" ".*"'
645 cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
646 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
647
648 # Add user via first unit
649 for cmd in cmds:
650 output, _ = self.run_cmd_unit(sentry_units[0], cmd)
651
652 # Check connection against the other sentry_units
653 self.log.debug('Checking user connect against units...')
654 for sentry_unit in sentry_units:
655 connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
656 username=username,
657 password=password)
658 connection.close()
659
660 def delete_rmq_test_user(self, sentry_units, username="testuser1"):
661 """Delete a rabbitmq user via the first rmq juju unit.
662
663 :param sentry_units: list of sentry unit pointers
664 :param username: amqp user name, default to testuser1
665 :param password: amqp user password
666 :returns: None if successful or no such user.
667 """
668 self.log.debug('Deleting rmq user ({})...'.format(username))
669
670 # Check that the user exists
671 cmd_user_list = 'rabbitmqctl list_users'
672 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
673
674 if username not in output:
675 self.log.warning('User ({}) does not exist, returning '
676 'gracefully.'.format(username))
677 return
678
679 # Delete the user
680 cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
681 output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
682
683 def get_rmq_cluster_status(self, sentry_unit):
684 """Execute rabbitmq cluster status command on a unit and return
685 the full output.
686
687 :param unit: sentry unit
688 :returns: String containing console output of cluster status command
689 """
690 cmd = 'rabbitmqctl cluster_status'
691 output, _ = self.run_cmd_unit(sentry_unit, cmd)
692 self.log.debug('{} cluster_status:\n{}'.format(
693 sentry_unit.info['unit_name'], output))
694 return str(output)
695
696 def get_rmq_cluster_running_nodes(self, sentry_unit):
697 """Parse rabbitmqctl cluster_status output string, return list of
698 running rabbitmq cluster nodes.
699
700 :param unit: sentry unit
701 :returns: List containing node names of running nodes
702 """
703 # NOTE(beisner): rabbitmqctl cluster_status output is not
704 # json-parsable, do string chop foo, then json.loads that.
705 str_stat = self.get_rmq_cluster_status(sentry_unit)
706 if 'running_nodes' in str_stat:
707 pos_start = str_stat.find("{running_nodes,") + 15
708 pos_end = str_stat.find("]},", pos_start) + 1
709 str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
710 run_nodes = json.loads(str_run_nodes)
711 return run_nodes
712 else:
713 return []
714
715 def validate_rmq_cluster_running_nodes(self, sentry_units):
716 """Check that all rmq unit hostnames are represented in the
717 cluster_status output of all units.
718
719 :param host_names: dict of juju unit names to host names
720 :param units: list of sentry unit pointers (all rmq units)
721 :returns: None if successful, otherwise return error message
722 """
723 host_names = self.get_unit_hostnames(sentry_units)
724 errors = []
725
726 # Query every unit for cluster_status running nodes
727 for query_unit in sentry_units:
728 query_unit_name = query_unit.info['unit_name']
729 running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
730
731 # Confirm that every unit is represented in the queried unit's
732 # cluster_status running nodes output.
733 for validate_unit in sentry_units:
734 val_host_name = host_names[validate_unit.info['unit_name']]
735 val_node_name = 'rabbit@{}'.format(val_host_name)
736
737 if val_node_name not in running_nodes:
738 errors.append('Cluster member check failed on {}: {} not '
739 'in {}\n'.format(query_unit_name,
740 val_node_name,
741 running_nodes))
742 if errors:
743 return ''.join(errors)
744
745 def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
746 """Check a single juju rmq unit for ssl and port in the config file."""
747 host = sentry_unit.info['public-address']
748 unit_name = sentry_unit.info['unit_name']
749
750 conf_file = '/etc/rabbitmq/rabbitmq.config'
751 conf_contents = str(self.file_contents_safe(sentry_unit,
752 conf_file, max_wait=16))
753 # Checks
754 conf_ssl = 'ssl' in conf_contents
755 conf_port = str(port) in conf_contents
756
757 # Port explicitly checked in config
758 if port and conf_port and conf_ssl:
759 self.log.debug('SSL is enabled @{}:{} '
760 '({})'.format(host, port, unit_name))
761 return True
762 elif port and not conf_port and conf_ssl:
763 self.log.debug('SSL is enabled @{} but not on port {} '
764 '({})'.format(host, port, unit_name))
765 return False
766 # Port not checked (useful when checking that ssl is disabled)
767 elif not port and conf_ssl:
768 self.log.debug('SSL is enabled @{}:{} '
769 '({})'.format(host, port, unit_name))
770 return True
771 elif not conf_ssl:
772 self.log.debug('SSL not enabled @{}:{} '
773 '({})'.format(host, port, unit_name))
774 return False
775 else:
776 msg = ('Unknown condition when checking SSL status @{}:{} '
777 '({})'.format(host, port, unit_name))
778 amulet.raise_status(amulet.FAIL, msg)
779
780 def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
781 """Check that ssl is enabled on rmq juju sentry units.
782
783 :param sentry_units: list of all rmq sentry units
784 :param port: optional ssl port override to validate
785 :returns: None if successful, otherwise return error message
786 """
787 for sentry_unit in sentry_units:
788 if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
789 return ('Unexpected condition: ssl is disabled on unit '
790 '({})'.format(sentry_unit.info['unit_name']))
791 return None
792
793 def validate_rmq_ssl_disabled_units(self, sentry_units):
794 """Check that ssl is enabled on listed rmq juju sentry units.
795
796 :param sentry_units: list of all rmq sentry units
797 :returns: True if successful. Raise on error.
798 """
799 for sentry_unit in sentry_units:
800 if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
801 return ('Unexpected condition: ssl is enabled on unit '
802 '({})'.format(sentry_unit.info['unit_name']))
803 return None
804
805 def configure_rmq_ssl_on(self, sentry_units, deployment,
806 port=None, max_wait=60):
807 """Turn ssl charm config option on, with optional non-default
808 ssl port specification. Confirm that it is enabled on every
809 unit.
810
811 :param sentry_units: list of sentry units
812 :param deployment: amulet deployment object pointer
813 :param port: amqp port, use defaults if None
814 :param max_wait: maximum time to wait in seconds to confirm
815 :returns: None if successful. Raise on error.
816 """
817 self.log.debug('Setting ssl charm config option: on')
818
819 # Enable RMQ SSL
820 config = {'ssl': 'on'}
821 if port:
822 config['ssl_port'] = port
823
824 deployment.d.configure('rabbitmq-server', config)
825
826 # Wait for unit status
827 self.rmq_wait_for_cluster(deployment)
828
829 # Confirm
830 tries = 0
831 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
832 while ret and tries < (max_wait / 4):
833 time.sleep(4)
834 self.log.debug('Attempt {}: {}'.format(tries, ret))
835 ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
836 tries += 1
837
838 if ret:
839 amulet.raise_status(amulet.FAIL, ret)
840
841 def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
842 """Turn ssl charm config option off, confirm that it is disabled
843 on every unit.
844
845 :param sentry_units: list of sentry units
846 :param deployment: amulet deployment object pointer
847 :param max_wait: maximum time to wait in seconds to confirm
848 :returns: None if successful. Raise on error.
849 """
850 self.log.debug('Setting ssl charm config option: off')
851
852 # Disable RMQ SSL
853 config = {'ssl': 'off'}
854 deployment.d.configure('rabbitmq-server', config)
855
856 # Wait for unit status
857 self.rmq_wait_for_cluster(deployment)
858
859 # Confirm
860 tries = 0
861 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
862 while ret and tries < (max_wait / 4):
863 time.sleep(4)
864 self.log.debug('Attempt {}: {}'.format(tries, ret))
865 ret = self.validate_rmq_ssl_disabled_units(sentry_units)
866 tries += 1
867
868 if ret:
869 amulet.raise_status(amulet.FAIL, ret)
870
871 def connect_amqp_by_unit(self, sentry_unit, ssl=False,
872 port=None, fatal=True,
873 username="testuser1", password="changeme"):
874 """Establish and return a pika amqp connection to the rabbitmq service
875 running on a rmq juju unit.
876
877 :param sentry_unit: sentry unit pointer
878 :param ssl: boolean, default to False
879 :param port: amqp port, use defaults if None
880 :param fatal: boolean, default to True (raises on connect error)
881 :param username: amqp user name, default to testuser1
882 :param password: amqp user password
883 :returns: pika amqp connection pointer or None if failed and non-fatal
884 """
885 host = sentry_unit.info['public-address']
886 unit_name = sentry_unit.info['unit_name']
887
888 # Default port logic if port is not specified
889 if ssl and not port:
890 port = 5671
891 elif not ssl and not port:
892 port = 5672
893
894 self.log.debug('Connecting to amqp on {}:{} ({}) as '
895 '{}...'.format(host, port, unit_name, username))
896
897 try:
898 credentials = pika.PlainCredentials(username, password)
899 parameters = pika.ConnectionParameters(host=host, port=port,
900 credentials=credentials,
901 ssl=ssl,
902 connection_attempts=3,
903 retry_delay=5,
904 socket_timeout=1)
905 connection = pika.BlockingConnection(parameters)
906 assert connection.server_properties['product'] == 'RabbitMQ'
907 self.log.debug('Connect OK')
908 return connection
909 except Exception as e:
910 msg = ('amqp connection failed to {}:{} as '
911 '{} ({})'.format(host, port, username, str(e)))
912 if fatal:
913 amulet.raise_status(amulet.FAIL, msg)
914 else:
915 self.log.warn(msg)
916 return None
917
918 def publish_amqp_message_by_unit(self, sentry_unit, message,
919 queue="test", ssl=False,
920 username="testuser1",
921 password="changeme",
922 port=None):
923 """Publish an amqp message to a rmq juju unit.
924
925 :param sentry_unit: sentry unit pointer
926 :param message: amqp message string
927 :param queue: message queue, default to test
928 :param username: amqp user name, default to testuser1
929 :param password: amqp user password
930 :param ssl: boolean, default to False
931 :param port: amqp port, use defaults if None
932 :returns: None. Raises exception if publish failed.
933 """
934 self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
935 message))
936 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
937 port=port,
938 username=username,
939 password=password)
940
941 # NOTE(beisner): extra debug here re: pika hang potential:
942 # https://github.com/pika/pika/issues/297
943 # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
944 self.log.debug('Defining channel...')
945 channel = connection.channel()
946 self.log.debug('Declaring queue...')
947 channel.queue_declare(queue=queue, auto_delete=False, durable=True)
948 self.log.debug('Publishing message...')
949 channel.basic_publish(exchange='', routing_key=queue, body=message)
950 self.log.debug('Closing channel...')
951 channel.close()
952 self.log.debug('Closing connection...')
953 connection.close()
954
955 def get_amqp_message_by_unit(self, sentry_unit, queue="test",
956 username="testuser1",
957 password="changeme",
958 ssl=False, port=None):
959 """Get an amqp message from a rmq juju unit.
960
961 :param sentry_unit: sentry unit pointer
962 :param queue: message queue, default to test
963 :param username: amqp user name, default to testuser1
964 :param password: amqp user password
965 :param ssl: boolean, default to False
966 :param port: amqp port, use defaults if None
967 :returns: amqp message body as string. Raise if get fails.
968 """
969 connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
970 port=port,
971 username=username,
972 password=password)
973 channel = connection.channel()
974 method_frame, _, body = channel.basic_get(queue)
975
976 if method_frame:
977 self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
978 body))
979 channel.basic_ack(method_frame.delivery_tag)
980 channel.close()
981 connection.close()
982 return body
983 else:
984 msg = 'No message retrieved.'
985 amulet.raise_status(amulet.FAIL, msg)

Subscribers

People subscribed via source and target branches