Merge lp:~gnuoy/charms/trusty/glance/next-charm-sync into lp:~openstack-charmers-archive/charms/trusty/glance/next

Proposed by Liam Young
Status: Merged
Merged at revision: 56
Proposed branch: lp:~gnuoy/charms/trusty/glance/next-charm-sync
Merge into: lp:~openstack-charmers-archive/charms/trusty/glance/next
Diff against target: 1530 lines (+897/-113)
17 files modified
hooks/charmhelpers/contrib/hahelpers/cluster.py (+55/-13)
hooks/charmhelpers/contrib/network/ip.py (+19/-1)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+13/-7)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+86/-20)
hooks/charmhelpers/contrib/openstack/context.py (+31/-4)
hooks/charmhelpers/contrib/openstack/ip.py (+7/-3)
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg (+3/-3)
hooks/charmhelpers/core/host.py (+34/-1)
hooks/charmhelpers/core/services/__init__.py (+2/-0)
hooks/charmhelpers/core/services/base.py (+305/-0)
hooks/charmhelpers/core/services/helpers.py (+125/-0)
hooks/charmhelpers/core/templating.py (+51/-0)
hooks/charmhelpers/fetch/__init__.py (+1/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+20/-7)
tests/charmhelpers/contrib/amulet/utils.py (+46/-27)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+13/-7)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+86/-20)
To merge this branch: bzr merge lp:~gnuoy/charms/trusty/glance/next-charm-sync
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+230636@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Liam Young (gnuoy) wrote :

Approved by jamespage

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-07-25 09:37:25 +0000
+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2014-08-13 13:59:42 +0000
@@ -6,6 +6,11 @@
6# Adam Gandelman <adamg@ubuntu.com>6# Adam Gandelman <adamg@ubuntu.com>
7#7#
88
9"""
10Helpers for clustering and determining "cluster leadership" and other
11clustering-related helpers.
12"""
13
9import subprocess14import subprocess
10import os15import os
1116
@@ -19,6 +24,7 @@
19 config as config_get,24 config as config_get,
20 INFO,25 INFO,
21 ERROR,26 ERROR,
27 WARNING,
22 unit_get,28 unit_get,
23)29)
2430
@@ -27,6 +33,29 @@
27 pass33 pass
2834
2935
36def is_elected_leader(resource):
37 """
38 Returns True if the charm executing this is the elected cluster leader.
39
40 It relies on two mechanisms to determine leadership:
41 1. If the charm is part of a corosync cluster, call corosync to
42 determine leadership.
43 2. If the charm is not part of a corosync cluster, the leader is
44 determined as being "the alive unit with the lowest unit numer". In
45 other words, the oldest surviving unit.
46 """
47 if is_clustered():
48 if not is_crm_leader(resource):
49 log('Deferring action to CRM leader.', level=INFO)
50 return False
51 else:
52 peers = peer_units()
53 if peers and not oldest_peer(peers):
54 log('Deferring action to oldest service unit.', level=INFO)
55 return False
56 return True
57
58
30def is_clustered():59def is_clustered():
31 for r_id in (relation_ids('ha') or []):60 for r_id in (relation_ids('ha') or []):
32 for unit in (relation_list(r_id) or []):61 for unit in (relation_list(r_id) or []):
@@ -38,7 +67,11 @@
38 return False67 return False
3968
4069
41def is_leader(resource):70def is_crm_leader(resource):
71 """
72 Returns True if the charm calling this is the elected corosync leader,
73 as returned by calling the external "crm" command.
74 """
42 cmd = [75 cmd = [
43 "crm", "resource",76 "crm", "resource",
44 "show", resource77 "show", resource
@@ -54,15 +87,31 @@
54 return False87 return False
5588
5689
57def peer_units():90def is_leader(resource):
91 log("is_leader is deprecated. Please consider using is_crm_leader "
92 "instead.", level=WARNING)
93 return is_crm_leader(resource)
94
95
96def peer_units(peer_relation="cluster"):
58 peers = []97 peers = []
59 for r_id in (relation_ids('cluster') or []):98 for r_id in (relation_ids(peer_relation) or []):
60 for unit in (relation_list(r_id) or []):99 for unit in (relation_list(r_id) or []):
61 peers.append(unit)100 peers.append(unit)
62 return peers101 return peers
63102
64103
104def peer_ips(peer_relation='cluster', addr_key='private-address'):
105 '''Return a dict of peers and their private-address'''
106 peers = {}
107 for r_id in relation_ids(peer_relation):
108 for unit in relation_list(r_id):
109 peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
110 return peers
111
112
65def oldest_peer(peers):113def oldest_peer(peers):
114 """Determines who the oldest peer is by comparing unit numbers."""
66 local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])115 local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
67 for peer in peers:116 for peer in peers:
68 remote_unit_no = int(peer.split('/')[1])117 remote_unit_no = int(peer.split('/')[1])
@@ -72,16 +121,9 @@
72121
73122
74def eligible_leader(resource):123def eligible_leader(resource):
75 if is_clustered():124 log("eligible_leader is deprecated. Please consider using "
76 if not is_leader(resource):125 "is_elected_leader instead.", level=WARNING)
77 log('Deferring action to CRM leader.', level=INFO)126 return is_elected_leader(resource)
78 return False
79 else:
80 peers = peer_units()
81 if peers and not oldest_peer(peers):
82 log('Deferring action to oldest service unit.', level=INFO)
83 return False
84 return True
85127
86128
87def https():129def https():
88130
=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 2014-07-24 10:26:34 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2014-08-13 13:59:42 +0000
@@ -4,7 +4,7 @@
44
5from charmhelpers.fetch import apt_install5from charmhelpers.fetch import apt_install
6from charmhelpers.core.hookenv import (6from charmhelpers.core.hookenv import (
7 ERROR, log,7 ERROR, log, config,
8)8)
99
10try:10try:
@@ -154,3 +154,21 @@
154get_iface_for_address = partial(_get_for_address, key='iface')154get_iface_for_address = partial(_get_for_address, key='iface')
155155
156get_netmask_for_address = partial(_get_for_address, key='netmask')156get_netmask_for_address = partial(_get_for_address, key='netmask')
157
158
159def get_ipv6_addr(iface="eth0"):
160 try:
161 iface_addrs = netifaces.ifaddresses(iface)
162 if netifaces.AF_INET6 not in iface_addrs:
163 raise Exception("Interface '%s' doesn't have an ipv6 address." % iface)
164
165 addresses = netifaces.ifaddresses(iface)[netifaces.AF_INET6]
166 ipv6_addr = [a['addr'] for a in addresses if not a['addr'].startswith('fe80')
167 and config('vip') != a['addr']]
168 if not ipv6_addr:
169 raise Exception("Interface '%s' doesn't have global ipv6 address." % iface)
170
171 return ipv6_addr[0]
172
173 except ValueError:
174 raise ValueError("Invalid interface '%s'" % iface)
157175
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-10 21:43:51 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-08-13 13:59:42 +0000
@@ -4,8 +4,11 @@
44
55
6class OpenStackAmuletDeployment(AmuletDeployment):6class OpenStackAmuletDeployment(AmuletDeployment):
7 """This class inherits from AmuletDeployment and has additional support7 """OpenStack amulet deployment.
8 that is specifically for use by OpenStack charms."""8
9 This class inherits from AmuletDeployment and has additional support
10 that is specifically for use by OpenStack charms.
11 """
912
10 def __init__(self, series=None, openstack=None, source=None):13 def __init__(self, series=None, openstack=None, source=None):
11 """Initialize the deployment environment."""14 """Initialize the deployment environment."""
@@ -40,11 +43,14 @@
40 self.d.configure(service, config)43 self.d.configure(service, config)
4144
42 def _get_openstack_release(self):45 def _get_openstack_release(self):
43 """Return an integer representing the enum value of the openstack46 """Get openstack release.
44 release."""47
45 self.precise_essex, self.precise_folsom, self.precise_grizzly, \48 Return an integer representing the enum value of the openstack
46 self.precise_havana, self.precise_icehouse, \49 release.
47 self.trusty_icehouse = range(6)50 """
51 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
52 self.precise_havana, self.precise_icehouse,
53 self.trusty_icehouse) = range(6)
48 releases = {54 releases = {
49 ('precise', None): self.precise_essex,55 ('precise', None): self.precise_essex,
50 ('precise', 'cloud:precise-folsom'): self.precise_folsom,56 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
5157
=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-10 21:43:51 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-08-13 13:59:42 +0000
@@ -16,8 +16,11 @@
1616
1717
18class OpenStackAmuletUtils(AmuletUtils):18class OpenStackAmuletUtils(AmuletUtils):
19 """This class inherits from AmuletUtils and has additional support19 """OpenStack amulet utilities.
20 that is specifically for use by OpenStack charms."""20
21 This class inherits from AmuletUtils and has additional support
22 that is specifically for use by OpenStack charms.
23 """
2124
22 def __init__(self, log_level=ERROR):25 def __init__(self, log_level=ERROR):
23 """Initialize the deployment environment."""26 """Initialize the deployment environment."""
@@ -25,13 +28,17 @@
2528
26 def validate_endpoint_data(self, endpoints, admin_port, internal_port,29 def validate_endpoint_data(self, endpoints, admin_port, internal_port,
27 public_port, expected):30 public_port, expected):
28 """Validate actual endpoint data vs expected endpoint data. The ports31 """Validate endpoint data.
29 are used to find the matching endpoint."""32
33 Validate actual endpoint data vs expected endpoint data. The ports
34 are used to find the matching endpoint.
35 """
30 found = False36 found = False
31 for ep in endpoints:37 for ep in endpoints:
32 self.log.debug('endpoint: {}'.format(repr(ep)))38 self.log.debug('endpoint: {}'.format(repr(ep)))
33 if admin_port in ep.adminurl and internal_port in ep.internalurl \39 if (admin_port in ep.adminurl and
34 and public_port in ep.publicurl:40 internal_port in ep.internalurl and
41 public_port in ep.publicurl):
35 found = True42 found = True
36 actual = {'id': ep.id,43 actual = {'id': ep.id,
37 'region': ep.region,44 'region': ep.region,
@@ -47,8 +54,11 @@
47 return 'endpoint not found'54 return 'endpoint not found'
4855
49 def validate_svc_catalog_endpoint_data(self, expected, actual):56 def validate_svc_catalog_endpoint_data(self, expected, actual):
50 """Validate a list of actual service catalog endpoints vs a list of57 """Validate service catalog endpoint data.
51 expected service catalog endpoints."""58
59 Validate a list of actual service catalog endpoints vs a list of
60 expected service catalog endpoints.
61 """
52 self.log.debug('actual: {}'.format(repr(actual)))62 self.log.debug('actual: {}'.format(repr(actual)))
53 for k, v in expected.iteritems():63 for k, v in expected.iteritems():
54 if k in actual:64 if k in actual:
@@ -60,8 +70,11 @@
60 return ret70 return ret
6171
62 def validate_tenant_data(self, expected, actual):72 def validate_tenant_data(self, expected, actual):
63 """Validate a list of actual tenant data vs list of expected tenant73 """Validate tenant data.
64 data."""74
75 Validate a list of actual tenant data vs list of expected tenant
76 data.
77 """
65 self.log.debug('actual: {}'.format(repr(actual)))78 self.log.debug('actual: {}'.format(repr(actual)))
66 for e in expected:79 for e in expected:
67 found = False80 found = False
@@ -78,8 +91,11 @@
78 return ret91 return ret
7992
80 def validate_role_data(self, expected, actual):93 def validate_role_data(self, expected, actual):
81 """Validate a list of actual role data vs a list of expected role94 """Validate role data.
82 data."""95
96 Validate a list of actual role data vs a list of expected role
97 data.
98 """
83 self.log.debug('actual: {}'.format(repr(actual)))99 self.log.debug('actual: {}'.format(repr(actual)))
84 for e in expected:100 for e in expected:
85 found = False101 found = False
@@ -95,8 +111,11 @@
95 return ret111 return ret
96112
97 def validate_user_data(self, expected, actual):113 def validate_user_data(self, expected, actual):
98 """Validate a list of actual user data vs a list of expected user114 """Validate user data.
99 data."""115
116 Validate a list of actual user data vs a list of expected user
117 data.
118 """
100 self.log.debug('actual: {}'.format(repr(actual)))119 self.log.debug('actual: {}'.format(repr(actual)))
101 for e in expected:120 for e in expected:
102 found = False121 found = False
@@ -114,21 +133,24 @@
114 return ret133 return ret
115134
116 def validate_flavor_data(self, expected, actual):135 def validate_flavor_data(self, expected, actual):
117 """Validate a list of actual flavors vs a list of expected flavors."""136 """Validate flavor data.
137
138 Validate a list of actual flavors vs a list of expected flavors.
139 """
118 self.log.debug('actual: {}'.format(repr(actual)))140 self.log.debug('actual: {}'.format(repr(actual)))
119 act = [a.name for a in actual]141 act = [a.name for a in actual]
120 return self._validate_list_data(expected, act)142 return self._validate_list_data(expected, act)
121143
122 def tenant_exists(self, keystone, tenant):144 def tenant_exists(self, keystone, tenant):
123 """Return True if tenant exists"""145 """Return True if tenant exists."""
124 return tenant in [t.name for t in keystone.tenants.list()]146 return tenant in [t.name for t in keystone.tenants.list()]
125147
126 def authenticate_keystone_admin(self, keystone_sentry, user, password,148 def authenticate_keystone_admin(self, keystone_sentry, user, password,
127 tenant):149 tenant):
128 """Authenticates admin user with the keystone admin endpoint."""150 """Authenticates admin user with the keystone admin endpoint."""
129 service_ip = \151 unit = keystone_sentry
130 keystone_sentry.relation('shared-db',152 service_ip = unit.relation('shared-db',
131 'mysql:shared-db')['private-address']153 'mysql:shared-db')['private-address']
132 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))154 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
133 return keystone_client.Client(username=user, password=password,155 return keystone_client.Client(username=user, password=password,
134 tenant_name=tenant, auth_url=ep)156 tenant_name=tenant, auth_url=ep)
@@ -177,12 +199,40 @@
177 image = glance.images.create(name=image_name, is_public=True,199 image = glance.images.create(name=image_name, is_public=True,
178 disk_format='qcow2',200 disk_format='qcow2',
179 container_format='bare', data=f)201 container_format='bare', data=f)
202 count = 1
203 status = image.status
204 while status != 'active' and count < 10:
205 time.sleep(3)
206 image = glance.images.get(image.id)
207 status = image.status
208 self.log.debug('image status: {}'.format(status))
209 count += 1
210
211 if status != 'active':
212 self.log.error('image creation timed out')
213 return None
214
180 return image215 return image
181216
182 def delete_image(self, glance, image):217 def delete_image(self, glance, image):
183 """Delete the specified image."""218 """Delete the specified image."""
219 num_before = len(list(glance.images.list()))
184 glance.images.delete(image)220 glance.images.delete(image)
185221
222 count = 1
223 num_after = len(list(glance.images.list()))
224 while num_after != (num_before - 1) and count < 10:
225 time.sleep(3)
226 num_after = len(list(glance.images.list()))
227 self.log.debug('number of images: {}'.format(num_after))
228 count += 1
229
230 if num_after != (num_before - 1):
231 self.log.error('image deletion timed out')
232 return False
233
234 return True
235
186 def create_instance(self, nova, image_name, instance_name, flavor):236 def create_instance(self, nova, image_name, instance_name, flavor):
187 """Create the specified instance."""237 """Create the specified instance."""
188 image = nova.images.find(name=image_name)238 image = nova.images.find(name=image_name)
@@ -199,11 +249,27 @@
199 self.log.debug('instance status: {}'.format(status))249 self.log.debug('instance status: {}'.format(status))
200 count += 1250 count += 1
201251
202 if status == 'BUILD':252 if status != 'ACTIVE':
253 self.log.error('instance creation timed out')
203 return None254 return None
204255
205 return instance256 return instance
206257
207 def delete_instance(self, nova, instance):258 def delete_instance(self, nova, instance):
208 """Delete the specified instance."""259 """Delete the specified instance."""
260 num_before = len(list(nova.servers.list()))
209 nova.servers.delete(instance)261 nova.servers.delete(instance)
262
263 count = 1
264 num_after = len(list(nova.servers.list()))
265 while num_after != (num_before - 1) and count < 10:
266 time.sleep(3)
267 num_after = len(list(nova.servers.list()))
268 self.log.debug('number of instances: {}'.format(num_after))
269 count += 1
270
271 if num_after != (num_before - 1):
272 self.log.error('instance deletion timed out')
273 return False
274
275 return True
210276
=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py 2014-07-25 09:37:25 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2014-08-13 13:59:42 +0000
@@ -44,7 +44,10 @@
44 neutron_plugin_attribute,44 neutron_plugin_attribute,
45)45)
4646
47from charmhelpers.contrib.network.ip import get_address_in_network47from charmhelpers.contrib.network.ip import (
48 get_address_in_network,
49 get_ipv6_addr,
50)
4851
49CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'52CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
5053
@@ -401,9 +404,12 @@
401404
402 cluster_hosts = {}405 cluster_hosts = {}
403 l_unit = local_unit().replace('/', '-')406 l_unit = local_unit().replace('/', '-')
404 cluster_hosts[l_unit] = \407 if config('prefer-ipv6'):
405 get_address_in_network(config('os-internal-network'),408 addr = get_ipv6_addr()
406 unit_get('private-address'))409 else:
410 addr = unit_get('private-address')
411 cluster_hosts[l_unit] = get_address_in_network(config('os-internal-network'),
412 addr)
407413
408 for rid in relation_ids('cluster'):414 for rid in relation_ids('cluster'):
409 for unit in related_units(rid):415 for unit in related_units(rid):
@@ -414,6 +420,16 @@
414 ctxt = {420 ctxt = {
415 'units': cluster_hosts,421 'units': cluster_hosts,
416 }422 }
423
424 if config('prefer-ipv6'):
425 ctxt['local_host'] = 'ip6-localhost'
426 ctxt['haproxy_host'] = '::'
427 ctxt['stat_port'] = ':::8888'
428 else:
429 ctxt['local_host'] = '127.0.0.1'
430 ctxt['haproxy_host'] = '0.0.0.0'
431 ctxt['stat_port'] = ':8888'
432
417 if len(cluster_hosts.keys()) > 1:433 if len(cluster_hosts.keys()) > 1:
418 # Enable haproxy when we have enough peers.434 # Enable haproxy when we have enough peers.
419 log('Ensuring haproxy enabled in /etc/default/haproxy.')435 log('Ensuring haproxy enabled in /etc/default/haproxy.')
@@ -753,6 +769,17 @@
753 return ctxt769 return ctxt
754770
755771
772class LogLevelContext(OSContextGenerator):
773
774 def __call__(self):
775 ctxt = {}
776 ctxt['debug'] = \
777 False if config('debug') is None else config('debug')
778 ctxt['verbose'] = \
779 False if config('verbose') is None else config('verbose')
780 return ctxt
781
782
756class SyslogContext(OSContextGenerator):783class SyslogContext(OSContextGenerator):
757784
758 def __call__(self):785 def __call__(self):
759786
=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
--- hooks/charmhelpers/contrib/openstack/ip.py 2014-07-03 15:31:54 +0000
+++ hooks/charmhelpers/contrib/openstack/ip.py 2014-08-13 13:59:42 +0000
@@ -7,6 +7,7 @@
7 get_address_in_network,7 get_address_in_network,
8 is_address_in_network,8 is_address_in_network,
9 is_ipv6,9 is_ipv6,
10 get_ipv6_addr,
10)11)
1112
12from charmhelpers.contrib.hahelpers.cluster import is_clustered13from charmhelpers.contrib.hahelpers.cluster import is_clustered
@@ -64,10 +65,13 @@
64 vip):65 vip):
65 resolved_address = vip66 resolved_address = vip
66 else:67 else:
68 if config('prefer-ipv6'):
69 fallback_addr = get_ipv6_addr()
70 else:
71 fallback_addr = unit_get(_address_map[endpoint_type]['fallback'])
67 resolved_address = get_address_in_network(72 resolved_address = get_address_in_network(
68 config(_address_map[endpoint_type]['config']),73 config(_address_map[endpoint_type]['config']), fallback_addr)
69 unit_get(_address_map[endpoint_type]['fallback'])74
70 )
71 if resolved_address is None:75 if resolved_address is None:
72 raise ValueError('Unable to resolve a suitable IP address'76 raise ValueError('Unable to resolve a suitable IP address'
73 ' based on charm state and configuration')77 ' based on charm state and configuration')
7478
=== modified file 'hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg'
--- hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2014-07-03 13:03:26 +0000
+++ hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2014-08-13 13:59:42 +0000
@@ -1,6 +1,6 @@
1global1global
2 log 127.0.0.1 local02 log {{ local_host }} local0
3 log 127.0.0.1 local1 notice3 log {{ local_host }} local1 notice
4 maxconn 200004 maxconn 20000
5 user haproxy5 user haproxy
6 group haproxy6 group haproxy
@@ -17,7 +17,7 @@
17 timeout client 3000017 timeout client 30000
18 timeout server 3000018 timeout server 30000
1919
20listen stats :888820listen stats {{ stat_port }}
21 mode http21 mode http
22 stats enable22 stats enable
23 stats hide-version23 stats hide-version
2424
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2014-07-25 09:37:25 +0000
+++ hooks/charmhelpers/core/host.py 2014-08-13 13:59:42 +0000
@@ -12,6 +12,8 @@
12import string12import string
13import subprocess13import subprocess
14import hashlib14import hashlib
15import shutil
16from contextlib import contextmanager
1517
16from collections import OrderedDict18from collections import OrderedDict
1719
@@ -52,7 +54,7 @@
52def service_running(service):54def service_running(service):
53 """Determine whether a system service is running"""55 """Determine whether a system service is running"""
54 try:56 try:
55 output = subprocess.check_output(['service', service, 'status'])57 output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
56 except subprocess.CalledProcessError:58 except subprocess.CalledProcessError:
57 return False59 return False
58 else:60 else:
@@ -62,6 +64,16 @@
62 return False64 return False
6365
6466
67def service_available(service_name):
68 """Determine whether a system service is available"""
69 try:
70 subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
71 except subprocess.CalledProcessError:
72 return False
73 else:
74 return True
75
76
65def adduser(username, password=None, shell='/bin/bash', system_user=False):77def adduser(username, password=None, shell='/bin/bash', system_user=False):
66 """Add a user to the system"""78 """Add a user to the system"""
67 try:79 try:
@@ -329,3 +341,24 @@
329 pkgcache = apt_pkg.Cache()341 pkgcache = apt_pkg.Cache()
330 pkg = pkgcache[package]342 pkg = pkgcache[package]
331 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)343 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
344
345
346@contextmanager
347def chdir(d):
348 cur = os.getcwd()
349 try:
350 yield os.chdir(d)
351 finally:
352 os.chdir(cur)
353
354
355def chownr(path, owner, group):
356 uid = pwd.getpwnam(owner).pw_uid
357 gid = grp.getgrnam(group).gr_gid
358
359 for root, dirs, files in os.walk(path):
360 for name in dirs + files:
361 full = os.path.join(root, name)
362 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
363 if not broken_symlink:
364 os.chown(full, uid, gid)
332365
=== added directory 'hooks/charmhelpers/core/services'
=== added file 'hooks/charmhelpers/core/services/__init__.py'
--- hooks/charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services/__init__.py 2014-08-13 13:59:42 +0000
@@ -0,0 +1,2 @@
1from .base import *
2from .helpers import *
03
=== added file 'hooks/charmhelpers/core/services/base.py'
--- hooks/charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services/base.py 2014-08-13 13:59:42 +0000
@@ -0,0 +1,305 @@
1import os
2import re
3import json
4from collections import Iterable
5
6from charmhelpers.core import host
7from charmhelpers.core import hookenv
8
9
10__all__ = ['ServiceManager', 'ManagerCallback',
11 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
12 'service_restart', 'service_stop']
13
14
15class ServiceManager(object):
16 def __init__(self, services=None):
17 """
18 Register a list of services, given their definitions.
19
20 Traditional charm authoring is focused on implementing hooks. That is,
21 the charm author is thinking in terms of "What hook am I handling; what
22 does this hook need to do?" However, in most cases, the real question
23 should be "Do I have the information I need to configure and start this
24 piece of software and, if so, what are the steps for doing so?" The
25 ServiceManager framework tries to bring the focus to the data and the
26 setup tasks, in the most declarative way possible.
27
28 Service definitions are dicts in the following formats (all keys except
29 'service' are optional)::
30
31 {
32 "service": <service name>,
33 "required_data": <list of required data contexts>,
34 "data_ready": <one or more callbacks>,
35 "data_lost": <one or more callbacks>,
36 "start": <one or more callbacks>,
37 "stop": <one or more callbacks>,
38 "ports": <list of ports to manage>,
39 }
40
41 The 'required_data' list should contain dicts of required data (or
42 dependency managers that act like dicts and know how to collect the data).
43 Only when all items in the 'required_data' list are populated are the list
44 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
45 information.
46
47 The 'data_ready' value should be either a single callback, or a list of
48 callbacks, to be called when all items in 'required_data' pass `is_ready()`.
49 Each callback will be called with the service name as the only parameter.
50 After all of the 'data_ready' callbacks are called, the 'start' callbacks
51 are fired.
52
53 The 'data_lost' value should be either a single callback, or a list of
54 callbacks, to be called when a 'required_data' item no longer passes
55 `is_ready()`. Each callback will be called with the service name as the
56 only parameter. After all of the 'data_lost' callbacks are called,
57 the 'stop' callbacks are fired.
58
59 The 'start' value should be either a single callback, or a list of
60 callbacks, to be called when starting the service, after the 'data_ready'
61 callbacks are complete. Each callback will be called with the service
62 name as the only parameter. This defaults to
63 `[host.service_start, services.open_ports]`.
64
65 The 'stop' value should be either a single callback, or a list of
66 callbacks, to be called when stopping the service. If the service is
67 being stopped because it no longer has all of its 'required_data', this
68 will be called after all of the 'data_lost' callbacks are complete.
69 Each callback will be called with the service name as the only parameter.
70 This defaults to `[services.close_ports, host.service_stop]`.
71
72 The 'ports' value should be a list of ports to manage. The default
73 'start' handler will open the ports after the service is started,
74 and the default 'stop' handler will close the ports prior to stopping
75 the service.
76
77
78 Examples:
79
80 The following registers an Upstart service called bingod that depends on
81 a mongodb relation and which runs a custom `db_migrate` function prior to
82 restarting the service, and a Runit service called spadesd::
83
84 manager = services.ServiceManager([
85 {
86 'service': 'bingod',
87 'ports': [80, 443],
88 'required_data': [MongoRelation(), config(), {'my': 'data'}],
89 'data_ready': [
90 services.template(source='bingod.conf'),
91 services.template(source='bingod.ini',
92 target='/etc/bingod.ini',
93 owner='bingo', perms=0400),
94 ],
95 },
96 {
97 'service': 'spadesd',
98 'data_ready': services.template(source='spadesd_run.j2',
99 target='/etc/sv/spadesd/run',
100 perms=0555),
101 'start': runit_start,
102 'stop': runit_stop,
103 },
104 ])
105 manager.manage()
106 """
107 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
108 self._ready = None
109 self.services = {}
110 for service in services or []:
111 service_name = service['service']
112 self.services[service_name] = service
113
114 def manage(self):
115 """
116 Handle the current hook by doing The Right Thing with the registered services.
117 """
118 hook_name = hookenv.hook_name()
119 if hook_name == 'stop':
120 self.stop_services()
121 else:
122 self.provide_data()
123 self.reconfigure_services()
124
125 def provide_data(self):
126 hook_name = hookenv.hook_name()
127 for service in self.services.values():
128 for provider in service.get('provided_data', []):
129 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
130 data = provider.provide_data()
131 if provider._is_ready(data):
132 hookenv.relation_set(None, data)
133
134 def reconfigure_services(self, *service_names):
135 """
136 Update all files for one or more registered services, and,
137 if ready, optionally restart them.
138
139 If no service names are given, reconfigures all registered services.
140 """
141 for service_name in service_names or self.services.keys():
142 if self.is_ready(service_name):
143 self.fire_event('data_ready', service_name)
144 self.fire_event('start', service_name, default=[
145 service_restart,
146 manage_ports])
147 self.save_ready(service_name)
148 else:
149 if self.was_ready(service_name):
150 self.fire_event('data_lost', service_name)
151 self.fire_event('stop', service_name, default=[
152 manage_ports,
153 service_stop])
154 self.save_lost(service_name)
155
156 def stop_services(self, *service_names):
157 """
158 Stop one or more registered services, by name.
159
160 If no service names are given, stops all registered services.
161 """
162 for service_name in service_names or self.services.keys():
163 self.fire_event('stop', service_name, default=[
164 manage_ports,
165 service_stop])
166
167 def get_service(self, service_name):
168 """
169 Given the name of a registered service, return its service definition.
170 """
171 service = self.services.get(service_name)
172 if not service:
173 raise KeyError('Service not registered: %s' % service_name)
174 return service
175
176 def fire_event(self, event_name, service_name, default=None):
177 """
178 Fire a data_ready, data_lost, start, or stop event on a given service.
179 """
180 service = self.get_service(service_name)
181 callbacks = service.get(event_name, default)
182 if not callbacks:
183 return
184 if not isinstance(callbacks, Iterable):
185 callbacks = [callbacks]
186 for callback in callbacks:
187 if isinstance(callback, ManagerCallback):
188 callback(self, service_name, event_name)
189 else:
190 callback(service_name)
191
192 def is_ready(self, service_name):
193 """
194 Determine if a registered service is ready, by checking its 'required_data'.
195
196 A 'required_data' item can be any mapping type, and is considered ready
197 if `bool(item)` evaluates as True.
198 """
199 service = self.get_service(service_name)
200 reqs = service.get('required_data', [])
201 return all(bool(req) for req in reqs)
202
203 def _load_ready_file(self):
204 if self._ready is not None:
205 return
206 if os.path.exists(self._ready_file):
207 with open(self._ready_file) as fp:
208 self._ready = set(json.load(fp))
209 else:
210 self._ready = set()
211
212 def _save_ready_file(self):
213 if self._ready is None:
214 return
215 with open(self._ready_file, 'w') as fp:
216 json.dump(list(self._ready), fp)
217
218 def save_ready(self, service_name):
219 """
220 Save an indicator that the given service is now data_ready.
221 """
222 self._load_ready_file()
223 self._ready.add(service_name)
224 self._save_ready_file()
225
226 def save_lost(self, service_name):
227 """
228 Save an indicator that the given service is no longer data_ready.
229 """
230 self._load_ready_file()
231 self._ready.discard(service_name)
232 self._save_ready_file()
233
234 def was_ready(self, service_name):
235 """
236 Determine if the given service was previously data_ready.
237 """
238 self._load_ready_file()
239 return service_name in self._ready
240
241
242class ManagerCallback(object):
243 """
244 Special case of a callback that takes the `ServiceManager` instance
245 in addition to the service name.
246
247 Subclasses should implement `__call__` which should accept three parameters:
248
249 * `manager` The `ServiceManager` instance
250 * `service_name` The name of the service it's being triggered for
251 * `event_name` The name of the event that this callback is handling
252 """
253 def __call__(self, manager, service_name, event_name):
254 raise NotImplementedError()
255
256
257class PortManagerCallback(ManagerCallback):
258 """
259 Callback class that will open or close ports, for use as either
260 a start or stop action.
261 """
262 def __call__(self, manager, service_name, event_name):
263 service = manager.get_service(service_name)
264 new_ports = service.get('ports', [])
265 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
266 if os.path.exists(port_file):
267 with open(port_file) as fp:
268 old_ports = fp.read().split(',')
269 for old_port in old_ports:
270 if bool(old_port):
271 old_port = int(old_port)
272 if old_port not in new_ports:
273 hookenv.close_port(old_port)
274 with open(port_file, 'w') as fp:
275 fp.write(','.join(str(port) for port in new_ports))
276 for port in new_ports:
277 if event_name == 'start':
278 hookenv.open_port(port)
279 elif event_name == 'stop':
280 hookenv.close_port(port)
281
282
283def service_stop(service_name):
284 """
285 Wrapper around host.service_stop to prevent spurious "unknown service"
286 messages in the logs.
287 """
288 if host.service_running(service_name):
289 host.service_stop(service_name)
290
291
292def service_restart(service_name):
293 """
294 Wrapper around host.service_restart to prevent spurious "unknown service"
295 messages in the logs.
296 """
297 if host.service_available(service_name):
298 if host.service_running(service_name):
299 host.service_restart(service_name)
300 else:
301 host.service_start(service_name)
302
303
304# Convenience aliases
305open_ports = close_ports = manage_ports = PortManagerCallback()
0306
=== added file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2014-08-13 13:59:42 +0000
@@ -0,0 +1,125 @@
1from charmhelpers.core import hookenv
2from charmhelpers.core import templating
3
4from charmhelpers.core.services.base import ManagerCallback
5
6
7__all__ = ['RelationContext', 'TemplateCallback',
8 'render_template', 'template']
9
10
11class RelationContext(dict):
12 """
13 Base class for a context generator that gets relation data from juju.
14
15 Subclasses must provide the attributes `name`, which is the name of the
16 interface of interest, `interface`, which is the type of the interface of
17 interest, and `required_keys`, which is the set of keys required for the
18 relation to be considered complete. The data for all interfaces matching
19 the `name` attribute that are complete will used to populate the dictionary
20 values (see `get_data`, below).
21
22 The generated context will be namespaced under the interface type, to prevent
23 potential naming conflicts.
24 """
25 name = None
26 interface = None
27 required_keys = []
28
29 def __init__(self, *args, **kwargs):
30 super(RelationContext, self).__init__(*args, **kwargs)
31 self.get_data()
32
33 def __bool__(self):
34 """
35 Returns True if all of the required_keys are available.
36 """
37 return self.is_ready()
38
39 __nonzero__ = __bool__
40
41 def __repr__(self):
42 return super(RelationContext, self).__repr__()
43
44 def is_ready(self):
45 """
46 Returns True if all of the `required_keys` are available from any units.
47 """
48 ready = len(self.get(self.name, [])) > 0
49 if not ready:
50 hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
51 return ready
52
53 def _is_ready(self, unit_data):
54 """
55 Helper method that tests a set of relation data and returns True if
56 all of the `required_keys` are present.
57 """
58 return set(unit_data.keys()).issuperset(set(self.required_keys))
59
60 def get_data(self):
61 """
62 Retrieve the relation data for each unit involved in a relation and,
63 if complete, store it in a list under `self[self.name]`. This
64 is automatically called when the RelationContext is instantiated.
65
66 The units are sorted lexographically first by the service ID, then by
67 the unit ID. Thus, if an interface has two other services, 'db:1'
68 and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
69 and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
70 set of data, the relation data for the units will be stored in the
71 order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
72
73 If you only care about a single unit on the relation, you can just
74 access it as `{{ interface[0]['key'] }}`. However, if you can at all
75 support multiple units on a relation, you should iterate over the list,
76 like::
77
78 {% for unit in interface -%}
79 {{ unit['key'] }}{% if not loop.last %},{% endif %}
80 {%- endfor %}
81
82 Note that since all sets of relation data from all related services and
83 units are in a single list, if you need to know which service or unit a
84 set of data came from, you'll need to extend this class to preserve
85 that information.
86 """
87 if not hookenv.relation_ids(self.name):
88 return
89
90 ns = self.setdefault(self.name, [])
91 for rid in sorted(hookenv.relation_ids(self.name)):
92 for unit in sorted(hookenv.related_units(rid)):
93 reldata = hookenv.relation_get(rid=rid, unit=unit)
94 if self._is_ready(reldata):
95 ns.append(reldata)
96
97 def provide_data(self):
98 """
99 Return data to be relation_set for this interface.
100 """
101 return {}
102
103
104class TemplateCallback(ManagerCallback):
105 """
106 Callback class that will render a template, for use as a ready action.
107 """
108 def __init__(self, source, target, owner='root', group='root', perms=0444):
109 self.source = source
110 self.target = target
111 self.owner = owner
112 self.group = group
113 self.perms = perms
114
115 def __call__(self, manager, service_name, event_name):
116 service = manager.get_service(service_name)
117 context = {}
118 for ctx in service.get('required_data', []):
119 context.update(ctx)
120 templating.render(self.source, self.target, context,
121 self.owner, self.group, self.perms)
122
123
124# Convenience aliases for templates
125render_template = template = TemplateCallback
0126
=== added file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/templating.py 2014-08-13 13:59:42 +0000
@@ -0,0 +1,51 @@
1import os
2
3from charmhelpers.core import host
4from charmhelpers.core import hookenv
5
6
7def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
8 """
9 Render a template.
10
11 The `source` path, if not absolute, is relative to the `templates_dir`.
12
13 The `target` path should be absolute.
14
15 The context should be a dict containing the values to be replaced in the
16 template.
17
18 The `owner`, `group`, and `perms` options will be passed to `write_file`.
19
20 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
21
22 Note: Using this requires python-jinja2; if it is not installed, calling
23 this will attempt to use charmhelpers.fetch.apt_install to install it.
24 """
25 try:
26 from jinja2 import FileSystemLoader, Environment, exceptions
27 except ImportError:
28 try:
29 from charmhelpers.fetch import apt_install
30 except ImportError:
31 hookenv.log('Could not import jinja2, and could not import '
32 'charmhelpers.fetch to install it',
33 level=hookenv.ERROR)
34 raise
35 apt_install('python-jinja2', fatal=True)
36 from jinja2 import FileSystemLoader, Environment, exceptions
37
38 if templates_dir is None:
39 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
40 loader = Environment(loader=FileSystemLoader(templates_dir))
41 try:
42 source = source
43 template = loader.get_template(source)
44 except exceptions.TemplateNotFound as e:
45 hookenv.log('Could not load template %s from %s.' %
46 (source, templates_dir),
47 level=hookenv.ERROR)
48 raise e
49 content = template.render(context)
50 host.mkdir(os.path.dirname(target))
51 host.write_file(target, content, owner, group, perms)
052
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2014-07-25 09:37:25 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2014-08-13 13:59:42 +0000
@@ -122,6 +122,7 @@
122 # Tell apt to build an in-memory cache to prevent race conditions (if122 # Tell apt to build an in-memory cache to prevent race conditions (if
123 # another process is already building the cache).123 # another process is already building the cache).
124 apt_pkg.config.set("Dir::Cache::pkgcache", "")124 apt_pkg.config.set("Dir::Cache::pkgcache", "")
125 apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
125126
126 cache = apt_pkg.Cache()127 cache = apt_pkg.Cache()
127 _pkgs = []128 _pkgs = []
128129
=== modified file 'tests/charmhelpers/contrib/amulet/deployment.py'
--- tests/charmhelpers/contrib/amulet/deployment.py 2014-07-25 09:37:25 +0000
+++ tests/charmhelpers/contrib/amulet/deployment.py 2014-08-13 13:59:42 +0000
@@ -1,9 +1,14 @@
1import amulet1import amulet
22
3import os
4
35
4class AmuletDeployment(object):6class AmuletDeployment(object):
5 """This class provides generic Amulet deployment and test runner7 """Amulet deployment.
6 methods."""8
9 This class provides generic Amulet deployment and test runner
10 methods.
11 """
712
8 def __init__(self, series=None):13 def __init__(self, series=None):
9 """Initialize the deployment environment."""14 """Initialize the deployment environment."""
@@ -16,11 +21,19 @@
16 self.d = amulet.Deployment()21 self.d = amulet.Deployment()
1722
18 def _add_services(self, this_service, other_services):23 def _add_services(self, this_service, other_services):
19 """Add services to the deployment where this_service is the local charm24 """Add services.
25
26 Add services to the deployment where this_service is the local charm
20 that we're focused on testing and other_services are the other27 that we're focused on testing and other_services are the other
21 charms that come from the charm store."""28 charms that come from the charm store.
29 """
22 name, units = range(2)30 name, units = range(2)
23 self.this_service = this_service[name]31
32 if this_service[name] != os.path.basename(os.getcwd()):
33 s = this_service[name]
34 msg = "The charm's root directory name needs to be {}".format(s)
35 amulet.raise_status(amulet.FAIL, msg=msg)
36
24 self.d.add(this_service[name], units=this_service[units])37 self.d.add(this_service[name], units=this_service[units])
2538
26 for svc in other_services:39 for svc in other_services:
@@ -45,10 +58,10 @@
45 """Deploy environment and wait for all hooks to finish executing."""58 """Deploy environment and wait for all hooks to finish executing."""
46 try:59 try:
47 self.d.setup()60 self.d.setup()
48 self.d.sentry.wait()61 self.d.sentry.wait(timeout=900)
49 except amulet.helpers.TimeoutError:62 except amulet.helpers.TimeoutError:
50 amulet.raise_status(amulet.FAIL, msg="Deployment timed out")63 amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
51 except:64 except Exception:
52 raise65 raise
5366
54 def run_tests(self):67 def run_tests(self):
5568
=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
--- tests/charmhelpers/contrib/amulet/utils.py 2014-07-25 09:37:25 +0000
+++ tests/charmhelpers/contrib/amulet/utils.py 2014-08-13 13:59:42 +0000
@@ -3,12 +3,15 @@
3import logging3import logging
4import re4import re
5import sys5import sys
6from time import sleep6import time
77
88
9class AmuletUtils(object):9class AmuletUtils(object):
10 """This class provides common utility functions that are used by Amulet10 """Amulet utilities.
11 tests."""11
12 This class provides common utility functions that are used by Amulet
13 tests.
14 """
1215
13 def __init__(self, log_level=logging.ERROR):16 def __init__(self, log_level=logging.ERROR):
14 self.log = self.get_logger(level=log_level)17 self.log = self.get_logger(level=log_level)
@@ -17,8 +20,8 @@
17 """Get a logger object that will log to stdout."""20 """Get a logger object that will log to stdout."""
18 log = logging21 log = logging
19 logger = log.getLogger(name)22 logger = log.getLogger(name)
20 fmt = \23 fmt = log.Formatter("%(asctime)s %(funcName)s "
21 log.Formatter("%(asctime)s %(funcName)s %(levelname)s: %(message)s")24 "%(levelname)s: %(message)s")
2225
23 handler = log.StreamHandler(stream=sys.stdout)26 handler = log.StreamHandler(stream=sys.stdout)
24 handler.setLevel(level)27 handler.setLevel(level)
@@ -38,7 +41,7 @@
38 def valid_url(self, url):41 def valid_url(self, url):
39 p = re.compile(42 p = re.compile(
40 r'^(?:http|ftp)s?://'43 r'^(?:http|ftp)s?://'
41 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # flake8: noqa44 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
42 r'localhost|'45 r'localhost|'
43 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'46 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
44 r'(?::\d+)?'47 r'(?::\d+)?'
@@ -50,8 +53,11 @@
50 return False53 return False
5154
52 def validate_services(self, commands):55 def validate_services(self, commands):
53 """Verify the specified services are running on the corresponding56 """Validate services.
54 service units."""57
58 Verify the specified services are running on the corresponding
59 service units.
60 """
55 for k, v in commands.iteritems():61 for k, v in commands.iteritems():
56 for cmd in v:62 for cmd in v:
57 output, code = k.run(cmd)63 output, code = k.run(cmd)
@@ -66,9 +72,13 @@
66 config.readfp(io.StringIO(file_contents))72 config.readfp(io.StringIO(file_contents))
67 return config73 return config
6874
69 def validate_config_data(self, sentry_unit, config_file, section, expected):75 def validate_config_data(self, sentry_unit, config_file, section,
70 """Verify that the specified section of the config file contains76 expected):
71 the expected option key:value pairs."""77 """Validate config file data.
78
79 Verify that the specified section of the config file contains
80 the expected option key:value pairs.
81 """
72 config = self._get_config(sentry_unit, config_file)82 config = self._get_config(sentry_unit, config_file)
7383
74 if section != 'DEFAULT' and not config.has_section(section):84 if section != 'DEFAULT' and not config.has_section(section):
@@ -78,20 +88,23 @@
78 if not config.has_option(section, k):88 if not config.has_option(section, k):
79 return "section [{}] is missing option {}".format(section, k)89 return "section [{}] is missing option {}".format(section, k)
80 if config.get(section, k) != expected[k]:90 if config.get(section, k) != expected[k]:
81 return "section [{}] {}:{} != expected {}:{}".format(section,91 return "section [{}] {}:{} != expected {}:{}".format(
82 k, config.get(section, k), k, expected[k])92 section, k, config.get(section, k), k, expected[k])
83 return None93 return None
8494
85 def _validate_dict_data(self, expected, actual):95 def _validate_dict_data(self, expected, actual):
86 """Compare expected dictionary data vs actual dictionary data.96 """Validate dictionary data.
97
98 Compare expected dictionary data vs actual dictionary data.
87 The values in the 'expected' dictionary can be strings, bools, ints,99 The values in the 'expected' dictionary can be strings, bools, ints,
88 longs, or can be a function that evaluate a variable and returns a100 longs, or can be a function that evaluate a variable and returns a
89 bool."""101 bool.
102 """
90 for k, v in expected.iteritems():103 for k, v in expected.iteritems():
91 if k in actual:104 if k in actual:
92 if isinstance(v, basestring) or \105 if (isinstance(v, basestring) or
93 isinstance(v, bool) or \106 isinstance(v, bool) or
94 isinstance(v, (int, long)):107 isinstance(v, (int, long))):
95 if v != actual[k]:108 if v != actual[k]:
96 return "{}:{}".format(k, actual[k])109 return "{}:{}".format(k, actual[k])
97 elif not v(actual[k]):110 elif not v(actual[k]):
@@ -114,7 +127,7 @@
114 return None127 return None
115128
116 def not_null(self, string):129 def not_null(self, string):
117 if string != None:130 if string is not None:
118 return True131 return True
119 else:132 else:
120 return False133 return False
@@ -128,9 +141,12 @@
128 return sentry_unit.directory_stat(directory)['mtime']141 return sentry_unit.directory_stat(directory)['mtime']
129142
130 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):143 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
131 """Determine start time of the process based on the last modification144 """Get process' start time.
145
146 Determine start time of the process based on the last modification
132 time of the /proc/pid directory. If pgrep_full is True, the process147 time of the /proc/pid directory. If pgrep_full is True, the process
133 name is matched against the full command line."""148 name is matched against the full command line.
149 """
134 if pgrep_full:150 if pgrep_full:
135 cmd = 'pgrep -o -f {}'.format(service)151 cmd = 'pgrep -o -f {}'.format(service)
136 else:152 else:
@@ -139,13 +155,16 @@
139 return self._get_dir_mtime(sentry_unit, proc_dir)155 return self._get_dir_mtime(sentry_unit, proc_dir)
140156
141 def service_restarted(self, sentry_unit, service, filename,157 def service_restarted(self, sentry_unit, service, filename,
142 pgrep_full=False):158 pgrep_full=False, sleep_time=20):
143 """Compare a service's start time vs a file's last modification time159 """Check if service was restarted.
160
161 Compare a service's start time vs a file's last modification time
144 (such as a config file for that service) to determine if the service162 (such as a config file for that service) to determine if the service
145 has been restarted."""163 has been restarted.
146 sleep(10)164 """
147 if self._get_proc_start_time(sentry_unit, service, pgrep_full) >= \165 time.sleep(sleep_time)
148 self._get_file_mtime(sentry_unit, filename):166 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
167 self._get_file_mtime(sentry_unit, filename)):
149 return True168 return True
150 else:169 else:
151 return False170 return False
152171
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2014-07-25 09:37:25 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2014-08-13 13:59:42 +0000
@@ -4,8 +4,11 @@
44
55
6class OpenStackAmuletDeployment(AmuletDeployment):6class OpenStackAmuletDeployment(AmuletDeployment):
7 """This class inherits from AmuletDeployment and has additional support7 """OpenStack amulet deployment.
8 that is specifically for use by OpenStack charms."""8
9 This class inherits from AmuletDeployment and has additional support
10 that is specifically for use by OpenStack charms.
11 """
912
10 def __init__(self, series=None, openstack=None, source=None):13 def __init__(self, series=None, openstack=None, source=None):
11 """Initialize the deployment environment."""14 """Initialize the deployment environment."""
@@ -40,11 +43,14 @@
40 self.d.configure(service, config)43 self.d.configure(service, config)
4144
42 def _get_openstack_release(self):45 def _get_openstack_release(self):
43 """Return an integer representing the enum value of the openstack46 """Get openstack release.
44 release."""47
45 self.precise_essex, self.precise_folsom, self.precise_grizzly, \48 Return an integer representing the enum value of the openstack
46 self.precise_havana, self.precise_icehouse, \49 release.
47 self.trusty_icehouse = range(6)50 """
51 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
52 self.precise_havana, self.precise_icehouse,
53 self.trusty_icehouse) = range(6)
48 releases = {54 releases = {
49 ('precise', None): self.precise_essex,55 ('precise', None): self.precise_essex,
50 ('precise', 'cloud:precise-folsom'): self.precise_folsom,56 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
5157
=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2014-07-25 09:37:25 +0000
+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2014-08-13 13:59:42 +0000
@@ -16,8 +16,11 @@
1616
1717
18class OpenStackAmuletUtils(AmuletUtils):18class OpenStackAmuletUtils(AmuletUtils):
19 """This class inherits from AmuletUtils and has additional support19 """OpenStack amulet utilities.
20 that is specifically for use by OpenStack charms."""20
21 This class inherits from AmuletUtils and has additional support
22 that is specifically for use by OpenStack charms.
23 """
2124
22 def __init__(self, log_level=ERROR):25 def __init__(self, log_level=ERROR):
23 """Initialize the deployment environment."""26 """Initialize the deployment environment."""
@@ -25,13 +28,17 @@
2528
26 def validate_endpoint_data(self, endpoints, admin_port, internal_port,29 def validate_endpoint_data(self, endpoints, admin_port, internal_port,
27 public_port, expected):30 public_port, expected):
28 """Validate actual endpoint data vs expected endpoint data. The ports31 """Validate endpoint data.
29 are used to find the matching endpoint."""32
33 Validate actual endpoint data vs expected endpoint data. The ports
34 are used to find the matching endpoint.
35 """
30 found = False36 found = False
31 for ep in endpoints:37 for ep in endpoints:
32 self.log.debug('endpoint: {}'.format(repr(ep)))38 self.log.debug('endpoint: {}'.format(repr(ep)))
33 if admin_port in ep.adminurl and internal_port in ep.internalurl \39 if (admin_port in ep.adminurl and
34 and public_port in ep.publicurl:40 internal_port in ep.internalurl and
41 public_port in ep.publicurl):
35 found = True42 found = True
36 actual = {'id': ep.id,43 actual = {'id': ep.id,
37 'region': ep.region,44 'region': ep.region,
@@ -47,8 +54,11 @@
47 return 'endpoint not found'54 return 'endpoint not found'
4855
49 def validate_svc_catalog_endpoint_data(self, expected, actual):56 def validate_svc_catalog_endpoint_data(self, expected, actual):
50 """Validate a list of actual service catalog endpoints vs a list of57 """Validate service catalog endpoint data.
51 expected service catalog endpoints."""58
59 Validate a list of actual service catalog endpoints vs a list of
60 expected service catalog endpoints.
61 """
52 self.log.debug('actual: {}'.format(repr(actual)))62 self.log.debug('actual: {}'.format(repr(actual)))
53 for k, v in expected.iteritems():63 for k, v in expected.iteritems():
54 if k in actual:64 if k in actual:
@@ -60,8 +70,11 @@
60 return ret70 return ret
6171
62 def validate_tenant_data(self, expected, actual):72 def validate_tenant_data(self, expected, actual):
63 """Validate a list of actual tenant data vs list of expected tenant73 """Validate tenant data.
64 data."""74
75 Validate a list of actual tenant data vs list of expected tenant
76 data.
77 """
65 self.log.debug('actual: {}'.format(repr(actual)))78 self.log.debug('actual: {}'.format(repr(actual)))
66 for e in expected:79 for e in expected:
67 found = False80 found = False
@@ -78,8 +91,11 @@
78 return ret91 return ret
7992
80 def validate_role_data(self, expected, actual):93 def validate_role_data(self, expected, actual):
81 """Validate a list of actual role data vs a list of expected role94 """Validate role data.
82 data."""95
96 Validate a list of actual role data vs a list of expected role
97 data.
98 """
83 self.log.debug('actual: {}'.format(repr(actual)))99 self.log.debug('actual: {}'.format(repr(actual)))
84 for e in expected:100 for e in expected:
85 found = False101 found = False
@@ -95,8 +111,11 @@
95 return ret111 return ret
96112
97 def validate_user_data(self, expected, actual):113 def validate_user_data(self, expected, actual):
98 """Validate a list of actual user data vs a list of expected user114 """Validate user data.
99 data."""115
116 Validate a list of actual user data vs a list of expected user
117 data.
118 """
100 self.log.debug('actual: {}'.format(repr(actual)))119 self.log.debug('actual: {}'.format(repr(actual)))
101 for e in expected:120 for e in expected:
102 found = False121 found = False
@@ -114,21 +133,24 @@
114 return ret133 return ret
115134
116 def validate_flavor_data(self, expected, actual):135 def validate_flavor_data(self, expected, actual):
117 """Validate a list of actual flavors vs a list of expected flavors."""136 """Validate flavor data.
137
138 Validate a list of actual flavors vs a list of expected flavors.
139 """
118 self.log.debug('actual: {}'.format(repr(actual)))140 self.log.debug('actual: {}'.format(repr(actual)))
119 act = [a.name for a in actual]141 act = [a.name for a in actual]
120 return self._validate_list_data(expected, act)142 return self._validate_list_data(expected, act)
121143
122 def tenant_exists(self, keystone, tenant):144 def tenant_exists(self, keystone, tenant):
123 """Return True if tenant exists"""145 """Return True if tenant exists."""
124 return tenant in [t.name for t in keystone.tenants.list()]146 return tenant in [t.name for t in keystone.tenants.list()]
125147
126 def authenticate_keystone_admin(self, keystone_sentry, user, password,148 def authenticate_keystone_admin(self, keystone_sentry, user, password,
127 tenant):149 tenant):
128 """Authenticates admin user with the keystone admin endpoint."""150 """Authenticates admin user with the keystone admin endpoint."""
129 service_ip = \151 unit = keystone_sentry
130 keystone_sentry.relation('shared-db',152 service_ip = unit.relation('shared-db',
131 'mysql:shared-db')['private-address']153 'mysql:shared-db')['private-address']
132 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))154 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
133 return keystone_client.Client(username=user, password=password,155 return keystone_client.Client(username=user, password=password,
134 tenant_name=tenant, auth_url=ep)156 tenant_name=tenant, auth_url=ep)
@@ -177,12 +199,40 @@
177 image = glance.images.create(name=image_name, is_public=True,199 image = glance.images.create(name=image_name, is_public=True,
178 disk_format='qcow2',200 disk_format='qcow2',
179 container_format='bare', data=f)201 container_format='bare', data=f)
202 count = 1
203 status = image.status
204 while status != 'active' and count < 10:
205 time.sleep(3)
206 image = glance.images.get(image.id)
207 status = image.status
208 self.log.debug('image status: {}'.format(status))
209 count += 1
210
211 if status != 'active':
212 self.log.error('image creation timed out')
213 return None
214
180 return image215 return image
181216
182 def delete_image(self, glance, image):217 def delete_image(self, glance, image):
183 """Delete the specified image."""218 """Delete the specified image."""
219 num_before = len(list(glance.images.list()))
184 glance.images.delete(image)220 glance.images.delete(image)
185221
222 count = 1
223 num_after = len(list(glance.images.list()))
224 while num_after != (num_before - 1) and count < 10:
225 time.sleep(3)
226 num_after = len(list(glance.images.list()))
227 self.log.debug('number of images: {}'.format(num_after))
228 count += 1
229
230 if num_after != (num_before - 1):
231 self.log.error('image deletion timed out')
232 return False
233
234 return True
235
186 def create_instance(self, nova, image_name, instance_name, flavor):236 def create_instance(self, nova, image_name, instance_name, flavor):
187 """Create the specified instance."""237 """Create the specified instance."""
188 image = nova.images.find(name=image_name)238 image = nova.images.find(name=image_name)
@@ -199,11 +249,27 @@
199 self.log.debug('instance status: {}'.format(status))249 self.log.debug('instance status: {}'.format(status))
200 count += 1250 count += 1
201251
202 if status == 'BUILD':252 if status != 'ACTIVE':
253 self.log.error('instance creation timed out')
203 return None254 return None
204255
205 return instance256 return instance
206257
207 def delete_instance(self, nova, instance):258 def delete_instance(self, nova, instance):
208 """Delete the specified instance."""259 """Delete the specified instance."""
260 num_before = len(list(nova.servers.list()))
209 nova.servers.delete(instance)261 nova.servers.delete(instance)
262
263 count = 1
264 num_after = len(list(nova.servers.list()))
265 while num_after != (num_before - 1) and count < 10:
266 time.sleep(3)
267 num_after = len(list(nova.servers.list()))
268 self.log.debug('number of instances: {}'.format(num_after))
269 count += 1
270
271 if num_after != (num_before - 1):
272 self.log.error('instance deletion timed out')
273 return False
274
275 return True

Subscribers

People subscribed via source and target branches