1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
1 |
# Copyright 2014-2015 Canonical Limited.
|
2 |
#
|
|
3 |
# This file is part of charm-helpers.
|
|
4 |
#
|
|
5 |
# charm-helpers is free software: you can redistribute it and/or modify
|
|
6 |
# it under the terms of the GNU Lesser General Public License version 3 as
|
|
7 |
# published by the Free Software Foundation.
|
|
8 |
#
|
|
9 |
# charm-helpers is distributed in the hope that it will be useful,
|
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12 |
# GNU Lesser General Public License for more details.
|
|
13 |
#
|
|
14 |
# 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/>.
|
|
16 |
||
17 |
# Common python helper functions used for OpenStack charms.
|
|
18 |
from collections import OrderedDict |
|
19 |
from functools import wraps |
|
20 |
||
21 |
import subprocess |
|
22 |
import json |
|
23 |
import os |
|
24 |
import sys |
|
14
by Paul Larson
Add support for xenial |
25 |
import re |
26 |
import itertools |
|
27 |
import functools |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
28 |
|
29 |
import six |
|
14
by Paul Larson
Add support for xenial |
30 |
import tempfile |
31 |
import traceback |
|
32 |
import uuid |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
33 |
import yaml |
34 |
||
14
by Paul Larson
Add support for xenial |
35 |
from charmhelpers.contrib.network import ip |
36 |
||
37 |
from charmhelpers.core import ( |
|
38 |
unitdata, |
|
39 |
)
|
|
40 |
||
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
41 |
from charmhelpers.core.hookenv import ( |
14
by Paul Larson
Add support for xenial |
42 |
action_fail, |
43 |
action_set, |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
44 |
config, |
45 |
log as juju_log, |
|
46 |
charm_dir, |
|
14
by Paul Larson
Add support for xenial |
47 |
DEBUG, |
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
48 |
INFO, |
14
by Paul Larson
Add support for xenial |
49 |
related_units, |
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
50 |
relation_ids, |
14
by Paul Larson
Add support for xenial |
51 |
relation_set, |
52 |
status_set, |
|
53 |
hook_name
|
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
54 |
)
|
55 |
||
56 |
from charmhelpers.contrib.storage.linux.lvm import ( |
|
57 |
deactivate_lvm_volume_group, |
|
58 |
is_lvm_physical_volume, |
|
59 |
remove_lvm_physical_volume, |
|
60 |
)
|
|
61 |
||
62 |
from charmhelpers.contrib.network.ip import ( |
|
14
by Paul Larson
Add support for xenial |
63 |
get_ipv6_addr, |
64 |
is_ipv6, |
|
65 |
port_has_listener, |
|
66 |
)
|
|
67 |
||
68 |
from charmhelpers.contrib.python.packages import ( |
|
69 |
pip_create_virtualenv, |
|
70 |
pip_install, |
|
71 |
)
|
|
72 |
||
73 |
from charmhelpers.core.host import ( |
|
74 |
lsb_release, |
|
75 |
mounts, |
|
76 |
umount, |
|
77 |
service_running, |
|
78 |
service_pause, |
|
79 |
service_resume, |
|
80 |
restart_on_change_helper, |
|
81 |
)
|
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
82 |
from charmhelpers.fetch import apt_install, apt_cache, install_remote |
83 |
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk |
|
84 |
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device |
|
85 |
||
86 |
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu" |
|
87 |
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA' |
|
88 |
||
89 |
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed ' |
|
90 |
'restricted main multiverse universe') |
|
91 |
||
92 |
UBUNTU_OPENSTACK_RELEASE = OrderedDict([ |
|
93 |
('oneiric', 'diablo'), |
|
94 |
('precise', 'essex'), |
|
95 |
('quantal', 'folsom'), |
|
96 |
('raring', 'grizzly'), |
|
97 |
('saucy', 'havana'), |
|
98 |
('trusty', 'icehouse'), |
|
99 |
('utopic', 'juno'), |
|
100 |
('vivid', 'kilo'), |
|
14
by Paul Larson
Add support for xenial |
101 |
('wily', 'liberty'), |
102 |
('xenial', 'mitaka'), |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
103 |
])
|
104 |
||
105 |
||
106 |
OPENSTACK_CODENAMES = OrderedDict([ |
|
107 |
('2011.2', 'diablo'), |
|
108 |
('2012.1', 'essex'), |
|
109 |
('2012.2', 'folsom'), |
|
110 |
('2013.1', 'grizzly'), |
|
111 |
('2013.2', 'havana'), |
|
112 |
('2014.1', 'icehouse'), |
|
113 |
('2014.2', 'juno'), |
|
114 |
('2015.1', 'kilo'), |
|
14
by Paul Larson
Add support for xenial |
115 |
('2015.2', 'liberty'), |
116 |
('2016.1', 'mitaka'), |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
117 |
])
|
118 |
||
14
by Paul Larson
Add support for xenial |
119 |
# The ugly duckling - must list releases oldest to newest
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
120 |
SWIFT_CODENAMES = OrderedDict([ |
14
by Paul Larson
Add support for xenial |
121 |
('diablo', |
122 |
['1.4.3']), |
|
123 |
('essex', |
|
124 |
['1.4.8']), |
|
125 |
('folsom', |
|
126 |
['1.7.4']), |
|
127 |
('grizzly', |
|
128 |
['1.7.6', '1.7.7', '1.8.0']), |
|
129 |
('havana', |
|
130 |
['1.9.0', '1.9.1', '1.10.0']), |
|
131 |
('icehouse', |
|
132 |
['1.11.0', '1.12.0', '1.13.0', '1.13.1']), |
|
133 |
('juno', |
|
134 |
['2.0.0', '2.1.0', '2.2.0']), |
|
135 |
('kilo', |
|
136 |
['2.2.1', '2.2.2']), |
|
137 |
('liberty', |
|
138 |
['2.3.0', '2.4.0', '2.5.0']), |
|
139 |
('mitaka', |
|
140 |
['2.5.0', '2.6.0', '2.7.0']), |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
141 |
])
|
142 |
||
14
by Paul Larson
Add support for xenial |
143 |
# >= Liberty version->codename mapping
|
144 |
PACKAGE_CODENAMES = { |
|
145 |
'nova-common': OrderedDict([ |
|
146 |
('12.0', 'liberty'), |
|
147 |
('13.0', 'mitaka'), |
|
148 |
]),
|
|
149 |
'neutron-common': OrderedDict([ |
|
150 |
('7.0', 'liberty'), |
|
151 |
('8.0', 'mitaka'), |
|
152 |
('8.1', 'mitaka'), |
|
153 |
]),
|
|
154 |
'cinder-common': OrderedDict([ |
|
155 |
('7.0', 'liberty'), |
|
156 |
('8.0', 'mitaka'), |
|
157 |
]),
|
|
158 |
'keystone': OrderedDict([ |
|
159 |
('8.0', 'liberty'), |
|
160 |
('8.1', 'liberty'), |
|
161 |
('9.0', 'mitaka'), |
|
162 |
]),
|
|
163 |
'horizon-common': OrderedDict([ |
|
164 |
('8.0', 'liberty'), |
|
165 |
('9.0', 'mitaka'), |
|
166 |
]),
|
|
167 |
'ceilometer-common': OrderedDict([ |
|
168 |
('5.0', 'liberty'), |
|
169 |
('6.0', 'mitaka'), |
|
170 |
]),
|
|
171 |
'heat-common': OrderedDict([ |
|
172 |
('5.0', 'liberty'), |
|
173 |
('6.0', 'mitaka'), |
|
174 |
]),
|
|
175 |
'glance-common': OrderedDict([ |
|
176 |
('11.0', 'liberty'), |
|
177 |
('12.0', 'mitaka'), |
|
178 |
]),
|
|
179 |
'openstack-dashboard': OrderedDict([ |
|
180 |
('8.0', 'liberty'), |
|
181 |
('9.0', 'mitaka'), |
|
182 |
]),
|
|
183 |
}
|
|
184 |
||
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
185 |
DEFAULT_LOOPBACK_SIZE = '5G' |
186 |
||
187 |
||
188 |
def error_out(msg): |
|
189 |
juju_log("FATAL ERROR: %s" % msg, level='ERROR') |
|
190 |
sys.exit(1) |
|
191 |
||
192 |
||
193 |
def get_os_codename_install_source(src): |
|
194 |
'''Derive OpenStack release codename from a given installation source.'''
|
|
195 |
ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] |
|
196 |
rel = '' |
|
197 |
if src is None: |
|
198 |
return rel |
|
199 |
if src in ['distro', 'distro-proposed']: |
|
200 |
try: |
|
201 |
rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel] |
|
202 |
except KeyError: |
|
203 |
e = 'Could not derive openstack release for '\ |
|
204 |
'this Ubuntu release: %s' % ubuntu_rel |
|
205 |
error_out(e) |
|
206 |
return rel |
|
207 |
||
208 |
if src.startswith('cloud:'): |
|
209 |
ca_rel = src.split(':')[1] |
|
210 |
ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0] |
|
211 |
return ca_rel |
|
212 |
||
213 |
# Best guess match based on deb string provided
|
|
214 |
if src.startswith('deb') or src.startswith('ppa'): |
|
215 |
for k, v in six.iteritems(OPENSTACK_CODENAMES): |
|
216 |
if v in src: |
|
217 |
return v |
|
218 |
||
219 |
||
220 |
def get_os_version_install_source(src): |
|
221 |
codename = get_os_codename_install_source(src) |
|
222 |
return get_os_version_codename(codename) |
|
223 |
||
224 |
||
225 |
def get_os_codename_version(vers): |
|
226 |
'''Determine OpenStack codename from version number.'''
|
|
227 |
try: |
|
228 |
return OPENSTACK_CODENAMES[vers] |
|
229 |
except KeyError: |
|
230 |
e = 'Could not determine OpenStack codename for version %s' % vers |
|
231 |
error_out(e) |
|
232 |
||
233 |
||
14
by Paul Larson
Add support for xenial |
234 |
def get_os_version_codename(codename, version_map=OPENSTACK_CODENAMES): |
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
235 |
'''Determine OpenStack version number from codename.'''
|
14
by Paul Larson
Add support for xenial |
236 |
for k, v in six.iteritems(version_map): |
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
237 |
if v == codename: |
238 |
return k |
|
239 |
e = 'Could not derive OpenStack version for '\ |
|
240 |
'codename: %s' % codename |
|
241 |
error_out(e) |
|
242 |
||
243 |
||
14
by Paul Larson
Add support for xenial |
244 |
def get_os_version_codename_swift(codename): |
245 |
'''Determine OpenStack version number of swift from codename.'''
|
|
246 |
for k, v in six.iteritems(SWIFT_CODENAMES): |
|
247 |
if k == codename: |
|
248 |
return v[-1] |
|
249 |
e = 'Could not derive swift version for '\ |
|
250 |
'codename: %s' % codename |
|
251 |
error_out(e) |
|
252 |
||
253 |
||
254 |
def get_swift_codename(version): |
|
255 |
'''Determine OpenStack codename that corresponds to swift version.'''
|
|
256 |
codenames = [k for k, v in six.iteritems(SWIFT_CODENAMES) if version in v] |
|
257 |
if len(codenames) > 1: |
|
258 |
# If more than one release codename contains this version we determine
|
|
259 |
# the actual codename based on the highest available install source.
|
|
260 |
for codename in reversed(codenames): |
|
261 |
releases = UBUNTU_OPENSTACK_RELEASE |
|
262 |
release = [k for k, v in six.iteritems(releases) if codename in v] |
|
263 |
ret = subprocess.check_output(['apt-cache', 'policy', 'swift']) |
|
264 |
if codename in ret or release[0] in ret: |
|
265 |
return codename |
|
266 |
elif len(codenames) == 1: |
|
267 |
return codenames[0] |
|
268 |
return None |
|
269 |
||
270 |
||
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
271 |
def get_os_codename_package(package, fatal=True): |
272 |
'''Derive OpenStack release codename from an installed package.'''
|
|
273 |
import apt_pkg as apt |
|
274 |
||
275 |
cache = apt_cache() |
|
276 |
||
277 |
try: |
|
278 |
pkg = cache[package] |
|
279 |
except: |
|
280 |
if not fatal: |
|
281 |
return None |
|
282 |
# the package is unknown to the current apt cache.
|
|
283 |
e = 'Could not determine version of package with no installation '\ |
|
284 |
'candidate: %s' % package |
|
285 |
error_out(e) |
|
286 |
||
287 |
if not pkg.current_ver: |
|
288 |
if not fatal: |
|
289 |
return None |
|
290 |
# package is known, but no version is currently installed.
|
|
291 |
e = 'Could not determine version of uninstalled package: %s' % package |
|
292 |
error_out(e) |
|
293 |
||
294 |
vers = apt.upstream_version(pkg.current_ver.ver_str) |
|
14
by Paul Larson
Add support for xenial |
295 |
if 'swift' in pkg.name: |
296 |
# Fully x.y.z match for swift versions
|
|
297 |
match = re.match('^(\d+)\.(\d+)\.(\d+)', vers) |
|
298 |
else: |
|
299 |
# x.y match only for 20XX.X
|
|
300 |
# and ignore patch level for other packages
|
|
301 |
match = re.match('^(\d+)\.(\d+)', vers) |
|
302 |
||
303 |
if match: |
|
304 |
vers = match.group(0) |
|
305 |
||
306 |
# >= Liberty independent project versions
|
|
307 |
if (package in PACKAGE_CODENAMES and |
|
308 |
vers in PACKAGE_CODENAMES[package]): |
|
309 |
return PACKAGE_CODENAMES[package][vers] |
|
310 |
else: |
|
311 |
# < Liberty co-ordinated project versions
|
|
312 |
try: |
|
313 |
if 'swift' in pkg.name: |
|
314 |
return get_swift_codename(vers) |
|
315 |
else: |
|
316 |
return OPENSTACK_CODENAMES[vers] |
|
317 |
except KeyError: |
|
318 |
if not fatal: |
|
319 |
return None |
|
320 |
e = 'Could not determine OpenStack codename for version %s' % vers |
|
321 |
error_out(e) |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
322 |
|
323 |
||
324 |
def get_os_version_package(pkg, fatal=True): |
|
325 |
'''Derive OpenStack version number from an installed package.'''
|
|
326 |
codename = get_os_codename_package(pkg, fatal=fatal) |
|
327 |
||
328 |
if not codename: |
|
329 |
return None |
|
330 |
||
331 |
if 'swift' in pkg: |
|
332 |
vers_map = SWIFT_CODENAMES |
|
14
by Paul Larson
Add support for xenial |
333 |
for cname, version in six.iteritems(vers_map): |
334 |
if cname == codename: |
|
335 |
return version[-1] |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
336 |
else: |
337 |
vers_map = OPENSTACK_CODENAMES |
|
14
by Paul Larson
Add support for xenial |
338 |
for version, cname in six.iteritems(vers_map): |
339 |
if cname == codename: |
|
340 |
return version |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
341 |
# e = "Could not determine OpenStack version for package: %s" % pkg
|
342 |
# error_out(e)
|
|
343 |
||
344 |
||
345 |
os_rel = None |
|
346 |
||
347 |
||
348 |
def os_release(package, base='essex'): |
|
349 |
'''
|
|
350 |
Returns OpenStack release codename from a cached global.
|
|
351 |
If the codename can not be determined from either an installed package or
|
|
352 |
the installation source, the earliest release supported by the charm should
|
|
353 |
be returned.
|
|
354 |
'''
|
|
355 |
global os_rel |
|
356 |
if os_rel: |
|
357 |
return os_rel |
|
358 |
os_rel = (get_os_codename_package(package, fatal=False) or |
|
359 |
get_os_codename_install_source(config('openstack-origin')) or |
|
360 |
base) |
|
361 |
return os_rel |
|
362 |
||
363 |
||
364 |
def import_key(keyid): |
|
14
by Paul Larson
Add support for xenial |
365 |
key = keyid.strip() |
366 |
if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and |
|
367 |
key.endswith('-----END PGP PUBLIC KEY BLOCK-----')): |
|
368 |
juju_log("PGP key found (looks like ASCII Armor format)", level=DEBUG) |
|
369 |
juju_log("Importing ASCII Armor PGP key", level=DEBUG) |
|
370 |
with tempfile.NamedTemporaryFile() as keyfile: |
|
371 |
with open(keyfile.name, 'w') as fd: |
|
372 |
fd.write(key) |
|
373 |
fd.write("\n") |
|
374 |
||
375 |
cmd = ['apt-key', 'add', keyfile.name] |
|
376 |
try: |
|
377 |
subprocess.check_call(cmd) |
|
378 |
except subprocess.CalledProcessError: |
|
379 |
error_out("Error importing PGP key '%s'" % key) |
|
380 |
else: |
|
381 |
juju_log("PGP key found (looks like Radix64 format)", level=DEBUG) |
|
382 |
juju_log("Importing PGP key from keyserver", level=DEBUG) |
|
383 |
cmd = ['apt-key', 'adv', '--keyserver', |
|
384 |
'hkp://keyserver.ubuntu.com:80', '--recv-keys', key] |
|
385 |
try: |
|
386 |
subprocess.check_call(cmd) |
|
387 |
except subprocess.CalledProcessError: |
|
388 |
error_out("Error importing PGP key '%s'" % key) |
|
389 |
||
390 |
||
391 |
def get_source_and_pgp_key(input): |
|
392 |
"""Look for a pgp key ID or ascii-armor key in the given input."""
|
|
393 |
index = input.strip() |
|
394 |
index = input.rfind('|') |
|
395 |
if index < 0: |
|
396 |
return input, None |
|
397 |
||
398 |
key = input[index + 1:].strip('|') |
|
399 |
source = input[:index] |
|
400 |
return source, key |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
401 |
|
402 |
||
403 |
def configure_installation_source(rel): |
|
404 |
'''Configure apt installation source.'''
|
|
405 |
if rel == 'distro': |
|
406 |
return
|
|
407 |
elif rel == 'distro-proposed': |
|
408 |
ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] |
|
409 |
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f: |
|
410 |
f.write(DISTRO_PROPOSED % ubuntu_rel) |
|
411 |
elif rel[:4] == "ppa:": |
|
14
by Paul Larson
Add support for xenial |
412 |
src, key = get_source_and_pgp_key(rel) |
413 |
if key: |
|
414 |
import_key(key) |
|
415 |
||
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
416 |
subprocess.check_call(["add-apt-repository", "-y", src]) |
417 |
elif rel[:3] == "deb": |
|
14
by Paul Larson
Add support for xenial |
418 |
src, key = get_source_and_pgp_key(rel) |
419 |
if key: |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
420 |
import_key(key) |
14
by Paul Larson
Add support for xenial |
421 |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
422 |
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f: |
423 |
f.write(src) |
|
424 |
elif rel[:6] == 'cloud:': |
|
425 |
ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] |
|
426 |
rel = rel.split(':')[1] |
|
427 |
u_rel = rel.split('-')[0] |
|
428 |
ca_rel = rel.split('-')[1] |
|
429 |
||
430 |
if u_rel != ubuntu_rel: |
|
431 |
e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\ |
|
432 |
'version (%s)' % (ca_rel, ubuntu_rel) |
|
433 |
error_out(e) |
|
434 |
||
435 |
if 'staging' in ca_rel: |
|
436 |
# staging is just a regular PPA.
|
|
437 |
os_rel = ca_rel.split('/')[0] |
|
438 |
ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel |
|
439 |
cmd = 'add-apt-repository -y %s' % ppa |
|
440 |
subprocess.check_call(cmd.split(' ')) |
|
441 |
return
|
|
442 |
||
443 |
# map charm config options to actual archive pockets.
|
|
444 |
pockets = { |
|
445 |
'folsom': 'precise-updates/folsom', |
|
446 |
'folsom/updates': 'precise-updates/folsom', |
|
447 |
'folsom/proposed': 'precise-proposed/folsom', |
|
448 |
'grizzly': 'precise-updates/grizzly', |
|
449 |
'grizzly/updates': 'precise-updates/grizzly', |
|
450 |
'grizzly/proposed': 'precise-proposed/grizzly', |
|
451 |
'havana': 'precise-updates/havana', |
|
452 |
'havana/updates': 'precise-updates/havana', |
|
453 |
'havana/proposed': 'precise-proposed/havana', |
|
454 |
'icehouse': 'precise-updates/icehouse', |
|
455 |
'icehouse/updates': 'precise-updates/icehouse', |
|
456 |
'icehouse/proposed': 'precise-proposed/icehouse', |
|
457 |
'juno': 'trusty-updates/juno', |
|
458 |
'juno/updates': 'trusty-updates/juno', |
|
459 |
'juno/proposed': 'trusty-proposed/juno', |
|
460 |
'kilo': 'trusty-updates/kilo', |
|
461 |
'kilo/updates': 'trusty-updates/kilo', |
|
462 |
'kilo/proposed': 'trusty-proposed/kilo', |
|
14
by Paul Larson
Add support for xenial |
463 |
'liberty': 'trusty-updates/liberty', |
464 |
'liberty/updates': 'trusty-updates/liberty', |
|
465 |
'liberty/proposed': 'trusty-proposed/liberty', |
|
466 |
'mitaka': 'trusty-updates/mitaka', |
|
467 |
'mitaka/updates': 'trusty-updates/mitaka', |
|
468 |
'mitaka/proposed': 'trusty-proposed/mitaka', |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
469 |
}
|
470 |
||
471 |
try: |
|
472 |
pocket = pockets[ca_rel] |
|
473 |
except KeyError: |
|
474 |
e = 'Invalid Cloud Archive release specified: %s' % rel |
|
475 |
error_out(e) |
|
476 |
||
477 |
src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket) |
|
478 |
apt_install('ubuntu-cloud-keyring', fatal=True) |
|
479 |
||
480 |
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f: |
|
481 |
f.write(src) |
|
482 |
else: |
|
483 |
error_out("Invalid openstack-release specified: %s" % rel) |
|
484 |
||
485 |
||
14
by Paul Larson
Add support for xenial |
486 |
def config_value_changed(option): |
487 |
"""
|
|
488 |
Determine if config value changed since last call to this function.
|
|
489 |
"""
|
|
490 |
hook_data = unitdata.HookData() |
|
491 |
with hook_data(): |
|
492 |
db = unitdata.kv() |
|
493 |
current = config(option) |
|
494 |
saved = db.get(option) |
|
495 |
db.set(option, current) |
|
496 |
if saved is None: |
|
497 |
return False |
|
498 |
return current != saved |
|
499 |
||
500 |
||
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
501 |
def save_script_rc(script_path="scripts/scriptrc", **env_vars): |
502 |
"""
|
|
503 |
Write an rc file in the charm-delivered directory containing
|
|
504 |
exported environment variables provided by env_vars. Any charm scripts run
|
|
505 |
outside the juju hook environment can source this scriptrc to obtain
|
|
506 |
updated config information necessary to perform health checks or
|
|
507 |
service changes.
|
|
508 |
"""
|
|
509 |
juju_rc_path = "%s/%s" % (charm_dir(), script_path) |
|
510 |
if not os.path.exists(os.path.dirname(juju_rc_path)): |
|
511 |
os.mkdir(os.path.dirname(juju_rc_path)) |
|
512 |
with open(juju_rc_path, 'wb') as rc_script: |
|
513 |
rc_script.write( |
|
514 |
"#!/bin/bash\n") |
|
515 |
[rc_script.write('export %s=%s\n' % (u, p)) |
|
516 |
for u, p in six.iteritems(env_vars) if u != "script_path"] |
|
517 |
||
518 |
||
519 |
def openstack_upgrade_available(package): |
|
520 |
"""
|
|
521 |
Determines if an OpenStack upgrade is available from installation
|
|
522 |
source, based on version of installed package.
|
|
523 |
||
524 |
:param package: str: Name of installed package.
|
|
525 |
||
526 |
:returns: bool: : Returns True if configured installation source offers
|
|
527 |
a newer version of package.
|
|
528 |
||
529 |
"""
|
|
530 |
||
531 |
import apt_pkg as apt |
|
532 |
src = config('openstack-origin') |
|
533 |
cur_vers = get_os_version_package(package) |
|
14
by Paul Larson
Add support for xenial |
534 |
if "swift" in package: |
535 |
codename = get_os_codename_install_source(src) |
|
536 |
avail_vers = get_os_version_codename_swift(codename) |
|
537 |
else: |
|
538 |
avail_vers = get_os_version_install_source(src) |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
539 |
apt.init() |
14
by Paul Larson
Add support for xenial |
540 |
if "swift" in package: |
541 |
major_cur_vers = cur_vers.split('.', 1)[0] |
|
542 |
major_avail_vers = avail_vers.split('.', 1)[0] |
|
543 |
major_diff = apt.version_compare(major_avail_vers, major_cur_vers) |
|
544 |
return avail_vers > cur_vers and (major_diff == 1 or major_diff == 0) |
|
545 |
return apt.version_compare(avail_vers, cur_vers) == 1 |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
546 |
|
547 |
||
548 |
def ensure_block_device(block_device): |
|
549 |
'''
|
|
550 |
Confirm block_device, create as loopback if necessary.
|
|
551 |
||
552 |
:param block_device: str: Full path of block device to ensure.
|
|
553 |
||
554 |
:returns: str: Full path of ensured block device.
|
|
555 |
'''
|
|
556 |
_none = ['None', 'none', None] |
|
557 |
if (block_device in _none): |
|
558 |
error_out('prepare_storage(): Missing required input: block_device=%s.' |
|
559 |
% block_device) |
|
560 |
||
561 |
if block_device.startswith('/dev/'): |
|
562 |
bdev = block_device |
|
563 |
elif block_device.startswith('/'): |
|
564 |
_bd = block_device.split('|') |
|
565 |
if len(_bd) == 2: |
|
566 |
bdev, size = _bd |
|
567 |
else: |
|
568 |
bdev = block_device |
|
569 |
size = DEFAULT_LOOPBACK_SIZE |
|
570 |
bdev = ensure_loopback_device(bdev, size) |
|
571 |
else: |
|
572 |
bdev = '/dev/%s' % block_device |
|
573 |
||
574 |
if not is_block_device(bdev): |
|
575 |
error_out('Failed to locate valid block device at %s' % bdev) |
|
576 |
||
577 |
return bdev |
|
578 |
||
579 |
||
580 |
def clean_storage(block_device): |
|
581 |
'''
|
|
582 |
Ensures a block device is clean. That is:
|
|
583 |
- unmounted
|
|
584 |
- any lvm volume groups are deactivated
|
|
585 |
- any lvm physical device signatures removed
|
|
586 |
- partition table wiped
|
|
587 |
||
588 |
:param block_device: str: Full path to block device to clean.
|
|
589 |
'''
|
|
590 |
for mp, d in mounts(): |
|
591 |
if d == block_device: |
|
592 |
juju_log('clean_storage(): %s is mounted @ %s, unmounting.' % |
|
593 |
(d, mp), level=INFO) |
|
594 |
umount(mp, persist=True) |
|
595 |
||
596 |
if is_lvm_physical_volume(block_device): |
|
597 |
deactivate_lvm_volume_group(block_device) |
|
598 |
remove_lvm_physical_volume(block_device) |
|
599 |
else: |
|
600 |
zap_disk(block_device) |
|
601 |
||
14
by Paul Larson
Add support for xenial |
602 |
is_ip = ip.is_ip |
603 |
ns_query = ip.ns_query |
|
604 |
get_host_ip = ip.get_host_ip |
|
605 |
get_hostname = ip.get_hostname |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
606 |
|
607 |
||
608 |
def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'): |
|
609 |
mm_map = {} |
|
610 |
if os.path.isfile(mm_file): |
|
611 |
with open(mm_file, 'r') as f: |
|
612 |
mm_map = json.load(f) |
|
613 |
return mm_map |
|
614 |
||
615 |
||
616 |
def sync_db_with_multi_ipv6_addresses(database, database_user, |
|
617 |
relation_prefix=None): |
|
618 |
hosts = get_ipv6_addr(dynamic_only=False) |
|
619 |
||
14
by Paul Larson
Add support for xenial |
620 |
if config('vip'): |
621 |
vips = config('vip').split() |
|
622 |
for vip in vips: |
|
623 |
if vip and is_ipv6(vip): |
|
624 |
hosts.append(vip) |
|
625 |
||
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
626 |
kwargs = {'database': database, |
627 |
'username': database_user, |
|
628 |
'hostname': json.dumps(hosts)} |
|
629 |
||
630 |
if relation_prefix: |
|
631 |
for key in list(kwargs.keys()): |
|
632 |
kwargs["%s_%s" % (relation_prefix, key)] = kwargs[key] |
|
633 |
del kwargs[key] |
|
634 |
||
635 |
for rid in relation_ids('shared-db'): |
|
636 |
relation_set(relation_id=rid, **kwargs) |
|
637 |
||
638 |
||
639 |
def os_requires_version(ostack_release, pkg): |
|
640 |
"""
|
|
641 |
Decorator for hook to specify minimum supported release
|
|
642 |
"""
|
|
643 |
def wrap(f): |
|
644 |
@wraps(f) |
|
645 |
def wrapped_f(*args): |
|
646 |
if os_release(pkg) < ostack_release: |
|
647 |
raise Exception("This hook is not supported on releases" |
|
648 |
" before %s" % ostack_release) |
|
649 |
f(*args) |
|
650 |
return wrapped_f |
|
651 |
return wrap |
|
652 |
||
653 |
||
654 |
def git_install_requested(): |
|
14
by Paul Larson
Add support for xenial |
655 |
"""
|
656 |
Returns true if openstack-origin-git is specified.
|
|
657 |
"""
|
|
658 |
return config('openstack-origin-git') is not None |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
659 |
|
660 |
||
661 |
requirements_dir = None |
|
662 |
||
663 |
||
14
by Paul Larson
Add support for xenial |
664 |
def _git_yaml_load(projects_yaml): |
665 |
"""
|
|
666 |
Load the specified yaml into a dictionary.
|
|
667 |
"""
|
|
668 |
if not projects_yaml: |
|
669 |
return None |
|
670 |
||
671 |
return yaml.load(projects_yaml) |
|
672 |
||
673 |
||
674 |
def git_clone_and_install(projects_yaml, core_project): |
|
675 |
"""
|
|
676 |
Clone/install all specified OpenStack repositories.
|
|
677 |
||
678 |
The expected format of projects_yaml is:
|
|
679 |
||
680 |
repositories:
|
|
681 |
- {name: keystone,
|
|
682 |
repository: 'git://git.openstack.org/openstack/keystone.git',
|
|
683 |
branch: 'stable/icehouse'}
|
|
684 |
- {name: requirements,
|
|
685 |
repository: 'git://git.openstack.org/openstack/requirements.git',
|
|
686 |
branch: 'stable/icehouse'}
|
|
687 |
||
688 |
directory: /mnt/openstack-git
|
|
689 |
http_proxy: squid-proxy-url
|
|
690 |
https_proxy: squid-proxy-url
|
|
691 |
||
692 |
The directory, http_proxy, and https_proxy keys are optional.
|
|
693 |
||
694 |
"""
|
|
695 |
global requirements_dir |
|
696 |
parent_dir = '/mnt/openstack-git' |
|
697 |
http_proxy = None |
|
698 |
||
699 |
projects = _git_yaml_load(projects_yaml) |
|
700 |
_git_validate_projects_yaml(projects, core_project) |
|
701 |
||
702 |
old_environ = dict(os.environ) |
|
703 |
||
704 |
if 'http_proxy' in projects.keys(): |
|
705 |
http_proxy = projects['http_proxy'] |
|
706 |
os.environ['http_proxy'] = projects['http_proxy'] |
|
707 |
if 'https_proxy' in projects.keys(): |
|
708 |
os.environ['https_proxy'] = projects['https_proxy'] |
|
709 |
||
710 |
if 'directory' in projects.keys(): |
|
711 |
parent_dir = projects['directory'] |
|
712 |
||
713 |
pip_create_virtualenv(os.path.join(parent_dir, 'venv')) |
|
714 |
||
715 |
# Upgrade setuptools and pip from default virtualenv versions. The default
|
|
716 |
# versions in trusty break master OpenStack branch deployments.
|
|
717 |
for p in ['pip', 'setuptools']: |
|
718 |
pip_install(p, upgrade=True, proxy=http_proxy, |
|
719 |
venv=os.path.join(parent_dir, 'venv')) |
|
720 |
||
721 |
for p in projects['repositories']: |
|
722 |
repo = p['repository'] |
|
723 |
branch = p['branch'] |
|
724 |
depth = '1' |
|
725 |
if 'depth' in p.keys(): |
|
726 |
depth = p['depth'] |
|
727 |
if p['name'] == 'requirements': |
|
728 |
repo_dir = _git_clone_and_install_single(repo, branch, depth, |
|
729 |
parent_dir, http_proxy, |
|
730 |
update_requirements=False) |
|
731 |
requirements_dir = repo_dir |
|
732 |
else: |
|
733 |
repo_dir = _git_clone_and_install_single(repo, branch, depth, |
|
734 |
parent_dir, http_proxy, |
|
735 |
update_requirements=True) |
|
736 |
||
737 |
os.environ = old_environ |
|
738 |
||
739 |
||
740 |
def _git_validate_projects_yaml(projects, core_project): |
|
741 |
"""
|
|
742 |
Validate the projects yaml.
|
|
743 |
"""
|
|
744 |
_git_ensure_key_exists('repositories', projects) |
|
745 |
||
746 |
for project in projects['repositories']: |
|
747 |
_git_ensure_key_exists('name', project.keys()) |
|
748 |
_git_ensure_key_exists('repository', project.keys()) |
|
749 |
_git_ensure_key_exists('branch', project.keys()) |
|
750 |
||
751 |
if projects['repositories'][0]['name'] != 'requirements': |
|
752 |
error_out('{} git repo must be specified first'.format('requirements')) |
|
753 |
||
754 |
if projects['repositories'][-1]['name'] != core_project: |
|
755 |
error_out('{} git repo must be specified last'.format(core_project)) |
|
756 |
||
757 |
||
758 |
def _git_ensure_key_exists(key, keys): |
|
759 |
"""
|
|
760 |
Ensure that key exists in keys.
|
|
761 |
"""
|
|
762 |
if key not in keys: |
|
763 |
error_out('openstack-origin-git key \'{}\' is missing'.format(key)) |
|
764 |
||
765 |
||
766 |
def _git_clone_and_install_single(repo, branch, depth, parent_dir, http_proxy, |
|
767 |
update_requirements): |
|
768 |
"""
|
|
769 |
Clone and install a single git repository.
|
|
770 |
"""
|
|
771 |
if not os.path.exists(parent_dir): |
|
772 |
juju_log('Directory already exists at {}. ' |
|
773 |
'No need to create directory.'.format(parent_dir)) |
|
774 |
os.mkdir(parent_dir) |
|
775 |
||
776 |
juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch)) |
|
777 |
repo_dir = install_remote( |
|
778 |
repo, dest=parent_dir, branch=branch, depth=depth) |
|
779 |
||
780 |
venv = os.path.join(parent_dir, 'venv') |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
781 |
|
782 |
if update_requirements: |
|
783 |
if not requirements_dir: |
|
784 |
error_out('requirements repo must be cloned before ' |
|
785 |
'updating from global requirements.') |
|
14
by Paul Larson
Add support for xenial |
786 |
_git_update_requirements(venv, repo_dir, requirements_dir) |
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
787 |
|
788 |
juju_log('Installing git repo from dir: {}'.format(repo_dir)) |
|
14
by Paul Larson
Add support for xenial |
789 |
if http_proxy: |
790 |
pip_install(repo_dir, proxy=http_proxy, venv=venv) |
|
791 |
else: |
|
792 |
pip_install(repo_dir, venv=venv) |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
793 |
|
794 |
return repo_dir |
|
795 |
||
796 |
||
14
by Paul Larson
Add support for xenial |
797 |
def _git_update_requirements(venv, package_dir, reqs_dir): |
798 |
"""
|
|
799 |
Update from global requirements.
|
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
800 |
|
14
by Paul Larson
Add support for xenial |
801 |
Update an OpenStack git directory's requirements.txt and
|
802 |
test-requirements.txt from global-requirements.txt.
|
|
803 |
"""
|
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
804 |
orig_dir = os.getcwd() |
805 |
os.chdir(reqs_dir) |
|
14
by Paul Larson
Add support for xenial |
806 |
python = os.path.join(venv, 'bin/python') |
807 |
cmd = [python, 'update.py', package_dir] |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
808 |
try: |
14
by Paul Larson
Add support for xenial |
809 |
subprocess.check_call(cmd) |
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
810 |
except subprocess.CalledProcessError: |
811 |
package = os.path.basename(package_dir) |
|
14
by Paul Larson
Add support for xenial |
812 |
error_out("Error updating {} from " |
813 |
"global-requirements.txt".format(package)) |
|
1
by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI |
814 |
os.chdir(orig_dir) |
14
by Paul Larson
Add support for xenial |
815 |
|
816 |
||
817 |
def git_pip_venv_dir(projects_yaml): |
|
818 |
"""
|
|
819 |
Return the pip virtualenv path.
|
|
820 |
"""
|
|
821 |
parent_dir = '/mnt/openstack-git' |
|
822 |
||
823 |
projects = _git_yaml_load(projects_yaml) |
|
824 |
||
825 |
if 'directory' in projects.keys(): |
|
826 |
parent_dir = projects['directory'] |
|
827 |
||
828 |
return os.path.join(parent_dir, 'venv') |
|
829 |
||
830 |
||
831 |
def git_src_dir(projects_yaml, project): |
|
832 |
"""
|
|
833 |
Return the directory where the specified project's source is located.
|
|
834 |
"""
|
|
835 |
parent_dir = '/mnt/openstack-git' |
|
836 |
||
837 |
projects = _git_yaml_load(projects_yaml) |
|
838 |
||
839 |
if 'directory' in projects.keys(): |
|
840 |
parent_dir = projects['directory'] |
|
841 |
||
842 |
for p in projects['repositories']: |
|
843 |
if p['name'] == project: |
|
844 |
return os.path.join(parent_dir, os.path.basename(p['repository'])) |
|
845 |
||
846 |
return None |
|
847 |
||
848 |
||
849 |
def git_yaml_value(projects_yaml, key): |
|
850 |
"""
|
|
851 |
Return the value in projects_yaml for the specified key.
|
|
852 |
"""
|
|
853 |
projects = _git_yaml_load(projects_yaml) |
|
854 |
||
855 |
if key in projects.keys(): |
|
856 |
return projects[key] |
|
857 |
||
858 |
return None |
|
859 |
||
860 |
||
861 |
def os_workload_status(configs, required_interfaces, charm_func=None): |
|
862 |
"""
|
|
863 |
Decorator to set workload status based on complete contexts
|
|
864 |
"""
|
|
865 |
def wrap(f): |
|
866 |
@wraps(f) |
|
867 |
def wrapped_f(*args, **kwargs): |
|
868 |
# Run the original function first
|
|
869 |
f(*args, **kwargs) |
|
870 |
# Set workload status now that contexts have been
|
|
871 |
# acted on
|
|
872 |
set_os_workload_status(configs, required_interfaces, charm_func) |
|
873 |
return wrapped_f |
|
874 |
return wrap |
|
875 |
||
876 |
||
877 |
def set_os_workload_status(configs, required_interfaces, charm_func=None, |
|
878 |
services=None, ports=None): |
|
879 |
"""Set the state of the workload status for the charm.
|
|
880 |
||
881 |
This calls _determine_os_workload_status() to get the new state, message
|
|
882 |
and sets the status using status_set()
|
|
883 |
||
884 |
@param configs: a templating.OSConfigRenderer() object
|
|
885 |
@param required_interfaces: {generic: [specific, specific2, ...]}
|
|
886 |
@param charm_func: a callable function that returns state, message. The
|
|
887 |
signature is charm_func(configs) -> (state, message)
|
|
888 |
@param services: list of strings OR dictionary specifying services/ports
|
|
889 |
@param ports: OPTIONAL list of port numbers.
|
|
890 |
@returns state, message: the new workload status, user message
|
|
891 |
"""
|
|
892 |
state, message = _determine_os_workload_status( |
|
893 |
configs, required_interfaces, charm_func, services, ports) |
|
894 |
status_set(state, message) |
|
895 |
||
896 |
||
897 |
def _determine_os_workload_status( |
|
898 |
configs, required_interfaces, charm_func=None, |
|
899 |
services=None, ports=None): |
|
900 |
"""Determine the state of the workload status for the charm.
|
|
901 |
||
902 |
This function returns the new workload status for the charm based
|
|
903 |
on the state of the interfaces, the paused state and whether the
|
|
904 |
services are actually running and any specified ports are open.
|
|
905 |
||
906 |
This checks:
|
|
907 |
||
908 |
1. if the unit should be paused, that it is actually paused. If so the
|
|
909 |
state is 'maintenance' + message, else 'broken'.
|
|
910 |
2. that the interfaces/relations are complete. If they are not then
|
|
911 |
it sets the state to either 'broken' or 'waiting' and an appropriate
|
|
912 |
message.
|
|
913 |
3. If all the relation data is set, then it checks that the actual
|
|
914 |
services really are running. If not it sets the state to 'broken'.
|
|
915 |
||
916 |
If everything is okay then the state returns 'active'.
|
|
917 |
||
918 |
@param configs: a templating.OSConfigRenderer() object
|
|
919 |
@param required_interfaces: {generic: [specific, specific2, ...]}
|
|
920 |
@param charm_func: a callable function that returns state, message. The
|
|
921 |
signature is charm_func(configs) -> (state, message)
|
|
922 |
@param services: list of strings OR dictionary specifying services/ports
|
|
923 |
@param ports: OPTIONAL list of port numbers.
|
|
924 |
@returns state, message: the new workload status, user message
|
|
925 |
"""
|
|
926 |
state, message = _ows_check_if_paused(services, ports) |
|
927 |
||
928 |
if state is None: |
|
929 |
state, message = _ows_check_generic_interfaces( |
|
930 |
configs, required_interfaces) |
|
931 |
||
932 |
if state != 'maintenance' and charm_func: |
|
933 |
# _ows_check_charm_func() may modify the state, message
|
|
934 |
state, message = _ows_check_charm_func( |
|
935 |
state, message, lambda: charm_func(configs)) |
|
936 |
||
937 |
if state is None: |
|
938 |
state, message = _ows_check_services_running(services, ports) |
|
939 |
||
940 |
if state is None: |
|
941 |
state = 'active' |
|
942 |
message = "Unit is ready" |
|
943 |
juju_log(message, 'INFO') |
|
944 |
||
945 |
return state, message |
|
946 |
||
947 |
||
948 |
def _ows_check_if_paused(services=None, ports=None): |
|
949 |
"""Check if the unit is supposed to be paused, and if so check that the
|
|
950 |
services/ports (if passed) are actually stopped/not being listened to.
|
|
951 |
||
952 |
if the unit isn't supposed to be paused, just return None, None
|
|
953 |
||
954 |
@param services: OPTIONAL services spec or list of service names.
|
|
955 |
@param ports: OPTIONAL list of port numbers.
|
|
956 |
@returns state, message or None, None
|
|
957 |
"""
|
|
958 |
if is_unit_paused_set(): |
|
959 |
state, message = check_actually_paused(services=services, |
|
960 |
ports=ports) |
|
961 |
if state is None: |
|
962 |
# we're paused okay, so set maintenance and return
|
|
963 |
state = "maintenance" |
|
964 |
message = "Paused. Use 'resume' action to resume normal service." |
|
965 |
return state, message |
|
966 |
return None, None |
|
967 |
||
968 |
||
969 |
def _ows_check_generic_interfaces(configs, required_interfaces): |
|
970 |
"""Check the complete contexts to determine the workload status.
|
|
971 |
||
972 |
- Checks for missing or incomplete contexts
|
|
973 |
- juju log details of missing required data.
|
|
974 |
- determines the correct workload status
|
|
975 |
- creates an appropriate message for status_set(...)
|
|
976 |
||
977 |
if there are no problems then the function returns None, None
|
|
978 |
||
979 |
@param configs: a templating.OSConfigRenderer() object
|
|
980 |
@params required_interfaces: {generic_interface: [specific_interface], }
|
|
981 |
@returns state, message or None, None
|
|
982 |
"""
|
|
983 |
incomplete_rel_data = incomplete_relation_data(configs, |
|
984 |
required_interfaces) |
|
985 |
state = None |
|
986 |
message = None |
|
987 |
missing_relations = set() |
|
988 |
incomplete_relations = set() |
|
989 |
||
990 |
for generic_interface, relations_states in incomplete_rel_data.items(): |
|
991 |
related_interface = None |
|
992 |
missing_data = {} |
|
993 |
# Related or not?
|
|
994 |
for interface, relation_state in relations_states.items(): |
|
995 |
if relation_state.get('related'): |
|
996 |
related_interface = interface |
|
997 |
missing_data = relation_state.get('missing_data') |
|
998 |
break
|
|
999 |
# No relation ID for the generic_interface?
|
|
1000 |
if not related_interface: |
|
1001 |
juju_log("{} relation is missing and must be related for " |
|
1002 |
"functionality. ".format(generic_interface), 'WARN') |
|
1003 |
state = 'blocked' |
|
1004 |
missing_relations.add(generic_interface) |
|
1005 |
else: |
|
1006 |
# Relation ID eists but no related unit
|
|
1007 |
if not missing_data: |
|
1008 |
# Edge case - relation ID exists but departings
|
|
1009 |
_hook_name = hook_name() |
|
1010 |
if (('departed' in _hook_name or 'broken' in _hook_name) and |
|
1011 |
related_interface in _hook_name): |
|
1012 |
state = 'blocked' |
|
1013 |
missing_relations.add(generic_interface) |
|
1014 |
juju_log("{} relation's interface, {}, " |
|
1015 |
"relationship is departed or broken "
|
|
1016 |
"and is required for functionality."
|
|
1017 |
"".format(generic_interface, related_interface), |
|
1018 |
"WARN") |
|
1019 |
# Normal case relation ID exists but no related unit
|
|
1020 |
# (joining)
|
|
1021 |
else: |
|
1022 |
juju_log("{} relations's interface, {}, is related but has" |
|
1023 |
" no units in the relation."
|
|
1024 |
"".format(generic_interface, related_interface), |
|
1025 |
"INFO") |
|
1026 |
# Related unit exists and data missing on the relation
|
|
1027 |
else: |
|
1028 |
juju_log("{} relation's interface, {}, is related awaiting " |
|
1029 |
"the following data from the relationship: {}. " |
|
1030 |
"".format(generic_interface, related_interface, |
|
1031 |
", ".join(missing_data)), "INFO") |
|
1032 |
if state != 'blocked': |
|
1033 |
state = 'waiting' |
|
1034 |
if generic_interface not in missing_relations: |
|
1035 |
incomplete_relations.add(generic_interface) |
|
1036 |
||
1037 |
if missing_relations: |
|
1038 |
message = "Missing relations: {}".format(", ".join(missing_relations)) |
|
1039 |
if incomplete_relations: |
|
1040 |
message += "; incomplete relations: {}" \ |
|
1041 |
"".format(", ".join(incomplete_relations)) |
|
1042 |
state = 'blocked' |
|
1043 |
elif incomplete_relations: |
|
1044 |
message = "Incomplete relations: {}" \ |
|
1045 |
"".format(", ".join(incomplete_relations)) |
|
1046 |
state = 'waiting' |
|
1047 |
||
1048 |
return state, message |
|
1049 |
||
1050 |
||
1051 |
def _ows_check_charm_func(state, message, charm_func_with_configs): |
|
1052 |
"""Run a custom check function for the charm to see if it wants to
|
|
1053 |
change the state. This is only run if not in 'maintenance' and
|
|
1054 |
tests to see if the new state is more important that the previous
|
|
1055 |
one determined by the interfaces/relations check.
|
|
1056 |
||
1057 |
@param state: the previously determined state so far.
|
|
1058 |
@param message: the user orientated message so far.
|
|
1059 |
@param charm_func: a callable function that returns state, message
|
|
1060 |
@returns state, message strings.
|
|
1061 |
"""
|
|
1062 |
if charm_func_with_configs: |
|
1063 |
charm_state, charm_message = charm_func_with_configs() |
|
1064 |
if charm_state != 'active' and charm_state != 'unknown': |
|
1065 |
state = workload_state_compare(state, charm_state) |
|
1066 |
if message: |
|
1067 |
charm_message = charm_message.replace("Incomplete relations: ", |
|
1068 |
"") |
|
1069 |
message = "{}, {}".format(message, charm_message) |
|
1070 |
else: |
|
1071 |
message = charm_message |
|
1072 |
return state, message |
|
1073 |
||
1074 |
||
1075 |
def _ows_check_services_running(services, ports): |
|
1076 |
"""Check that the services that should be running are actually running
|
|
1077 |
and that any ports specified are being listened to.
|
|
1078 |
||
1079 |
@param services: list of strings OR dictionary specifying services/ports
|
|
1080 |
@param ports: list of ports
|
|
1081 |
@returns state, message: strings or None, None
|
|
1082 |
"""
|
|
1083 |
messages = [] |
|
1084 |
state = None |
|
1085 |
if services is not None: |
|
1086 |
services = _extract_services_list_helper(services) |
|
1087 |
services_running, running = _check_running_services(services) |
|
1088 |
if not all(running): |
|
1089 |
messages.append( |
|
1090 |
"Services not running that should be: {}" |
|
1091 |
.format(", ".join(_filter_tuples(services_running, False)))) |
|
1092 |
state = 'blocked' |
|
1093 |
# also verify that the ports that should be open are open
|
|
1094 |
# NB, that ServiceManager objects only OPTIONALLY have ports
|
|
1095 |
map_not_open, ports_open = ( |
|
1096 |
_check_listening_on_services_ports(services)) |
|
1097 |
if not all(ports_open): |
|
1098 |
# find which service has missing ports. They are in service
|
|
1099 |
# order which makes it a bit easier.
|
|
1100 |
message_parts = {service: ", ".join([str(v) for v in open_ports]) |
|
1101 |
for service, open_ports in map_not_open.items()} |
|
1102 |
message = ", ".join( |
|
1103 |
["{}: [{}]".format(s, sp) for s, sp in message_parts.items()]) |
|
1104 |
messages.append( |
|
1105 |
"Services with ports not open that should be: {}" |
|
1106 |
.format(message)) |
|
1107 |
state = 'blocked' |
|
1108 |
||
1109 |
if ports is not None: |
|
1110 |
# and we can also check ports which we don't know the service for
|
|
1111 |
ports_open, ports_open_bools = _check_listening_on_ports_list(ports) |
|
1112 |
if not all(ports_open_bools): |
|
1113 |
messages.append( |
|
1114 |
"Ports which should be open, but are not: {}" |
|
1115 |
.format(", ".join([str(p) for p, v in ports_open |
|
1116 |
if not v]))) |
|
1117 |
state = 'blocked' |
|
1118 |
||
1119 |
if state is not None: |
|
1120 |
message = "; ".join(messages) |
|
1121 |
return state, message |
|
1122 |
||
1123 |
return None, None |
|
1124 |
||
1125 |
||
1126 |
def _extract_services_list_helper(services): |
|
1127 |
"""Extract a OrderedDict of {service: [ports]} of the supplied services
|
|
1128 |
for use by the other functions.
|
|
1129 |
||
1130 |
The services object can either be:
|
|
1131 |
- None : no services were passed (an empty dict is returned)
|
|
1132 |
- a list of strings
|
|
1133 |
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
|
|
1134 |
- An array of [{'service': service_name, ...}, ...]
|
|
1135 |
||
1136 |
@param services: see above
|
|
1137 |
@returns OrderedDict(service: [ports], ...)
|
|
1138 |
"""
|
|
1139 |
if services is None: |
|
1140 |
return {} |
|
1141 |
if isinstance(services, dict): |
|
1142 |
services = services.values() |
|
1143 |
# either extract the list of services from the dictionary, or if
|
|
1144 |
# it is a simple string, use that. i.e. works with mixed lists.
|
|
1145 |
_s = OrderedDict() |
|
1146 |
for s in services: |
|
1147 |
if isinstance(s, dict) and 'service' in s: |
|
1148 |
_s[s['service']] = s.get('ports', []) |
|
1149 |
if isinstance(s, str): |
|
1150 |
_s[s] = [] |
|
1151 |
return _s |
|
1152 |
||
1153 |
||
1154 |
def _check_running_services(services): |
|
1155 |
"""Check that the services dict provided is actually running and provide
|
|
1156 |
a list of (service, boolean) tuples for each service.
|
|
1157 |
||
1158 |
Returns both a zipped list of (service, boolean) and a list of booleans
|
|
1159 |
in the same order as the services.
|
|
1160 |
||
1161 |
@param services: OrderedDict of strings: [ports], one for each service to
|
|
1162 |
check.
|
|
1163 |
@returns [(service, boolean), ...], : results for checks
|
|
1164 |
[boolean] : just the result of the service checks
|
|
1165 |
"""
|
|
1166 |
services_running = [service_running(s) for s in services] |
|
1167 |
return list(zip(services, services_running)), services_running |
|
1168 |
||
1169 |
||
1170 |
def _check_listening_on_services_ports(services, test=False): |
|
1171 |
"""Check that the unit is actually listening (has the port open) on the
|
|
1172 |
ports that the service specifies are open. If test is True then the
|
|
1173 |
function returns the services with ports that are open rather than
|
|
1174 |
closed.
|
|
1175 |
||
1176 |
Returns an OrderedDict of service: ports and a list of booleans
|
|
1177 |
||
1178 |
@param services: OrderedDict(service: [port, ...], ...)
|
|
1179 |
@param test: default=False, if False, test for closed, otherwise open.
|
|
1180 |
@returns OrderedDict(service: [port-not-open, ...]...), [boolean]
|
|
1181 |
"""
|
|
1182 |
test = not(not(test)) # ensure test is True or False |
|
1183 |
all_ports = list(itertools.chain(*services.values())) |
|
1184 |
ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports] |
|
1185 |
map_ports = OrderedDict() |
|
1186 |
matched_ports = [p for p, opened in zip(all_ports, ports_states) |
|
1187 |
if opened == test] # essentially opened xor test |
|
1188 |
for service, ports in services.items(): |
|
1189 |
set_ports = set(ports).intersection(matched_ports) |
|
1190 |
if set_ports: |
|
1191 |
map_ports[service] = set_ports |
|
1192 |
return map_ports, ports_states |
|
1193 |
||
1194 |
||
1195 |
def _check_listening_on_ports_list(ports): |
|
1196 |
"""Check that the ports list given are being listened to
|
|
1197 |
||
1198 |
Returns a list of ports being listened to and a list of the
|
|
1199 |
booleans.
|
|
1200 |
||
1201 |
@param ports: LIST or port numbers.
|
|
1202 |
@returns [(port_num, boolean), ...], [boolean]
|
|
1203 |
"""
|
|
1204 |
ports_open = [port_has_listener('0.0.0.0', p) for p in ports] |
|
1205 |
return zip(ports, ports_open), ports_open |
|
1206 |
||
1207 |
||
1208 |
def _filter_tuples(services_states, state): |
|
1209 |
"""Return a simple list from a list of tuples according to the condition
|
|
1210 |
||
1211 |
@param services_states: LIST of (string, boolean): service and running
|
|
1212 |
state.
|
|
1213 |
@param state: Boolean to match the tuple against.
|
|
1214 |
@returns [LIST of strings] that matched the tuple RHS.
|
|
1215 |
"""
|
|
1216 |
return [s for s, b in services_states if b == state] |
|
1217 |
||
1218 |
||
1219 |
def workload_state_compare(current_workload_state, workload_state): |
|
1220 |
""" Return highest priority of two states"""
|
|
1221 |
hierarchy = {'unknown': -1, |
|
1222 |
'active': 0, |
|
1223 |
'maintenance': 1, |
|
1224 |
'waiting': 2, |
|
1225 |
'blocked': 3, |
|
1226 |
}
|
|
1227 |
||
1228 |
if hierarchy.get(workload_state) is None: |
|
1229 |
workload_state = 'unknown' |
|
1230 |
if hierarchy.get(current_workload_state) is None: |
|
1231 |
current_workload_state = 'unknown' |
|
1232 |
||
1233 |
# Set workload_state based on hierarchy of statuses
|
|
1234 |
if hierarchy.get(current_workload_state) > hierarchy.get(workload_state): |
|
1235 |
return current_workload_state |
|
1236 |
else: |
|
1237 |
return workload_state |
|
1238 |
||
1239 |
||
1240 |
def incomplete_relation_data(configs, required_interfaces): |
|
1241 |
"""Check complete contexts against required_interfaces
|
|
1242 |
Return dictionary of incomplete relation data.
|
|
1243 |
||
1244 |
configs is an OSConfigRenderer object with configs registered
|
|
1245 |
||
1246 |
required_interfaces is a dictionary of required general interfaces
|
|
1247 |
with dictionary values of possible specific interfaces.
|
|
1248 |
Example:
|
|
1249 |
required_interfaces = {'database': ['shared-db', 'pgsql-db']}
|
|
1250 |
||
1251 |
The interface is said to be satisfied if anyone of the interfaces in the
|
|
1252 |
list has a complete context.
|
|
1253 |
||
1254 |
Return dictionary of incomplete or missing required contexts with relation
|
|
1255 |
status of interfaces and any missing data points. Example:
|
|
1256 |
{'message':
|
|
1257 |
{'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
|
|
1258 |
'zeromq-configuration': {'related': False}},
|
|
1259 |
'identity':
|
|
1260 |
{'identity-service': {'related': False}},
|
|
1261 |
'database':
|
|
1262 |
{'pgsql-db': {'related': False},
|
|
1263 |
'shared-db': {'related': True}}}
|
|
1264 |
"""
|
|
1265 |
complete_ctxts = configs.complete_contexts() |
|
1266 |
incomplete_relations = [ |
|
1267 |
svc_type
|
|
1268 |
for svc_type, interfaces in required_interfaces.items() |
|
1269 |
if not set(interfaces).intersection(complete_ctxts)] |
|
1270 |
return { |
|
1271 |
i: configs.get_incomplete_context_data(required_interfaces[i]) |
|
1272 |
for i in incomplete_relations} |
|
1273 |
||
1274 |
||
1275 |
def do_action_openstack_upgrade(package, upgrade_callback, configs): |
|
1276 |
"""Perform action-managed OpenStack upgrade.
|
|
1277 |
||
1278 |
Upgrades packages to the configured openstack-origin version and sets
|
|
1279 |
the corresponding action status as a result.
|
|
1280 |
||
1281 |
If the charm was installed from source we cannot upgrade it.
|
|
1282 |
For backwards compatibility a config flag (action-managed-upgrade) must
|
|
1283 |
be set for this code to run, otherwise a full service level upgrade will
|
|
1284 |
fire on config-changed.
|
|
1285 |
||
1286 |
@param package: package name for determining if upgrade available
|
|
1287 |
@param upgrade_callback: function callback to charm's upgrade function
|
|
1288 |
@param configs: templating object derived from OSConfigRenderer class
|
|
1289 |
||
1290 |
@return: True if upgrade successful; False if upgrade failed or skipped
|
|
1291 |
"""
|
|
1292 |
ret = False |
|
1293 |
||
1294 |
if git_install_requested(): |
|
1295 |
action_set({'outcome': 'installed from source, skipped upgrade.'}) |
|
1296 |
else: |
|
1297 |
if openstack_upgrade_available(package): |
|
1298 |
if config('action-managed-upgrade'): |
|
1299 |
juju_log('Upgrading OpenStack release') |
|
1300 |
||
1301 |
try: |
|
1302 |
upgrade_callback(configs=configs) |
|
1303 |
action_set({'outcome': 'success, upgrade completed.'}) |
|
1304 |
ret = True |
|
1305 |
except: |
|
1306 |
action_set({'outcome': 'upgrade failed, see traceback.'}) |
|
1307 |
action_set({'traceback': traceback.format_exc()}) |
|
1308 |
action_fail('do_openstack_upgrade resulted in an ' |
|
1309 |
'unexpected error') |
|
1310 |
else: |
|
1311 |
action_set({'outcome': 'action-managed-upgrade config is ' |
|
1312 |
'False, skipped upgrade.'}) |
|
1313 |
else: |
|
1314 |
action_set({'outcome': 'no upgrade available.'}) |
|
1315 |
||
1316 |
return ret |
|
1317 |
||
1318 |
||
1319 |
def remote_restart(rel_name, remote_service=None): |
|
1320 |
trigger = { |
|
1321 |
'restart-trigger': str(uuid.uuid4()), |
|
1322 |
}
|
|
1323 |
if remote_service: |
|
1324 |
trigger['remote-service'] = remote_service |
|
1325 |
for rid in relation_ids(rel_name): |
|
1326 |
# This subordinate can be related to two seperate services using
|
|
1327 |
# different subordinate relations so only issue the restart if
|
|
1328 |
# the principle is conencted down the relation we think it is
|
|
1329 |
if related_units(relid=rid): |
|
1330 |
relation_set(relation_id=rid, |
|
1331 |
relation_settings=trigger, |
|
1332 |
)
|
|
1333 |
||
1334 |
||
1335 |
def check_actually_paused(services=None, ports=None): |
|
1336 |
"""Check that services listed in the services object and and ports
|
|
1337 |
are actually closed (not listened to), to verify that the unit is
|
|
1338 |
properly paused.
|
|
1339 |
||
1340 |
@param services: See _extract_services_list_helper
|
|
1341 |
@returns status, : string for status (None if okay)
|
|
1342 |
message : string for problem for status_set
|
|
1343 |
"""
|
|
1344 |
state = None |
|
1345 |
message = None |
|
1346 |
messages = [] |
|
1347 |
if services is not None: |
|
1348 |
services = _extract_services_list_helper(services) |
|
1349 |
services_running, services_states = _check_running_services(services) |
|
1350 |
if any(services_states): |
|
1351 |
# there shouldn't be any running so this is a problem
|
|
1352 |
messages.append("these services running: {}" |
|
1353 |
.format(", ".join( |
|
1354 |
_filter_tuples(services_running, True)))) |
|
1355 |
state = "blocked" |
|
1356 |
ports_open, ports_open_bools = ( |
|
1357 |
_check_listening_on_services_ports(services, True)) |
|
1358 |
if any(ports_open_bools): |
|
1359 |
message_parts = {service: ", ".join([str(v) for v in open_ports]) |
|
1360 |
for service, open_ports in ports_open.items()} |
|
1361 |
message = ", ".join( |
|
1362 |
["{}: [{}]".format(s, sp) for s, sp in message_parts.items()]) |
|
1363 |
messages.append( |
|
1364 |
"these service:ports are open: {}".format(message)) |
|
1365 |
state = 'blocked' |
|
1366 |
if ports is not None: |
|
1367 |
ports_open, bools = _check_listening_on_ports_list(ports) |
|
1368 |
if any(bools): |
|
1369 |
messages.append( |
|
1370 |
"these ports which should be closed, but are open: {}" |
|
1371 |
.format(", ".join([str(p) for p, v in ports_open if v]))) |
|
1372 |
state = 'blocked' |
|
1373 |
if messages: |
|
1374 |
message = ("Services should be paused but {}" |
|
1375 |
.format(", ".join(messages))) |
|
1376 |
return state, message |
|
1377 |
||
1378 |
||
1379 |
def set_unit_paused(): |
|
1380 |
"""Set the unit to a paused state in the local kv() store.
|
|
1381 |
This does NOT actually pause the unit
|
|
1382 |
"""
|
|
1383 |
with unitdata.HookData()() as t: |
|
1384 |
kv = t[0] |
|
1385 |
kv.set('unit-paused', True) |
|
1386 |
||
1387 |
||
1388 |
def clear_unit_paused(): |
|
1389 |
"""Clear the unit from a paused state in the local kv() store
|
|
1390 |
This does NOT actually restart any services - it only clears the
|
|
1391 |
local state.
|
|
1392 |
"""
|
|
1393 |
with unitdata.HookData()() as t: |
|
1394 |
kv = t[0] |
|
1395 |
kv.set('unit-paused', False) |
|
1396 |
||
1397 |
||
1398 |
def is_unit_paused_set(): |
|
1399 |
"""Return the state of the kv().get('unit-paused').
|
|
1400 |
This does NOT verify that the unit really is paused.
|
|
1401 |
||
1402 |
To help with units that don't have HookData() (testing)
|
|
1403 |
if it excepts, return False
|
|
1404 |
"""
|
|
1405 |
try: |
|
1406 |
with unitdata.HookData()() as t: |
|
1407 |
kv = t[0] |
|
1408 |
# transform something truth-y into a Boolean.
|
|
1409 |
return not(not(kv.get('unit-paused'))) |
|
1410 |
except: |
|
1411 |
return False |
|
1412 |
||
1413 |
||
1414 |
def pause_unit(assess_status_func, services=None, ports=None, |
|
1415 |
charm_func=None): |
|
1416 |
"""Pause a unit by stopping the services and setting 'unit-paused'
|
|
1417 |
in the local kv() store.
|
|
1418 |
||
1419 |
Also checks that the services have stopped and ports are no longer
|
|
1420 |
being listened to.
|
|
1421 |
||
1422 |
An optional charm_func() can be called that can either raise an
|
|
1423 |
Exception or return non None, None to indicate that the unit
|
|
1424 |
didn't pause cleanly.
|
|
1425 |
||
1426 |
The signature for charm_func is:
|
|
1427 |
charm_func() -> message: string
|
|
1428 |
||
1429 |
charm_func() is executed after any services are stopped, if supplied.
|
|
1430 |
||
1431 |
The services object can either be:
|
|
1432 |
- None : no services were passed (an empty dict is returned)
|
|
1433 |
- a list of strings
|
|
1434 |
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
|
|
1435 |
- An array of [{'service': service_name, ...}, ...]
|
|
1436 |
||
1437 |
@param assess_status_func: (f() -> message: string | None) or None
|
|
1438 |
@param services: OPTIONAL see above
|
|
1439 |
@param ports: OPTIONAL list of port
|
|
1440 |
@param charm_func: function to run for custom charm pausing.
|
|
1441 |
@returns None
|
|
1442 |
@raises Exception(message) on an error for action_fail().
|
|
1443 |
"""
|
|
1444 |
services = _extract_services_list_helper(services) |
|
1445 |
messages = [] |
|
1446 |
if services: |
|
1447 |
for service in services.keys(): |
|
1448 |
stopped = service_pause(service) |
|
1449 |
if not stopped: |
|
1450 |
messages.append("{} didn't stop cleanly.".format(service)) |
|
1451 |
if charm_func: |
|
1452 |
try: |
|
1453 |
message = charm_func() |
|
1454 |
if message: |
|
1455 |
messages.append(message) |
|
1456 |
except Exception as e: |
|
1457 |
message.append(str(e)) |
|
1458 |
set_unit_paused() |
|
1459 |
if assess_status_func: |
|
1460 |
message = assess_status_func() |
|
1461 |
if message: |
|
1462 |
messages.append(message) |
|
1463 |
if messages: |
|
1464 |
raise Exception("Couldn't pause: {}".format("; ".join(messages))) |
|
1465 |
||
1466 |
||
1467 |
def resume_unit(assess_status_func, services=None, ports=None, |
|
1468 |
charm_func=None): |
|
1469 |
"""Resume a unit by starting the services and clearning 'unit-paused'
|
|
1470 |
in the local kv() store.
|
|
1471 |
||
1472 |
Also checks that the services have started and ports are being listened to.
|
|
1473 |
||
1474 |
An optional charm_func() can be called that can either raise an
|
|
1475 |
Exception or return non None to indicate that the unit
|
|
1476 |
didn't resume cleanly.
|
|
1477 |
||
1478 |
The signature for charm_func is:
|
|
1479 |
charm_func() -> message: string
|
|
1480 |
||
1481 |
charm_func() is executed after any services are started, if supplied.
|
|
1482 |
||
1483 |
The services object can either be:
|
|
1484 |
- None : no services were passed (an empty dict is returned)
|
|
1485 |
- a list of strings
|
|
1486 |
- A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
|
|
1487 |
- An array of [{'service': service_name, ...}, ...]
|
|
1488 |
||
1489 |
@param assess_status_func: (f() -> message: string | None) or None
|
|
1490 |
@param services: OPTIONAL see above
|
|
1491 |
@param ports: OPTIONAL list of port
|
|
1492 |
@param charm_func: function to run for custom charm resuming.
|
|
1493 |
@returns None
|
|
1494 |
@raises Exception(message) on an error for action_fail().
|
|
1495 |
"""
|
|
1496 |
services = _extract_services_list_helper(services) |
|
1497 |
messages = [] |
|
1498 |
if services: |
|
1499 |
for service in services.keys(): |
|
1500 |
started = service_resume(service) |
|
1501 |
if not started: |
|
1502 |
messages.append("{} didn't start cleanly.".format(service)) |
|
1503 |
if charm_func: |
|
1504 |
try: |
|
1505 |
message = charm_func() |
|
1506 |
if message: |
|
1507 |
messages.append(message) |
|
1508 |
except Exception as e: |
|
1509 |
message.append(str(e)) |
|
1510 |
clear_unit_paused() |
|
1511 |
if assess_status_func: |
|
1512 |
message = assess_status_func() |
|
1513 |
if message: |
|
1514 |
messages.append(message) |
|
1515 |
if messages: |
|
1516 |
raise Exception("Couldn't resume: {}".format("; ".join(messages))) |
|
1517 |
||
1518 |
||
1519 |
def make_assess_status_func(*args, **kwargs): |
|
1520 |
"""Creates an assess_status_func() suitable for handing to pause_unit()
|
|
1521 |
and resume_unit().
|
|
1522 |
||
1523 |
This uses the _determine_os_workload_status(...) function to determine
|
|
1524 |
what the workload_status should be for the unit. If the unit is
|
|
1525 |
not in maintenance or active states, then the message is returned to
|
|
1526 |
the caller. This is so an action that doesn't result in either a
|
|
1527 |
complete pause or complete resume can signal failure with an action_fail()
|
|
1528 |
"""
|
|
1529 |
def _assess_status_func(): |
|
1530 |
state, message = _determine_os_workload_status(*args, **kwargs) |
|
1531 |
status_set(state, message) |
|
1532 |
if state not in ['maintenance', 'active']: |
|
1533 |
return message |
|
1534 |
return None |
|
1535 |
||
1536 |
return _assess_status_func |
|
1537 |
||
1538 |
||
1539 |
def pausable_restart_on_change(restart_map, stopstart=False, |
|
1540 |
restart_functions=None): |
|
1541 |
"""A restart_on_change decorator that checks to see if the unit is
|
|
1542 |
paused. If it is paused then the decorated function doesn't fire.
|
|
1543 |
||
1544 |
This is provided as a helper, as the @restart_on_change(...) decorator
|
|
1545 |
is in core.host, yet the openstack specific helpers are in this file
|
|
1546 |
(contrib.openstack.utils). Thus, this needs to be an optional feature
|
|
1547 |
for openstack charms (or charms that wish to use the openstack
|
|
1548 |
pause/resume type features).
|
|
1549 |
||
1550 |
It is used as follows:
|
|
1551 |
||
1552 |
from contrib.openstack.utils import (
|
|
1553 |
pausable_restart_on_change as restart_on_change)
|
|
1554 |
||
1555 |
@restart_on_change(restart_map, stopstart=<boolean>)
|
|
1556 |
def some_hook(...):
|
|
1557 |
pass
|
|
1558 |
||
1559 |
see core.utils.restart_on_change() for more details.
|
|
1560 |
||
1561 |
@param f: the function to decorate
|
|
1562 |
@param restart_map: the restart map {conf_file: [services]}
|
|
1563 |
@param stopstart: DEFAULT false; whether to stop, start or just restart
|
|
1564 |
@returns decorator to use a restart_on_change with pausability
|
|
1565 |
"""
|
|
1566 |
def wrap(f): |
|
1567 |
@functools.wraps(f) |
|
1568 |
def wrapped_f(*args, **kwargs): |
|
1569 |
if is_unit_paused_set(): |
|
1570 |
return f(*args, **kwargs) |
|
1571 |
# otherwise, normal restart_on_change functionality
|
|
1572 |
return restart_on_change_helper( |
|
1573 |
(lambda: f(*args, **kwargs)), restart_map, stopstart, |
|
1574 |
restart_functions) |
|
1575 |
return wrapped_f |
|
1576 |
return wrap |