Merge lp:~xavpaice/charms/trusty/thruk-agent/trunk into lp:~canonical-bootstack/charms/trusty/thruk-agent/trunk

Proposed by Xav Paice
Status: Merged
Approved by: James Troup
Approved revision: 35
Merged at revision: 35
Proposed branch: lp:~xavpaice/charms/trusty/thruk-agent/trunk
Merge into: lp:~canonical-bootstack/charms/trusty/thruk-agent/trunk
Diff against target: 4790 lines (+3050/-815)
33 files modified
charm-helpers.yaml (+1/-0)
config.yaml (+18/-4)
hooks/actions.py (+17/-0)
hooks/charmhelpers/__init__.py (+72/-13)
hooks/charmhelpers/core/__init__.py (+11/-13)
hooks/charmhelpers/core/decorators.py (+11/-13)
hooks/charmhelpers/core/files.py (+43/-0)
hooks/charmhelpers/core/fstab.py (+11/-13)
hooks/charmhelpers/core/hookenv.py (+530/-56)
hooks/charmhelpers/core/host.py (+629/-155)
hooks/charmhelpers/core/host_factory/centos.py (+72/-0)
hooks/charmhelpers/core/host_factory/ubuntu.py (+89/-0)
hooks/charmhelpers/core/hugepage.py (+69/-0)
hooks/charmhelpers/core/kernel.py (+72/-0)
hooks/charmhelpers/core/kernel_factory/centos.py (+17/-0)
hooks/charmhelpers/core/kernel_factory/ubuntu.py (+13/-0)
hooks/charmhelpers/core/services/__init__.py (+11/-13)
hooks/charmhelpers/core/services/base.py (+54/-32)
hooks/charmhelpers/core/services/helpers.py (+42/-19)
hooks/charmhelpers/core/strutils.py (+96/-15)
hooks/charmhelpers/core/sysctl.py (+11/-13)
hooks/charmhelpers/core/templating.py (+40/-24)
hooks/charmhelpers/core/unitdata.py (+72/-31)
hooks/charmhelpers/fetch/__init__.py (+54/-288)
hooks/charmhelpers/fetch/archiveurl.py (+19/-15)
hooks/charmhelpers/fetch/bzrurl.py (+48/-50)
hooks/charmhelpers/fetch/centos.py (+171/-0)
hooks/charmhelpers/fetch/giturl.py (+37/-39)
hooks/charmhelpers/fetch/snap.py (+122/-0)
hooks/charmhelpers/fetch/ubuntu.py (+568/-0)
hooks/charmhelpers/osplatform.py (+25/-0)
hooks/install (+4/-9)
hooks/services.py (+1/-0)
To merge this branch: bzr merge lp:~xavpaice/charms/trusty/thruk-agent/trunk
Reviewer Review Type Date Requested Status
James Troup (community) Approve
Review via email: mp+327579@code.launchpad.net
To post a comment you must log in.
Revision history for this message
James Troup (elmo) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charm-helpers.yaml'
2--- charm-helpers.yaml 2015-04-08 05:57:06 +0000
3+++ charm-helpers.yaml 2017-07-18 06:08:45 +0000
4@@ -3,3 +3,4 @@
5 include:
6 - core
7 - fetch
8+ - osplatform
9
10=== modified file 'config.yaml'
11--- config.yaml 2015-06-15 00:19:17 +0000
12+++ config.yaml 2017-07-18 06:08:45 +0000
13@@ -3,10 +3,6 @@
14 type: string
15 default: "ppa:canonical-bootstack/thruk"
16 description: "PPA to install thruk"
17- nagios_context:
18- type: string
19- default: "bootstack"
20- description: "Name for the thruk instance"
21 livestatus_path:
22 type: string
23 default: "/var/lib/nagios3/livestatus/socket"
24@@ -24,4 +20,22 @@
25
26 If you're running multiple environments with the same services in them
27 this allows you to differentiate between them.
28+ source:
29+ type: string
30+ default: "http://labs.consol.de/repo/stable/ubuntu xenial main"
31+ description: |
32+ Optional configuration to support use of additional sources such as:
33+
34+ - ppa:myteam/ppa
35+ - cloud:trusty-proposed/kilo
36+ - http://my.archive.com/ubuntu main
37+
38+ The last option should be used in conjunction with the key configuration
39+ option.
40+ key:
41+ type: string
42+ default: F8C1CA08A57B9ED7
43+ description: |
44+ Key ID to import to the apt keyring to support use with arbitary source
45+ configuration from outside of Launchpad archives or PPA's.
46
47
48=== modified file 'hooks/actions.py'
49--- hooks/actions.py 2015-08-25 22:59:55 +0000
50+++ hooks/actions.py 2017-07-18 06:08:45 +0000
51@@ -8,6 +8,10 @@
52 import hashlib
53 # import thruk_helpers
54
55+from charmhelpers.fetch import (
56+ apt_install, apt_update, add_source
57+)
58+
59
60 def log_start(service_name):
61 hookenv.log('thruk-agent starting')
62@@ -81,3 +85,16 @@
63
64 for rel_id in hookenv.relation_ids('thruk-agent'):
65 hookenv.relation_set(relation_id=rel_id, relation_settings=thruk_data)
66+
67+
68+def update_ppa(service_name):
69+ config = hookenv.config()
70+ new_source = config.get('source')
71+ prev_source = config.previous('source')
72+ if prev_source is not None and prev_source != new_source:
73+ subprocess.check_call(['add-apt-repository',
74+ '--yes', '--remove', prev_source])
75+ add_source(config.get('source'), config.get('key', None))
76+ apt_update(fatal=True)
77+ package_list = ["thruk", "pwgen", "apache2-utils"]
78+ apt_install(packages=package_list, fatal=True)
79
80=== modified file 'hooks/charmhelpers/__init__.py'
81--- hooks/charmhelpers/__init__.py 2015-04-08 05:57:06 +0000
82+++ hooks/charmhelpers/__init__.py 2017-07-18 06:08:45 +0000
83@@ -1,21 +1,24 @@
84 # Copyright 2014-2015 Canonical Limited.
85 #
86-# This file is part of charm-helpers.
87-#
88-# charm-helpers is free software: you can redistribute it and/or modify
89-# it under the terms of the GNU Lesser General Public License version 3 as
90-# published by the Free Software Foundation.
91-#
92-# charm-helpers is distributed in the hope that it will be useful,
93-# but WITHOUT ANY WARRANTY; without even the implied warranty of
94-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
95-# GNU Lesser General Public License for more details.
96-#
97-# You should have received a copy of the GNU Lesser General Public License
98-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
99+# Licensed under the Apache License, Version 2.0 (the "License");
100+# you may not use this file except in compliance with the License.
101+# You may obtain a copy of the License at
102+#
103+# http://www.apache.org/licenses/LICENSE-2.0
104+#
105+# Unless required by applicable law or agreed to in writing, software
106+# distributed under the License is distributed on an "AS IS" BASIS,
107+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
108+# See the License for the specific language governing permissions and
109+# limitations under the License.
110
111 # Bootstrap charm-helpers, installing its dependencies if necessary using
112 # only standard libraries.
113+from __future__ import print_function
114+from __future__ import absolute_import
115+
116+import functools
117+import inspect
118 import subprocess
119 import sys
120
121@@ -36,3 +39,59 @@
122 else:
123 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
124 import yaml # flake8: noqa
125+
126+
127+# Holds a list of mapping of mangled function names that have been deprecated
128+# using the @deprecate decorator below. This is so that the warning is only
129+# printed once for each usage of the function.
130+__deprecated_functions = {}
131+
132+
133+def deprecate(warning, date=None, log=None):
134+ """Add a deprecation warning the first time the function is used.
135+ The date, which is a string in semi-ISO8660 format indicate the year-month
136+ that the function is officially going to be removed.
137+
138+ usage:
139+
140+ @deprecate('use core/fetch/add_source() instead', '2017-04')
141+ def contributed_add_source_thing(...):
142+ ...
143+
144+ And it then prints to the log ONCE that the function is deprecated.
145+ The reason for passing the logging function (log) is so that hookenv.log
146+ can be used for a charm if needed.
147+
148+ :param warning: String to indicat where it has moved ot.
149+ :param date: optional sting, in YYYY-MM format to indicate when the
150+ function will definitely (probably) be removed.
151+ :param log: The log function to call to log. If not, logs to stdout
152+ """
153+ def wrap(f):
154+
155+ @functools.wraps(f)
156+ def wrapped_f(*args, **kwargs):
157+ try:
158+ module = inspect.getmodule(f)
159+ file = inspect.getsourcefile(f)
160+ lines = inspect.getsourcelines(f)
161+ f_name = "{}-{}-{}..{}-{}".format(
162+ module.__name__, file, lines[0], lines[-1], f.__name__)
163+ except (IOError, TypeError):
164+ # assume it was local, so just use the name of the function
165+ f_name = f.__name__
166+ if f_name not in __deprecated_functions:
167+ __deprecated_functions[f_name] = True
168+ s = "DEPRECATION WARNING: Function {} is being removed".format(
169+ f.__name__)
170+ if date:
171+ s = "{} on/around {}".format(s, date)
172+ if warning:
173+ s = "{} : {}".format(s, warning)
174+ if log:
175+ log(s)
176+ else:
177+ print(s)
178+ return f(*args, **kwargs)
179+ return wrapped_f
180+ return wrap
181
182=== modified file 'hooks/charmhelpers/core/__init__.py'
183--- hooks/charmhelpers/core/__init__.py 2015-04-08 05:57:06 +0000
184+++ hooks/charmhelpers/core/__init__.py 2017-07-18 06:08:45 +0000
185@@ -1,15 +1,13 @@
186 # Copyright 2014-2015 Canonical Limited.
187 #
188-# This file is part of charm-helpers.
189-#
190-# charm-helpers is free software: you can redistribute it and/or modify
191-# it under the terms of the GNU Lesser General Public License version 3 as
192-# published by the Free Software Foundation.
193-#
194-# charm-helpers is distributed in the hope that it will be useful,
195-# but WITHOUT ANY WARRANTY; without even the implied warranty of
196-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
197-# GNU Lesser General Public License for more details.
198-#
199-# You should have received a copy of the GNU Lesser General Public License
200-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
201+# Licensed under the Apache License, Version 2.0 (the "License");
202+# you may not use this file except in compliance with the License.
203+# You may obtain a copy of the License at
204+#
205+# http://www.apache.org/licenses/LICENSE-2.0
206+#
207+# Unless required by applicable law or agreed to in writing, software
208+# distributed under the License is distributed on an "AS IS" BASIS,
209+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
210+# See the License for the specific language governing permissions and
211+# limitations under the License.
212
213=== modified file 'hooks/charmhelpers/core/decorators.py'
214--- hooks/charmhelpers/core/decorators.py 2015-04-08 05:57:06 +0000
215+++ hooks/charmhelpers/core/decorators.py 2017-07-18 06:08:45 +0000
216@@ -1,18 +1,16 @@
217 # Copyright 2014-2015 Canonical Limited.
218 #
219-# This file is part of charm-helpers.
220-#
221-# charm-helpers is free software: you can redistribute it and/or modify
222-# it under the terms of the GNU Lesser General Public License version 3 as
223-# published by the Free Software Foundation.
224-#
225-# charm-helpers is distributed in the hope that it will be useful,
226-# but WITHOUT ANY WARRANTY; without even the implied warranty of
227-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
228-# GNU Lesser General Public License for more details.
229-#
230-# You should have received a copy of the GNU Lesser General Public License
231-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
232+# Licensed under the Apache License, Version 2.0 (the "License");
233+# you may not use this file except in compliance with the License.
234+# You may obtain a copy of the License at
235+#
236+# http://www.apache.org/licenses/LICENSE-2.0
237+#
238+# Unless required by applicable law or agreed to in writing, software
239+# distributed under the License is distributed on an "AS IS" BASIS,
240+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
241+# See the License for the specific language governing permissions and
242+# limitations under the License.
243
244 #
245 # Copyright 2014 Canonical Ltd.
246
247=== added file 'hooks/charmhelpers/core/files.py'
248--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
249+++ hooks/charmhelpers/core/files.py 2017-07-18 06:08:45 +0000
250@@ -0,0 +1,43 @@
251+#!/usr/bin/env python
252+# -*- coding: utf-8 -*-
253+
254+# Copyright 2014-2015 Canonical Limited.
255+#
256+# Licensed under the Apache License, Version 2.0 (the "License");
257+# you may not use this file except in compliance with the License.
258+# You may obtain a copy of the License at
259+#
260+# http://www.apache.org/licenses/LICENSE-2.0
261+#
262+# Unless required by applicable law or agreed to in writing, software
263+# distributed under the License is distributed on an "AS IS" BASIS,
264+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
265+# See the License for the specific language governing permissions and
266+# limitations under the License.
267+
268+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
269+
270+import os
271+import subprocess
272+
273+
274+def sed(filename, before, after, flags='g'):
275+ """
276+ Search and replaces the given pattern on filename.
277+
278+ :param filename: relative or absolute file path.
279+ :param before: expression to be replaced (see 'man sed')
280+ :param after: expression to replace with (see 'man sed')
281+ :param flags: sed-compatible regex flags in example, to make
282+ the search and replace case insensitive, specify ``flags="i"``.
283+ The ``g`` flag is always specified regardless, so you do not
284+ need to remember to include it when overriding this parameter.
285+ :returns: If the sed command exit code was zero then return,
286+ otherwise raise CalledProcessError.
287+ """
288+ expression = r's/{0}/{1}/{2}'.format(before,
289+ after, flags)
290+
291+ return subprocess.check_call(["sed", "-i", "-r", "-e",
292+ expression,
293+ os.path.expanduser(filename)])
294
295=== modified file 'hooks/charmhelpers/core/fstab.py'
296--- hooks/charmhelpers/core/fstab.py 2015-04-08 05:57:06 +0000
297+++ hooks/charmhelpers/core/fstab.py 2017-07-18 06:08:45 +0000
298@@ -3,19 +3,17 @@
299
300 # Copyright 2014-2015 Canonical Limited.
301 #
302-# This file is part of charm-helpers.
303-#
304-# charm-helpers is free software: you can redistribute it and/or modify
305-# it under the terms of the GNU Lesser General Public License version 3 as
306-# published by the Free Software Foundation.
307-#
308-# charm-helpers is distributed in the hope that it will be useful,
309-# but WITHOUT ANY WARRANTY; without even the implied warranty of
310-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
311-# GNU Lesser General Public License for more details.
312-#
313-# You should have received a copy of the GNU Lesser General Public License
314-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
315+# Licensed under the Apache License, Version 2.0 (the "License");
316+# you may not use this file except in compliance with the License.
317+# You may obtain a copy of the License at
318+#
319+# http://www.apache.org/licenses/LICENSE-2.0
320+#
321+# Unless required by applicable law or agreed to in writing, software
322+# distributed under the License is distributed on an "AS IS" BASIS,
323+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
324+# See the License for the specific language governing permissions and
325+# limitations under the License.
326
327 import io
328 import os
329
330=== modified file 'hooks/charmhelpers/core/hookenv.py'
331--- hooks/charmhelpers/core/hookenv.py 2015-04-08 05:57:06 +0000
332+++ hooks/charmhelpers/core/hookenv.py 2017-07-18 06:08:45 +0000
333@@ -1,18 +1,16 @@
334 # Copyright 2014-2015 Canonical Limited.
335 #
336-# This file is part of charm-helpers.
337-#
338-# charm-helpers is free software: you can redistribute it and/or modify
339-# it under the terms of the GNU Lesser General Public License version 3 as
340-# published by the Free Software Foundation.
341-#
342-# charm-helpers is distributed in the hope that it will be useful,
343-# but WITHOUT ANY WARRANTY; without even the implied warranty of
344-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
345-# GNU Lesser General Public License for more details.
346-#
347-# You should have received a copy of the GNU Lesser General Public License
348-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
349+# Licensed under the Apache License, Version 2.0 (the "License");
350+# you may not use this file except in compliance with the License.
351+# You may obtain a copy of the License at
352+#
353+# http://www.apache.org/licenses/LICENSE-2.0
354+#
355+# Unless required by applicable law or agreed to in writing, software
356+# distributed under the License is distributed on an "AS IS" BASIS,
357+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
358+# See the License for the specific language governing permissions and
359+# limitations under the License.
360
361 "Interactions with the Juju environment"
362 # Copyright 2013 Canonical Ltd.
363@@ -20,11 +18,18 @@
364 # Authors:
365 # Charm Helpers Developers <juju@lists.ubuntu.com>
366
367+from __future__ import print_function
368+import copy
369+from distutils.version import LooseVersion
370+from functools import wraps
371+import glob
372 import os
373 import json
374 import yaml
375 import subprocess
376 import sys
377+import errno
378+import tempfile
379 from subprocess import CalledProcessError
380
381 import six
382@@ -56,15 +61,18 @@
383
384 will cache the result of unit_get + 'test' for future calls.
385 """
386+ @wraps(func)
387 def wrapper(*args, **kwargs):
388 global cache
389 key = str((func, args, kwargs))
390 try:
391 return cache[key]
392 except KeyError:
393- res = func(*args, **kwargs)
394- cache[key] = res
395- return res
396+ pass # Drop out of the exception handler scope.
397+ res = func(*args, **kwargs)
398+ cache[key] = res
399+ return res
400+ wrapper._wrapped = func
401 return wrapper
402
403
404@@ -87,7 +95,18 @@
405 if not isinstance(message, six.string_types):
406 message = repr(message)
407 command += [message]
408- subprocess.call(command)
409+ # Missing juju-log should not cause failures in unit tests
410+ # Send log output to stderr
411+ try:
412+ subprocess.call(command)
413+ except OSError as e:
414+ if e.errno == errno.ENOENT:
415+ if level:
416+ message = "{}: {}".format(level, message)
417+ message = "juju-log: {}".format(message)
418+ print(message, file=sys.stderr)
419+ else:
420+ raise
421
422
423 class Serializable(UserDict):
424@@ -153,9 +172,19 @@
425 return os.environ.get('JUJU_RELATION', None)
426
427
428-def relation_id():
429- """The relation ID for the current relation hook"""
430- return os.environ.get('JUJU_RELATION_ID', None)
431+@cached
432+def relation_id(relation_name=None, service_or_unit=None):
433+ """The relation ID for the current or a specified relation"""
434+ if not relation_name and not service_or_unit:
435+ return os.environ.get('JUJU_RELATION_ID', None)
436+ elif relation_name and service_or_unit:
437+ service_name = service_or_unit.split('/')[0]
438+ for relid in relation_ids(relation_name):
439+ remote_service = remote_service_name(relid)
440+ if remote_service == service_name:
441+ return relid
442+ else:
443+ raise ValueError('Must specify neither or both of relation_name and service_or_unit')
444
445
446 def local_unit():
447@@ -165,7 +194,7 @@
448
449 def remote_unit():
450 """The remote unit for the current relation hook"""
451- return os.environ['JUJU_REMOTE_UNIT']
452+ return os.environ.get('JUJU_REMOTE_UNIT', None)
453
454
455 def service_name():
456@@ -173,9 +202,20 @@
457 return local_unit().split('/')[0]
458
459
460+@cached
461+def remote_service_name(relid=None):
462+ """The remote service name for a given relation-id (or the current relation)"""
463+ if relid is None:
464+ unit = remote_unit()
465+ else:
466+ units = related_units(relid)
467+ unit = units[0] if units else None
468+ return unit.split('/')[0] if unit else None
469+
470+
471 def hook_name():
472 """The name of the currently executing hook"""
473- return os.path.basename(sys.argv[0])
474+ return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
475
476
477 class Config(dict):
478@@ -225,23 +265,7 @@
479 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
480 if os.path.exists(self.path):
481 self.load_previous()
482-
483- def __getitem__(self, key):
484- """For regular dict lookups, check the current juju config first,
485- then the previous (saved) copy. This ensures that user-saved values
486- will be returned by a dict lookup.
487-
488- """
489- try:
490- return dict.__getitem__(self, key)
491- except KeyError:
492- return (self._prev_dict or {})[key]
493-
494- def keys(self):
495- prev_keys = []
496- if self._prev_dict is not None:
497- prev_keys = self._prev_dict.keys()
498- return list(set(prev_keys + list(dict.keys(self))))
499+ atexit(self._implicit_save)
500
501 def load_previous(self, path=None):
502 """Load previous copy of config from disk.
503@@ -260,6 +284,9 @@
504 self.path = path or self.path
505 with open(self.path) as f:
506 self._prev_dict = json.load(f)
507+ for k, v in copy.deepcopy(self._prev_dict).items():
508+ if k not in self:
509+ self[k] = v
510
511 def changed(self, key):
512 """Return True if the current value for this key is different from
513@@ -291,13 +318,13 @@
514 instance.
515
516 """
517- if self._prev_dict:
518- for k, v in six.iteritems(self._prev_dict):
519- if k not in self:
520- self[k] = v
521 with open(self.path, 'w') as f:
522 json.dump(self, f)
523
524+ def _implicit_save(self):
525+ if self.implicit_save:
526+ self.save()
527+
528
529 @cached
530 def config(scope=None):
531@@ -305,6 +332,8 @@
532 config_cmd_line = ['config-get']
533 if scope is not None:
534 config_cmd_line.append(scope)
535+ else:
536+ config_cmd_line.append('--all')
537 config_cmd_line.append('--format=json')
538 try:
539 config_data = json.loads(
540@@ -340,18 +369,49 @@
541 """Set relation information for the current unit"""
542 relation_settings = relation_settings if relation_settings else {}
543 relation_cmd_line = ['relation-set']
544+ accepts_file = "--file" in subprocess.check_output(
545+ relation_cmd_line + ["--help"], universal_newlines=True)
546 if relation_id is not None:
547 relation_cmd_line.extend(('-r', relation_id))
548- for k, v in (list(relation_settings.items()) + list(kwargs.items())):
549- if v is None:
550- relation_cmd_line.append('{}='.format(k))
551- else:
552- relation_cmd_line.append('{}={}'.format(k, v))
553- subprocess.check_call(relation_cmd_line)
554+ settings = relation_settings.copy()
555+ settings.update(kwargs)
556+ for key, value in settings.items():
557+ # Force value to be a string: it always should, but some call
558+ # sites pass in things like dicts or numbers.
559+ if value is not None:
560+ settings[key] = "{}".format(value)
561+ if accepts_file:
562+ # --file was introduced in Juju 1.23.2. Use it by default if
563+ # available, since otherwise we'll break if the relation data is
564+ # too big. Ideally we should tell relation-set to read the data from
565+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
566+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
567+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
568+ subprocess.check_call(
569+ relation_cmd_line + ["--file", settings_file.name])
570+ os.remove(settings_file.name)
571+ else:
572+ for key, value in settings.items():
573+ if value is None:
574+ relation_cmd_line.append('{}='.format(key))
575+ else:
576+ relation_cmd_line.append('{}={}'.format(key, value))
577+ subprocess.check_call(relation_cmd_line)
578 # Flush cache of any relation-gets for local unit
579 flush(local_unit())
580
581
582+def relation_clear(r_id=None):
583+ ''' Clears any relation data already set on relation r_id '''
584+ settings = relation_get(rid=r_id,
585+ unit=local_unit())
586+ for setting in settings:
587+ if setting not in ['public-address', 'private-address']:
588+ settings[setting] = None
589+ relation_set(relation_id=r_id,
590+ **settings)
591+
592+
593 @cached
594 def relation_ids(reltype=None):
595 """A list of relation_ids"""
596@@ -431,6 +491,76 @@
597
598
599 @cached
600+def peer_relation_id():
601+ '''Get the peers relation id if a peers relation has been joined, else None.'''
602+ md = metadata()
603+ section = md.get('peers')
604+ if section:
605+ for key in section:
606+ relids = relation_ids(key)
607+ if relids:
608+ return relids[0]
609+ return None
610+
611+
612+@cached
613+def relation_to_interface(relation_name):
614+ """
615+ Given the name of a relation, return the interface that relation uses.
616+
617+ :returns: The interface name, or ``None``.
618+ """
619+ return relation_to_role_and_interface(relation_name)[1]
620+
621+
622+@cached
623+def relation_to_role_and_interface(relation_name):
624+ """
625+ Given the name of a relation, return the role and the name of the interface
626+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
627+
628+ :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
629+ """
630+ _metadata = metadata()
631+ for role in ('provides', 'requires', 'peers'):
632+ interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
633+ if interface:
634+ return role, interface
635+ return None, None
636+
637+
638+@cached
639+def role_and_interface_to_relations(role, interface_name):
640+ """
641+ Given a role and interface name, return a list of relation names for the
642+ current charm that use that interface under that role (where role is one
643+ of ``provides``, ``requires``, or ``peers``).
644+
645+ :returns: A list of relation names.
646+ """
647+ _metadata = metadata()
648+ results = []
649+ for relation_name, relation in _metadata.get(role, {}).items():
650+ if relation['interface'] == interface_name:
651+ results.append(relation_name)
652+ return results
653+
654+
655+@cached
656+def interface_to_relations(interface_name):
657+ """
658+ Given an interface, return a list of relation names for the current
659+ charm that use that interface.
660+
661+ :returns: A list of relation names.
662+ """
663+ results = []
664+ for role in ('provides', 'requires', 'peers'):
665+ results.extend(role_and_interface_to_relations(role, interface_name))
666+ return results
667+
668+
669+@cached
670 def charm_name():
671 """Get the name of the current charm as is specified on metadata.yaml"""
672 return metadata().get('name')
673@@ -486,6 +616,20 @@
674 subprocess.check_call(_args)
675
676
677+def open_ports(start, end, protocol="TCP"):
678+ """Opens a range of service network ports"""
679+ _args = ['open-port']
680+ _args.append('{}-{}/{}'.format(start, end, protocol))
681+ subprocess.check_call(_args)
682+
683+
684+def close_ports(start, end, protocol="TCP"):
685+ """Close a range of service network ports"""
686+ _args = ['close-port']
687+ _args.append('{}-{}/{}'.format(start, end, protocol))
688+ subprocess.check_call(_args)
689+
690+
691 @cached
692 def unit_get(attribute):
693 """Get the unit ID for the remote unit"""
694@@ -496,11 +640,48 @@
695 return None
696
697
698+def unit_public_ip():
699+ """Get this unit's public IP address"""
700+ return unit_get('public-address')
701+
702+
703 def unit_private_ip():
704 """Get this unit's private IP address"""
705 return unit_get('private-address')
706
707
708+@cached
709+def storage_get(attribute=None, storage_id=None):
710+ """Get storage attributes"""
711+ _args = ['storage-get', '--format=json']
712+ if storage_id:
713+ _args.extend(('-s', storage_id))
714+ if attribute:
715+ _args.append(attribute)
716+ try:
717+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
718+ except ValueError:
719+ return None
720+
721+
722+@cached
723+def storage_list(storage_name=None):
724+ """List the storage IDs for the unit"""
725+ _args = ['storage-list', '--format=json']
726+ if storage_name:
727+ _args.append(storage_name)
728+ try:
729+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
730+ except ValueError:
731+ return None
732+ except OSError as e:
733+ import errno
734+ if e.errno == errno.ENOENT:
735+ # storage-list does not exist
736+ return []
737+ raise
738+
739+
740 class UnregisteredHookError(Exception):
741 """Raised when an undefined hook is called"""
742 pass
743@@ -528,10 +709,14 @@
744 hooks.execute(sys.argv)
745 """
746
747- def __init__(self, config_save=True):
748+ def __init__(self, config_save=None):
749 super(Hooks, self).__init__()
750 self._hooks = {}
751- self._config_save = config_save
752+
753+ # For unknown reasons, we allow the Hooks constructor to override
754+ # config().implicit_save.
755+ if config_save is not None:
756+ config().implicit_save = config_save
757
758 def register(self, name, function):
759 """Register a hook"""
760@@ -539,13 +724,16 @@
761
762 def execute(self, args):
763 """Execute a registered hook based on args[0]"""
764+ _run_atstart()
765 hook_name = os.path.basename(args[0])
766 if hook_name in self._hooks:
767- self._hooks[hook_name]()
768- if self._config_save:
769- cfg = config()
770- if cfg.implicit_save:
771- cfg.save()
772+ try:
773+ self._hooks[hook_name]()
774+ except SystemExit as x:
775+ if x.code is None or x.code == 0:
776+ _run_atexit()
777+ raise
778+ _run_atexit()
779 else:
780 raise UnregisteredHookError(hook_name)
781
782@@ -592,3 +780,289 @@
783
784 The results set by action_set are preserved."""
785 subprocess.check_call(['action-fail', message])
786+
787+
788+def action_name():
789+ """Get the name of the currently executing action."""
790+ return os.environ.get('JUJU_ACTION_NAME')
791+
792+
793+def action_uuid():
794+ """Get the UUID of the currently executing action."""
795+ return os.environ.get('JUJU_ACTION_UUID')
796+
797+
798+def action_tag():
799+ """Get the tag for the currently executing action."""
800+ return os.environ.get('JUJU_ACTION_TAG')
801+
802+
803+def status_set(workload_state, message):
804+ """Set the workload state with a message
805+
806+ Use status-set to set the workload state with a message which is visible
807+ to the user via juju status. If the status-set command is not found then
808+ assume this is juju < 1.23 and juju-log the message unstead.
809+
810+ workload_state -- valid juju workload state.
811+ message -- status update message
812+ """
813+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
814+ if workload_state not in valid_states:
815+ raise ValueError(
816+ '{!r} is not a valid workload state'.format(workload_state)
817+ )
818+ cmd = ['status-set', workload_state, message]
819+ try:
820+ ret = subprocess.call(cmd)
821+ if ret == 0:
822+ return
823+ except OSError as e:
824+ if e.errno != errno.ENOENT:
825+ raise
826+ log_message = 'status-set failed: {} {}'.format(workload_state,
827+ message)
828+ log(log_message, level='INFO')
829+
830+
831+def status_get():
832+ """Retrieve the previously set juju workload state and message
833+
834+ If the status-get command is not found then assume this is juju < 1.23 and
835+ return 'unknown', ""
836+
837+ """
838+ cmd = ['status-get', "--format=json", "--include-data"]
839+ try:
840+ raw_status = subprocess.check_output(cmd)
841+ except OSError as e:
842+ if e.errno == errno.ENOENT:
843+ return ('unknown', "")
844+ else:
845+ raise
846+ else:
847+ status = json.loads(raw_status.decode("UTF-8"))
848+ return (status["status"], status["message"])
849+
850+
851+def translate_exc(from_exc, to_exc):
852+ def inner_translate_exc1(f):
853+ @wraps(f)
854+ def inner_translate_exc2(*args, **kwargs):
855+ try:
856+ return f(*args, **kwargs)
857+ except from_exc:
858+ raise to_exc
859+
860+ return inner_translate_exc2
861+
862+ return inner_translate_exc1
863+
864+
865+def application_version_set(version):
866+ """Charm authors may trigger this command from any hook to output what
867+ version of the application is running. This could be a package version,
868+ for instance postgres version 9.5. It could also be a build number or
869+ version control revision identifier, for instance git sha 6fb7ba68. """
870+
871+ cmd = ['application-version-set']
872+ cmd.append(version)
873+ try:
874+ subprocess.check_call(cmd)
875+ except OSError:
876+ log("Application Version: {}".format(version))
877+
878+
879+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
880+def is_leader():
881+ """Does the current unit hold the juju leadership
882+
883+ Uses juju to determine whether the current unit is the leader of its peers
884+ """
885+ cmd = ['is-leader', '--format=json']
886+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
887+
888+
889+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
890+def leader_get(attribute=None):
891+ """Juju leader get value(s)"""
892+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
893+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
894+
895+
896+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
897+def leader_set(settings=None, **kwargs):
898+ """Juju leader set value(s)"""
899+ # Don't log secrets.
900+ # log("Juju leader-set '%s'" % (settings), level=DEBUG)
901+ cmd = ['leader-set']
902+ settings = settings or {}
903+ settings.update(kwargs)
904+ for k, v in settings.items():
905+ if v is None:
906+ cmd.append('{}='.format(k))
907+ else:
908+ cmd.append('{}={}'.format(k, v))
909+ subprocess.check_call(cmd)
910+
911+
912+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
913+def payload_register(ptype, klass, pid):
914+ """ is used while a hook is running to let Juju know that a
915+ payload has been started."""
916+ cmd = ['payload-register']
917+ for x in [ptype, klass, pid]:
918+ cmd.append(x)
919+ subprocess.check_call(cmd)
920+
921+
922+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
923+def payload_unregister(klass, pid):
924+ """ is used while a hook is running to let Juju know
925+ that a payload has been manually stopped. The <class> and <id> provided
926+ must match a payload that has been previously registered with juju using
927+ payload-register."""
928+ cmd = ['payload-unregister']
929+ for x in [klass, pid]:
930+ cmd.append(x)
931+ subprocess.check_call(cmd)
932+
933+
934+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
935+def payload_status_set(klass, pid, status):
936+ """is used to update the current status of a registered payload.
937+ The <class> and <id> provided must match a payload that has been previously
938+ registered with juju using payload-register. The <status> must be one of the
939+ follow: starting, started, stopping, stopped"""
940+ cmd = ['payload-status-set']
941+ for x in [klass, pid, status]:
942+ cmd.append(x)
943+ subprocess.check_call(cmd)
944+
945+
946+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
947+def resource_get(name):
948+ """used to fetch the resource path of the given name.
949+
950+ <name> must match a name of defined resource in metadata.yaml
951+
952+ returns either a path or False if resource not available
953+ """
954+ if not name:
955+ return False
956+
957+ cmd = ['resource-get', name]
958+ try:
959+ return subprocess.check_output(cmd).decode('UTF-8')
960+ except subprocess.CalledProcessError:
961+ return False
962+
963+
964+@cached
965+def juju_version():
966+ """Full version string (eg. '1.23.3.1-trusty-amd64')"""
967+ # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
968+ jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
969+ return subprocess.check_output([jujud, 'version'],
970+ universal_newlines=True).strip()
971+
972+
973+@cached
974+def has_juju_version(minimum_version):
975+ """Return True if the Juju version is at least the provided version"""
976+ return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
977+
978+
979+_atexit = []
980+_atstart = []
981+
982+
983+def atstart(callback, *args, **kwargs):
984+ '''Schedule a callback to run before the main hook.
985+
986+ Callbacks are run in the order they were added.
987+
988+ This is useful for modules and classes to perform initialization
989+ and inject behavior. In particular:
990+
991+ - Run common code before all of your hooks, such as logging
992+ the hook name or interesting relation data.
993+ - Defer object or module initialization that requires a hook
994+ context until we know there actually is a hook context,
995+ making testing easier.
996+ - Rather than requiring charm authors to include boilerplate to
997+ invoke your helper's behavior, have it run automatically if
998+ your object is instantiated or module imported.
999+
1000+ This is not at all useful after your hook framework as been launched.
1001+ '''
1002+ global _atstart
1003+ _atstart.append((callback, args, kwargs))
1004+
1005+
1006+def atexit(callback, *args, **kwargs):
1007+ '''Schedule a callback to run on successful hook completion.
1008+
1009+ Callbacks are run in the reverse order that they were added.'''
1010+ _atexit.append((callback, args, kwargs))
1011+
1012+
1013+def _run_atstart():
1014+ '''Hook frameworks must invoke this before running the main hook body.'''
1015+ global _atstart
1016+ for callback, args, kwargs in _atstart:
1017+ callback(*args, **kwargs)
1018+ del _atstart[:]
1019+
1020+
1021+def _run_atexit():
1022+ '''Hook frameworks must invoke this after the main hook body has
1023+ successfully completed. Do not invoke it if the hook fails.'''
1024+ global _atexit
1025+ for callback, args, kwargs in reversed(_atexit):
1026+ callback(*args, **kwargs)
1027+ del _atexit[:]
1028+
1029+
1030+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1031+def network_get_primary_address(binding):
1032+ '''
1033+ Retrieve the primary network address for a named binding
1034+
1035+ :param binding: string. The name of a relation of extra-binding
1036+ :return: string. The primary IP address for the named binding
1037+ :raise: NotImplementedError if run on Juju < 2.0
1038+ '''
1039+ cmd = ['network-get', '--primary-address', binding]
1040+ return subprocess.check_output(cmd).decode('UTF-8').strip()
1041+
1042+
1043+def add_metric(*args, **kwargs):
1044+ """Add metric values. Values may be expressed with keyword arguments. For
1045+ metric names containing dashes, these may be expressed as one or more
1046+ 'key=value' positional arguments. May only be called from the collect-metrics
1047+ hook."""
1048+ _args = ['add-metric']
1049+ _kvpairs = []
1050+ _kvpairs.extend(args)
1051+ _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
1052+ _args.extend(sorted(_kvpairs))
1053+ try:
1054+ subprocess.check_call(_args)
1055+ return
1056+ except EnvironmentError as e:
1057+ if e.errno != errno.ENOENT:
1058+ raise
1059+ log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
1060+ log(log_message, level='INFO')
1061+
1062+
1063+def meter_status():
1064+ """Get the meter status, if running in the meter-status-changed hook."""
1065+ return os.environ.get('JUJU_METER_STATUS')
1066+
1067+
1068+def meter_info():
1069+ """Get the meter status information, if running in the meter-status-changed
1070+ hook."""
1071+ return os.environ.get('JUJU_METER_INFO')
1072
1073=== modified file 'hooks/charmhelpers/core/host.py'
1074--- hooks/charmhelpers/core/host.py 2015-04-08 05:57:06 +0000
1075+++ hooks/charmhelpers/core/host.py 2017-07-18 06:08:45 +0000
1076@@ -1,18 +1,16 @@
1077 # Copyright 2014-2015 Canonical Limited.
1078 #
1079-# This file is part of charm-helpers.
1080-#
1081-# charm-helpers is free software: you can redistribute it and/or modify
1082-# it under the terms of the GNU Lesser General Public License version 3 as
1083-# published by the Free Software Foundation.
1084-#
1085-# charm-helpers is distributed in the hope that it will be useful,
1086-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1087-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1088-# GNU Lesser General Public License for more details.
1089-#
1090-# You should have received a copy of the GNU Lesser General Public License
1091-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1092+# Licensed under the Apache License, Version 2.0 (the "License");
1093+# you may not use this file except in compliance with the License.
1094+# You may obtain a copy of the License at
1095+#
1096+# http://www.apache.org/licenses/LICENSE-2.0
1097+#
1098+# Unless required by applicable law or agreed to in writing, software
1099+# distributed under the License is distributed on an "AS IS" BASIS,
1100+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1101+# See the License for the specific language governing permissions and
1102+# limitations under the License.
1103
1104 """Tools for working with the host system"""
1105 # Copyright 2012 Canonical Ltd.
1106@@ -24,85 +22,330 @@
1107 import os
1108 import re
1109 import pwd
1110+import glob
1111 import grp
1112 import random
1113 import string
1114 import subprocess
1115 import hashlib
1116+import functools
1117+import itertools
1118+import six
1119+
1120 from contextlib import contextmanager
1121 from collections import OrderedDict
1122-
1123-import six
1124-
1125 from .hookenv import log
1126 from .fstab import Fstab
1127-
1128-
1129-def service_start(service_name):
1130- """Start a system service"""
1131- return service('start', service_name)
1132-
1133-
1134-def service_stop(service_name):
1135- """Stop a system service"""
1136- return service('stop', service_name)
1137-
1138-
1139-def service_restart(service_name):
1140- """Restart a system service"""
1141+from charmhelpers.osplatform import get_platform
1142+
1143+__platform__ = get_platform()
1144+if __platform__ == "ubuntu":
1145+ from charmhelpers.core.host_factory.ubuntu import (
1146+ service_available,
1147+ add_new_group,
1148+ lsb_release,
1149+ cmp_pkgrevno,
1150+ CompareHostReleases,
1151+ ) # flake8: noqa -- ignore F401 for this import
1152+elif __platform__ == "centos":
1153+ from charmhelpers.core.host_factory.centos import (
1154+ service_available,
1155+ add_new_group,
1156+ lsb_release,
1157+ cmp_pkgrevno,
1158+ CompareHostReleases,
1159+ ) # flake8: noqa -- ignore F401 for this import
1160+
1161+UPDATEDB_PATH = '/etc/updatedb.conf'
1162+
1163+def service_start(service_name, **kwargs):
1164+ """Start a system service.
1165+
1166+ The specified service name is managed via the system level init system.
1167+ Some init systems (e.g. upstart) require that additional arguments be
1168+ provided in order to directly control service instances whereas other init
1169+ systems allow for addressing instances of a service directly by name (e.g.
1170+ systemd).
1171+
1172+ The kwargs allow for the additional parameters to be passed to underlying
1173+ init systems for those systems which require/allow for them. For example,
1174+ the ceph-osd upstart script requires the id parameter to be passed along
1175+ in order to identify which running daemon should be reloaded. The follow-
1176+ ing example stops the ceph-osd service for instance id=4:
1177+
1178+ service_stop('ceph-osd', id=4)
1179+
1180+ :param service_name: the name of the service to stop
1181+ :param **kwargs: additional parameters to pass to the init system when
1182+ managing services. These will be passed as key=value
1183+ parameters to the init system's commandline. kwargs
1184+ are ignored for systemd enabled systems.
1185+ """
1186+ return service('start', service_name, **kwargs)
1187+
1188+
1189+def service_stop(service_name, **kwargs):
1190+ """Stop a system service.
1191+
1192+ The specified service name is managed via the system level init system.
1193+ Some init systems (e.g. upstart) require that additional arguments be
1194+ provided in order to directly control service instances whereas other init
1195+ systems allow for addressing instances of a service directly by name (e.g.
1196+ systemd).
1197+
1198+ The kwargs allow for the additional parameters to be passed to underlying
1199+ init systems for those systems which require/allow for them. For example,
1200+ the ceph-osd upstart script requires the id parameter to be passed along
1201+ in order to identify which running daemon should be reloaded. The follow-
1202+ ing example stops the ceph-osd service for instance id=4:
1203+
1204+ service_stop('ceph-osd', id=4)
1205+
1206+ :param service_name: the name of the service to stop
1207+ :param **kwargs: additional parameters to pass to the init system when
1208+ managing services. These will be passed as key=value
1209+ parameters to the init system's commandline. kwargs
1210+ are ignored for systemd enabled systems.
1211+ """
1212+ return service('stop', service_name, **kwargs)
1213+
1214+
1215+def service_restart(service_name, **kwargs):
1216+ """Restart a system service.
1217+
1218+ The specified service name is managed via the system level init system.
1219+ Some init systems (e.g. upstart) require that additional arguments be
1220+ provided in order to directly control service instances whereas other init
1221+ systems allow for addressing instances of a service directly by name (e.g.
1222+ systemd).
1223+
1224+ The kwargs allow for the additional parameters to be passed to underlying
1225+ init systems for those systems which require/allow for them. For example,
1226+ the ceph-osd upstart script requires the id parameter to be passed along
1227+ in order to identify which running daemon should be restarted. The follow-
1228+ ing example restarts the ceph-osd service for instance id=4:
1229+
1230+ service_restart('ceph-osd', id=4)
1231+
1232+ :param service_name: the name of the service to restart
1233+ :param **kwargs: additional parameters to pass to the init system when
1234+ managing services. These will be passed as key=value
1235+ parameters to the init system's commandline. kwargs
1236+ are ignored for init systems not allowing additional
1237+ parameters via the commandline (systemd).
1238+ """
1239 return service('restart', service_name)
1240
1241
1242-def service_reload(service_name, restart_on_failure=False):
1243+def service_reload(service_name, restart_on_failure=False, **kwargs):
1244 """Reload a system service, optionally falling back to restart if
1245- reload fails"""
1246- service_result = service('reload', service_name)
1247+ reload fails.
1248+
1249+ The specified service name is managed via the system level init system.
1250+ Some init systems (e.g. upstart) require that additional arguments be
1251+ provided in order to directly control service instances whereas other init
1252+ systems allow for addressing instances of a service directly by name (e.g.
1253+ systemd).
1254+
1255+ The kwargs allow for the additional parameters to be passed to underlying
1256+ init systems for those systems which require/allow for them. For example,
1257+ the ceph-osd upstart script requires the id parameter to be passed along
1258+ in order to identify which running daemon should be reloaded. The follow-
1259+ ing example restarts the ceph-osd service for instance id=4:
1260+
1261+ service_reload('ceph-osd', id=4)
1262+
1263+ :param service_name: the name of the service to reload
1264+ :param restart_on_failure: boolean indicating whether to fallback to a
1265+ restart if the reload fails.
1266+ :param **kwargs: additional parameters to pass to the init system when
1267+ managing services. These will be passed as key=value
1268+ parameters to the init system's commandline. kwargs
1269+ are ignored for init systems not allowing additional
1270+ parameters via the commandline (systemd).
1271+ """
1272+ service_result = service('reload', service_name, **kwargs)
1273 if not service_result and restart_on_failure:
1274- service_result = service('restart', service_name)
1275+ service_result = service('restart', service_name, **kwargs)
1276 return service_result
1277
1278
1279-def service(action, service_name):
1280- """Control a system service"""
1281- cmd = ['service', service_name, action]
1282+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
1283+ **kwargs):
1284+ """Pause a system service.
1285+
1286+ Stop it, and prevent it from starting again at boot.
1287+
1288+ :param service_name: the name of the service to pause
1289+ :param init_dir: path to the upstart init directory
1290+ :param initd_dir: path to the sysv init directory
1291+ :param **kwargs: additional parameters to pass to the init system when
1292+ managing services. These will be passed as key=value
1293+ parameters to the init system's commandline. kwargs
1294+ are ignored for init systems which do not support
1295+ key=value arguments via the commandline.
1296+ """
1297+ stopped = True
1298+ if service_running(service_name, **kwargs):
1299+ stopped = service_stop(service_name, **kwargs)
1300+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1301+ sysv_file = os.path.join(initd_dir, service_name)
1302+ if init_is_systemd():
1303+ service('disable', service_name)
1304+ service('mask', service_name)
1305+ elif os.path.exists(upstart_file):
1306+ override_path = os.path.join(
1307+ init_dir, '{}.override'.format(service_name))
1308+ with open(override_path, 'w') as fh:
1309+ fh.write("manual\n")
1310+ elif os.path.exists(sysv_file):
1311+ subprocess.check_call(["update-rc.d", service_name, "disable"])
1312+ else:
1313+ raise ValueError(
1314+ "Unable to detect {0} as SystemD, Upstart {1} or"
1315+ " SysV {2}".format(
1316+ service_name, upstart_file, sysv_file))
1317+ return stopped
1318+
1319+
1320+def service_resume(service_name, init_dir="/etc/init",
1321+ initd_dir="/etc/init.d", **kwargs):
1322+ """Resume a system service.
1323+
1324+ Reenable starting again at boot. Start the service.
1325+
1326+ :param service_name: the name of the service to resume
1327+ :param init_dir: the path to the init dir
1328+ :param initd dir: the path to the initd dir
1329+ :param **kwargs: additional parameters to pass to the init system when
1330+ managing services. These will be passed as key=value
1331+ parameters to the init system's commandline. kwargs
1332+ are ignored for systemd enabled systems.
1333+ """
1334+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1335+ sysv_file = os.path.join(initd_dir, service_name)
1336+ if init_is_systemd():
1337+ service('unmask', service_name)
1338+ service('enable', service_name)
1339+ elif os.path.exists(upstart_file):
1340+ override_path = os.path.join(
1341+ init_dir, '{}.override'.format(service_name))
1342+ if os.path.exists(override_path):
1343+ os.unlink(override_path)
1344+ elif os.path.exists(sysv_file):
1345+ subprocess.check_call(["update-rc.d", service_name, "enable"])
1346+ else:
1347+ raise ValueError(
1348+ "Unable to detect {0} as SystemD, Upstart {1} or"
1349+ " SysV {2}".format(
1350+ service_name, upstart_file, sysv_file))
1351+ started = service_running(service_name, **kwargs)
1352+
1353+ if not started:
1354+ started = service_start(service_name, **kwargs)
1355+ return started
1356+
1357+
1358+def service(action, service_name, **kwargs):
1359+ """Control a system service.
1360+
1361+ :param action: the action to take on the service
1362+ :param service_name: the name of the service to perform th action on
1363+ :param **kwargs: additional params to be passed to the service command in
1364+ the form of key=value.
1365+ """
1366+ if init_is_systemd():
1367+ cmd = ['systemctl', action, service_name]
1368+ else:
1369+ cmd = ['service', service_name, action]
1370+ for key, value in six.iteritems(kwargs):
1371+ parameter = '%s=%s' % (key, value)
1372+ cmd.append(parameter)
1373 return subprocess.call(cmd) == 0
1374
1375
1376-def service_running(service):
1377- """Determine whether a system service is running"""
1378- try:
1379- output = subprocess.check_output(
1380- ['service', service, 'status'],
1381- stderr=subprocess.STDOUT).decode('UTF-8')
1382- except subprocess.CalledProcessError:
1383- return False
1384- else:
1385- if ("start/running" in output or "is running" in output):
1386- return True
1387- else:
1388- return False
1389-
1390-
1391-def service_available(service_name):
1392- """Determine whether a system service is available"""
1393- try:
1394- subprocess.check_output(
1395- ['service', service_name, 'status'],
1396- stderr=subprocess.STDOUT).decode('UTF-8')
1397- except subprocess.CalledProcessError as e:
1398- return 'unrecognized service' not in e.output
1399- else:
1400- return True
1401-
1402-
1403-def adduser(username, password=None, shell='/bin/bash', system_user=False):
1404- """Add a user to the system"""
1405+_UPSTART_CONF = "/etc/init/{}.conf"
1406+_INIT_D_CONF = "/etc/init.d/{}"
1407+
1408+
1409+def service_running(service_name, **kwargs):
1410+ """Determine whether a system service is running.
1411+
1412+ :param service_name: the name of the service
1413+ :param **kwargs: additional args to pass to the service command. This is
1414+ used to pass additional key=value arguments to the
1415+ service command line for managing specific instance
1416+ units (e.g. service ceph-osd status id=2). The kwargs
1417+ are ignored in systemd services.
1418+ """
1419+ if init_is_systemd():
1420+ return service('is-active', service_name)
1421+ else:
1422+ if os.path.exists(_UPSTART_CONF.format(service_name)):
1423+ try:
1424+ cmd = ['status', service_name]
1425+ for key, value in six.iteritems(kwargs):
1426+ parameter = '%s=%s' % (key, value)
1427+ cmd.append(parameter)
1428+ output = subprocess.check_output(cmd,
1429+ stderr=subprocess.STDOUT).decode('UTF-8')
1430+ except subprocess.CalledProcessError:
1431+ return False
1432+ else:
1433+ # This works for upstart scripts where the 'service' command
1434+ # returns a consistent string to represent running
1435+ # 'start/running'
1436+ if ("start/running" in output or
1437+ "is running" in output or
1438+ "up and running" in output):
1439+ return True
1440+ elif os.path.exists(_INIT_D_CONF.format(service_name)):
1441+ # Check System V scripts init script return codes
1442+ return service('status', service_name)
1443+ return False
1444+
1445+
1446+SYSTEMD_SYSTEM = '/run/systemd/system'
1447+
1448+
1449+def init_is_systemd():
1450+ """Return True if the host system uses systemd, False otherwise."""
1451+ if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
1452+ return False
1453+ return os.path.isdir(SYSTEMD_SYSTEM)
1454+
1455+
1456+def adduser(username, password=None, shell='/bin/bash',
1457+ system_user=False, primary_group=None,
1458+ secondary_groups=None, uid=None, home_dir=None):
1459+ """Add a user to the system.
1460+
1461+ Will log but otherwise succeed if the user already exists.
1462+
1463+ :param str username: Username to create
1464+ :param str password: Password for user; if ``None``, create a system user
1465+ :param str shell: The default shell for the user
1466+ :param bool system_user: Whether to create a login or system user
1467+ :param str primary_group: Primary group for user; defaults to username
1468+ :param list secondary_groups: Optional list of additional groups
1469+ :param int uid: UID for user being created
1470+ :param str home_dir: Home directory for user
1471+
1472+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
1473+ """
1474 try:
1475 user_info = pwd.getpwnam(username)
1476 log('user {0} already exists!'.format(username))
1477+ if uid:
1478+ user_info = pwd.getpwuid(int(uid))
1479+ log('user with uid {0} already exists!'.format(uid))
1480 except KeyError:
1481 log('creating user {0}'.format(username))
1482 cmd = ['useradd']
1483+ if uid:
1484+ cmd.extend(['--uid', str(uid)])
1485+ if home_dir:
1486+ cmd.extend(['--home', str(home_dir)])
1487 if system_user or password is None:
1488 cmd.append('--system')
1489 else:
1490@@ -111,52 +354,104 @@
1491 '--shell', shell,
1492 '--password', password,
1493 ])
1494+ if not primary_group:
1495+ try:
1496+ grp.getgrnam(username)
1497+ primary_group = username # avoid "group exists" error
1498+ except KeyError:
1499+ pass
1500+ if primary_group:
1501+ cmd.extend(['-g', primary_group])
1502+ if secondary_groups:
1503+ cmd.extend(['-G', ','.join(secondary_groups)])
1504 cmd.append(username)
1505 subprocess.check_call(cmd)
1506 user_info = pwd.getpwnam(username)
1507 return user_info
1508
1509
1510-def add_group(group_name, system_group=False):
1511- """Add a group to the system"""
1512+def user_exists(username):
1513+ """Check if a user exists"""
1514+ try:
1515+ pwd.getpwnam(username)
1516+ user_exists = True
1517+ except KeyError:
1518+ user_exists = False
1519+ return user_exists
1520+
1521+
1522+def uid_exists(uid):
1523+ """Check if a uid exists"""
1524+ try:
1525+ pwd.getpwuid(uid)
1526+ uid_exists = True
1527+ except KeyError:
1528+ uid_exists = False
1529+ return uid_exists
1530+
1531+
1532+def group_exists(groupname):
1533+ """Check if a group exists"""
1534+ try:
1535+ grp.getgrnam(groupname)
1536+ group_exists = True
1537+ except KeyError:
1538+ group_exists = False
1539+ return group_exists
1540+
1541+
1542+def gid_exists(gid):
1543+ """Check if a gid exists"""
1544+ try:
1545+ grp.getgrgid(gid)
1546+ gid_exists = True
1547+ except KeyError:
1548+ gid_exists = False
1549+ return gid_exists
1550+
1551+
1552+def add_group(group_name, system_group=False, gid=None):
1553+ """Add a group to the system
1554+
1555+ Will log but otherwise succeed if the group already exists.
1556+
1557+ :param str group_name: group to create
1558+ :param bool system_group: Create system group
1559+ :param int gid: GID for user being created
1560+
1561+ :returns: The password database entry struct, as returned by `grp.getgrnam`
1562+ """
1563 try:
1564 group_info = grp.getgrnam(group_name)
1565 log('group {0} already exists!'.format(group_name))
1566+ if gid:
1567+ group_info = grp.getgrgid(gid)
1568+ log('group with gid {0} already exists!'.format(gid))
1569 except KeyError:
1570 log('creating group {0}'.format(group_name))
1571- cmd = ['addgroup']
1572- if system_group:
1573- cmd.append('--system')
1574- else:
1575- cmd.extend([
1576- '--group',
1577- ])
1578- cmd.append(group_name)
1579- subprocess.check_call(cmd)
1580+ add_new_group(group_name, system_group, gid)
1581 group_info = grp.getgrnam(group_name)
1582 return group_info
1583
1584
1585 def add_user_to_group(username, group):
1586 """Add a user to a group"""
1587- cmd = [
1588- 'gpasswd', '-a',
1589- username,
1590- group
1591- ]
1592+ cmd = ['gpasswd', '-a', username, group]
1593 log("Adding user {} to group {}".format(username, group))
1594 subprocess.check_call(cmd)
1595
1596
1597-def rsync(from_path, to_path, flags='-r', options=None):
1598+def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
1599 """Replicate the contents of a path"""
1600 options = options or ['--delete', '--executability']
1601 cmd = ['/usr/bin/rsync', flags]
1602+ if timeout:
1603+ cmd = ['timeout', str(timeout)] + cmd
1604 cmd.extend(options)
1605 cmd.append(from_path)
1606 cmd.append(to_path)
1607 log(" ".join(cmd))
1608- return subprocess.check_output(cmd).decode('UTF-8').strip()
1609+ return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
1610
1611
1612 def symlink(source, destination):
1613@@ -202,14 +497,12 @@
1614
1615
1616 def fstab_remove(mp):
1617- """Remove the given mountpoint entry from /etc/fstab
1618- """
1619+ """Remove the given mountpoint entry from /etc/fstab"""
1620 return Fstab.remove_by_mountpoint(mp)
1621
1622
1623 def fstab_add(dev, mp, fs, options=None):
1624- """Adds the given device entry to the /etc/fstab file
1625- """
1626+ """Adds the given device entry to the /etc/fstab file"""
1627 return Fstab.add(dev, mp, fs, options=options)
1628
1629
1630@@ -253,9 +546,19 @@
1631 return system_mounts
1632
1633
1634+def fstab_mount(mountpoint):
1635+ """Mount filesystem using fstab"""
1636+ cmd_args = ['mount', mountpoint]
1637+ try:
1638+ subprocess.check_output(cmd_args)
1639+ except subprocess.CalledProcessError as e:
1640+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1641+ return False
1642+ return True
1643+
1644+
1645 def file_hash(path, hash_type='md5'):
1646- """
1647- Generate a hash checksum of the contents of 'path' or None if not found.
1648+ """Generate a hash checksum of the contents of 'path' or None if not found.
1649
1650 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
1651 such as md5, sha1, sha256, sha512, etc.
1652@@ -269,9 +572,22 @@
1653 return None
1654
1655
1656+def path_hash(path):
1657+ """Generate a hash checksum of all files matching 'path'. Standard
1658+ wildcards like '*' and '?' are supported, see documentation for the 'glob'
1659+ module for more information.
1660+
1661+ :return: dict: A { filename: hash } dictionary for all matched files.
1662+ Empty if none found.
1663+ """
1664+ return {
1665+ filename: file_hash(filename)
1666+ for filename in glob.iglob(path)
1667+ }
1668+
1669+
1670 def check_hash(path, checksum, hash_type='md5'):
1671- """
1672- Validate a file using a cryptographic checksum.
1673+ """Validate a file using a cryptographic checksum.
1674
1675 :param str checksum: Value of the checksum used to validate the file.
1676 :param str hash_type: Hash algorithm used to generate `checksum`.
1677@@ -286,54 +602,78 @@
1678
1679
1680 class ChecksumError(ValueError):
1681+ """A class derived from Value error to indicate the checksum failed."""
1682 pass
1683
1684
1685-def restart_on_change(restart_map, stopstart=False):
1686+def restart_on_change(restart_map, stopstart=False, restart_functions=None):
1687 """Restart services based on configuration files changing
1688
1689 This function is used a decorator, for example::
1690
1691 @restart_on_change({
1692 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
1693+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
1694 })
1695- def ceph_client_changed():
1696+ def config_changed():
1697 pass # your code here
1698
1699 In this example, the cinder-api and cinder-volume services
1700 would be restarted if /etc/ceph/ceph.conf is changed by the
1701- ceph_client_changed function.
1702+ ceph_client_changed function. The apache2 service would be
1703+ restarted if any file matching the pattern got changed, created
1704+ or removed. Standard wildcards are supported, see documentation
1705+ for the 'glob' module for more information.
1706+
1707+ @param restart_map: {path_file_name: [service_name, ...]
1708+ @param stopstart: DEFAULT false; whether to stop, start OR restart
1709+ @param restart_functions: nonstandard functions to use to restart services
1710+ {svc: func, ...}
1711+ @returns result from decorated function
1712 """
1713 def wrap(f):
1714+ @functools.wraps(f)
1715 def wrapped_f(*args, **kwargs):
1716- checksums = {}
1717- for path in restart_map:
1718- checksums[path] = file_hash(path)
1719- f(*args, **kwargs)
1720- restarts = []
1721- for path in restart_map:
1722- if checksums[path] != file_hash(path):
1723- restarts += restart_map[path]
1724- services_list = list(OrderedDict.fromkeys(restarts))
1725- if not stopstart:
1726- for service_name in services_list:
1727- service('restart', service_name)
1728- else:
1729- for action in ['stop', 'start']:
1730- for service_name in services_list:
1731- service(action, service_name)
1732+ return restart_on_change_helper(
1733+ (lambda: f(*args, **kwargs)), restart_map, stopstart,
1734+ restart_functions)
1735 return wrapped_f
1736 return wrap
1737
1738
1739-def lsb_release():
1740- """Return /etc/lsb-release in a dict"""
1741- d = {}
1742- with open('/etc/lsb-release', 'r') as lsb:
1743- for l in lsb:
1744- k, v = l.split('=')
1745- d[k.strip()] = v.strip()
1746- return d
1747+def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
1748+ restart_functions=None):
1749+ """Helper function to perform the restart_on_change function.
1750+
1751+ This is provided for decorators to restart services if files described
1752+ in the restart_map have changed after an invocation of lambda_f().
1753+
1754+ @param lambda_f: function to call.
1755+ @param restart_map: {file: [service, ...]}
1756+ @param stopstart: whether to stop, start or restart a service
1757+ @param restart_functions: nonstandard functions to use to restart services
1758+ {svc: func, ...}
1759+ @returns result of lambda_f()
1760+ """
1761+ if restart_functions is None:
1762+ restart_functions = {}
1763+ checksums = {path: path_hash(path) for path in restart_map}
1764+ r = lambda_f()
1765+ # create a list of lists of the services to restart
1766+ restarts = [restart_map[path]
1767+ for path in restart_map
1768+ if path_hash(path) != checksums[path]]
1769+ # create a flat list of ordered services without duplicates from lists
1770+ services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
1771+ if services_list:
1772+ actions = ('stop', 'start') if stopstart else ('restart',)
1773+ for service_name in services_list:
1774+ if service_name in restart_functions:
1775+ restart_functions[service_name](service_name)
1776+ else:
1777+ for action in actions:
1778+ service(action, service_name)
1779+ return r
1780
1781
1782 def pwgen(length=None):
1783@@ -352,36 +692,92 @@
1784 return(''.join(random_chars))
1785
1786
1787-def list_nics(nic_type):
1788- '''Return a list of nics of given type(s)'''
1789+def is_phy_iface(interface):
1790+ """Returns True if interface is not virtual, otherwise False."""
1791+ if interface:
1792+ sys_net = '/sys/class/net'
1793+ if os.path.isdir(sys_net):
1794+ for iface in glob.glob(os.path.join(sys_net, '*')):
1795+ if '/virtual/' in os.path.realpath(iface):
1796+ continue
1797+
1798+ if interface == os.path.basename(iface):
1799+ return True
1800+
1801+ return False
1802+
1803+
1804+def get_bond_master(interface):
1805+ """Returns bond master if interface is bond slave otherwise None.
1806+
1807+ NOTE: the provided interface is expected to be physical
1808+ """
1809+ if interface:
1810+ iface_path = '/sys/class/net/%s' % (interface)
1811+ if os.path.exists(iface_path):
1812+ if '/virtual/' in os.path.realpath(iface_path):
1813+ return None
1814+
1815+ master = os.path.join(iface_path, 'master')
1816+ if os.path.exists(master):
1817+ master = os.path.realpath(master)
1818+ # make sure it is a bond master
1819+ if os.path.exists(os.path.join(master, 'bonding')):
1820+ return os.path.basename(master)
1821+
1822+ return None
1823+
1824+
1825+def list_nics(nic_type=None):
1826+ """Return a list of nics of given type(s)"""
1827 if isinstance(nic_type, six.string_types):
1828 int_types = [nic_type]
1829 else:
1830 int_types = nic_type
1831+
1832 interfaces = []
1833- for int_type in int_types:
1834- cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1835+ if nic_type:
1836+ for int_type in int_types:
1837+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1838+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
1839+ ip_output = ip_output.split('\n')
1840+ ip_output = (line for line in ip_output if line)
1841+ for line in ip_output:
1842+ if line.split()[1].startswith(int_type):
1843+ matched = re.search('.*: (' + int_type +
1844+ r'[0-9]+\.[0-9]+)@.*', line)
1845+ if matched:
1846+ iface = matched.groups()[0]
1847+ else:
1848+ iface = line.split()[1].replace(":", "")
1849+
1850+ if iface not in interfaces:
1851+ interfaces.append(iface)
1852+ else:
1853+ cmd = ['ip', 'a']
1854 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1855- ip_output = (line for line in ip_output if line)
1856+ ip_output = (line.strip() for line in ip_output if line)
1857+
1858+ key = re.compile('^[0-9]+:\s+(.+):')
1859 for line in ip_output:
1860- if line.split()[1].startswith(int_type):
1861- matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
1862- if matched:
1863- interface = matched.groups()[0]
1864- else:
1865- interface = line.split()[1].replace(":", "")
1866- interfaces.append(interface)
1867+ matched = re.search(key, line)
1868+ if matched:
1869+ iface = matched.group(1)
1870+ iface = iface.partition("@")[0]
1871+ if iface not in interfaces:
1872+ interfaces.append(iface)
1873
1874 return interfaces
1875
1876
1877 def set_nic_mtu(nic, mtu):
1878- '''Set MTU on a network interface'''
1879+ """Set the Maximum Transmission Unit (MTU) on a network interface."""
1880 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
1881 subprocess.check_call(cmd)
1882
1883
1884 def get_nic_mtu(nic):
1885+ """Return the Maximum Transmission Unit (MTU) for a network interface."""
1886 cmd = ['ip', 'addr', 'show', nic]
1887 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1888 mtu = ""
1889@@ -393,6 +789,7 @@
1890
1891
1892 def get_nic_hwaddr(nic):
1893+ """Return the Media Access Control (MAC) for a network interface."""
1894 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1895 ip_output = subprocess.check_output(cmd).decode('UTF-8')
1896 hwaddr = ""
1897@@ -402,35 +799,31 @@
1898 return hwaddr
1899
1900
1901-def cmp_pkgrevno(package, revno, pkgcache=None):
1902- '''Compare supplied revno with the revno of the installed package
1903-
1904- * 1 => Installed revno is greater than supplied arg
1905- * 0 => Installed revno is the same as supplied arg
1906- * -1 => Installed revno is less than supplied arg
1907-
1908- This function imports apt_cache function from charmhelpers.fetch if
1909- the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1910- you call this function, or pass an apt_pkg.Cache() instance.
1911- '''
1912- import apt_pkg
1913- if not pkgcache:
1914- from charmhelpers.fetch import apt_cache
1915- pkgcache = apt_cache()
1916- pkg = pkgcache[package]
1917- return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
1918-
1919-
1920 @contextmanager
1921-def chdir(d):
1922+def chdir(directory):
1923+ """Change the current working directory to a different directory for a code
1924+ block and return the previous directory after the block exits. Useful to
1925+ run commands from a specificed directory.
1926+
1927+ :param str directory: The directory path to change to for this context.
1928+ """
1929 cur = os.getcwd()
1930 try:
1931- yield os.chdir(d)
1932+ yield os.chdir(directory)
1933 finally:
1934 os.chdir(cur)
1935
1936
1937-def chownr(path, owner, group, follow_links=True):
1938+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
1939+ """Recursively change user and group ownership of files and directories
1940+ in given path. Doesn't chown path itself by default, only its children.
1941+
1942+ :param str path: The string path to start changing ownership.
1943+ :param str owner: The owner string to use when looking up the uid.
1944+ :param str group: The group string to use when looking up the gid.
1945+ :param bool follow_links: Also follow and chown links if True
1946+ :param bool chowntopdir: Also chown path itself if True
1947+ """
1948 uid = pwd.getpwnam(owner).pw_uid
1949 gid = grp.getgrnam(group).gr_gid
1950 if follow_links:
1951@@ -438,7 +831,11 @@
1952 else:
1953 chown = os.lchown
1954
1955- for root, dirs, files in os.walk(path):
1956+ if chowntopdir:
1957+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
1958+ if not broken_symlink:
1959+ chown(path, uid, gid)
1960+ for root, dirs, files in os.walk(path, followlinks=follow_links):
1961 for name in dirs + files:
1962 full = os.path.join(root, name)
1963 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
1964@@ -447,4 +844,81 @@
1965
1966
1967 def lchownr(path, owner, group):
1968+ """Recursively change user and group ownership of files and directories
1969+ in a given path, not following symbolic links. See the documentation for
1970+ 'os.lchown' for more information.
1971+
1972+ :param str path: The string path to start changing ownership.
1973+ :param str owner: The owner string to use when looking up the uid.
1974+ :param str group: The group string to use when looking up the gid.
1975+ """
1976 chownr(path, owner, group, follow_links=False)
1977+
1978+
1979+def owner(path):
1980+ """Returns a tuple containing the username & groupname owning the path.
1981+
1982+ :param str path: the string path to retrieve the ownership
1983+ :return tuple(str, str): A (username, groupname) tuple containing the
1984+ name of the user and group owning the path.
1985+ :raises OSError: if the specified path does not exist
1986+ """
1987+ stat = os.stat(path)
1988+ username = pwd.getpwuid(stat.st_uid)[0]
1989+ groupname = grp.getgrgid(stat.st_gid)[0]
1990+ return username, groupname
1991+
1992+
1993+def get_total_ram():
1994+ """The total amount of system RAM in bytes.
1995+
1996+ This is what is reported by the OS, and may be overcommitted when
1997+ there are multiple containers hosted on the same machine.
1998+ """
1999+ with open('/proc/meminfo', 'r') as f:
2000+ for line in f.readlines():
2001+ if line:
2002+ key, value, unit = line.split()
2003+ if key == 'MemTotal:':
2004+ assert unit == 'kB', 'Unknown unit'
2005+ return int(value) * 1024 # Classic, not KiB.
2006+ raise NotImplementedError()
2007+
2008+
2009+UPSTART_CONTAINER_TYPE = '/run/container_type'
2010+
2011+
2012+def is_container():
2013+ """Determine whether unit is running in a container
2014+
2015+ @return: boolean indicating if unit is in a container
2016+ """
2017+ if init_is_systemd():
2018+ # Detect using systemd-detect-virt
2019+ return subprocess.call(['systemd-detect-virt',
2020+ '--container']) == 0
2021+ else:
2022+ # Detect using upstart container file marker
2023+ return os.path.exists(UPSTART_CONTAINER_TYPE)
2024+
2025+
2026+def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
2027+ with open(updatedb_path, 'r+') as f_id:
2028+ updatedb_text = f_id.read()
2029+ output = updatedb(updatedb_text, path)
2030+ f_id.seek(0)
2031+ f_id.write(output)
2032+ f_id.truncate()
2033+
2034+
2035+def updatedb(updatedb_text, new_path):
2036+ lines = [line for line in updatedb_text.split("\n")]
2037+ for i, line in enumerate(lines):
2038+ if line.startswith("PRUNEPATHS="):
2039+ paths_line = line.split("=")[1].replace('"', '')
2040+ paths = paths_line.split(" ")
2041+ if new_path not in paths:
2042+ paths.append(new_path)
2043+ lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
2044+ output = "\n".join(lines)
2045+ return output
2046
2047=== added directory 'hooks/charmhelpers/core/host_factory'
2048=== added file 'hooks/charmhelpers/core/host_factory/__init__.py'
2049=== added file 'hooks/charmhelpers/core/host_factory/centos.py'
2050--- hooks/charmhelpers/core/host_factory/centos.py 1970-01-01 00:00:00 +0000
2051+++ hooks/charmhelpers/core/host_factory/centos.py 2017-07-18 06:08:45 +0000
2052@@ -0,0 +1,72 @@
2053+import subprocess
2054+import yum
2055+import os
2056+
2057+from charmhelpers.core.strutils import BasicStringComparator
2058+
2059+
2060+class CompareHostReleases(BasicStringComparator):
2061+ """Provide comparisons of Host releases.
2062+
2063+ Use in the form of
2064+
2065+ if CompareHostReleases(release) > 'trusty':
2066+ # do something with mitaka
2067+ """
2068+
2069+ def __init__(self, item):
2070+ raise NotImplementedError(
2071+ "CompareHostReleases() is not implemented for CentOS")
2072+
2073+
2074+def service_available(service_name):
2075+ # """Determine whether a system service is available."""
2076+ if os.path.isdir('/run/systemd/system'):
2077+ cmd = ['systemctl', 'is-enabled', service_name]
2078+ else:
2079+ cmd = ['service', service_name, 'is-enabled']
2080+ return subprocess.call(cmd) == 0
2081+
2082+
2083+def add_new_group(group_name, system_group=False, gid=None):
2084+ cmd = ['groupadd']
2085+ if gid:
2086+ cmd.extend(['--gid', str(gid)])
2087+ if system_group:
2088+ cmd.append('-r')
2089+ cmd.append(group_name)
2090+ subprocess.check_call(cmd)
2091+
2092+
2093+def lsb_release():
2094+ """Return /etc/os-release in a dict."""
2095+ d = {}
2096+ with open('/etc/os-release', 'r') as lsb:
2097+ for l in lsb:
2098+ s = l.split('=')
2099+ if len(s) != 2:
2100+ continue
2101+ d[s[0].strip()] = s[1].strip()
2102+ return d
2103+
2104+
2105+def cmp_pkgrevno(package, revno, pkgcache=None):
2106+ """Compare supplied revno with the revno of the installed package.
2107+
2108+ * 1 => Installed revno is greater than supplied arg
2109+ * 0 => Installed revno is the same as supplied arg
2110+ * -1 => Installed revno is less than supplied arg
2111+
2112+ This function imports YumBase function if the pkgcache argument
2113+ is None.
2114+ """
2115+ if not pkgcache:
2116+ y = yum.YumBase()
2117+ packages = y.doPackageLists()
2118+ pkgcache = {i.Name: i.version for i in packages['installed']}
2119+ pkg = pkgcache[package]
2120+ if pkg > revno:
2121+ return 1
2122+ if pkg < revno:
2123+ return -1
2124+ return 0
2125
2126=== added file 'hooks/charmhelpers/core/host_factory/ubuntu.py'
2127--- hooks/charmhelpers/core/host_factory/ubuntu.py 1970-01-01 00:00:00 +0000
2128+++ hooks/charmhelpers/core/host_factory/ubuntu.py 2017-07-18 06:08:45 +0000
2129@@ -0,0 +1,89 @@
2130+import subprocess
2131+
2132+from charmhelpers.core.strutils import BasicStringComparator
2133+
2134+
2135+UBUNTU_RELEASES = (
2136+ 'lucid',
2137+ 'maverick',
2138+ 'natty',
2139+ 'oneiric',
2140+ 'precise',
2141+ 'quantal',
2142+ 'raring',
2143+ 'saucy',
2144+ 'trusty',
2145+ 'utopic',
2146+ 'vivid',
2147+ 'wily',
2148+ 'xenial',
2149+ 'yakkety',
2150+ 'zesty',
2151+ 'artful',
2152+)
2153+
2154+
2155+class CompareHostReleases(BasicStringComparator):
2156+ """Provide comparisons of Ubuntu releases.
2157+
2158+ Use in the form of
2159+
2160+ if CompareHostReleases(release) > 'trusty':
2161+ # do something with mitaka
2162+ """
2163+ _list = UBUNTU_RELEASES
2164+
2165+
2166+def service_available(service_name):
2167+ """Determine whether a system service is available"""
2168+ try:
2169+ subprocess.check_output(
2170+ ['service', service_name, 'status'],
2171+ stderr=subprocess.STDOUT).decode('UTF-8')
2172+ except subprocess.CalledProcessError as e:
2173+ return b'unrecognized service' not in e.output
2174+ else:
2175+ return True
2176+
2177+
2178+def add_new_group(group_name, system_group=False, gid=None):
2179+ cmd = ['addgroup']
2180+ if gid:
2181+ cmd.extend(['--gid', str(gid)])
2182+ if system_group:
2183+ cmd.append('--system')
2184+ else:
2185+ cmd.extend([
2186+ '--group',
2187+ ])
2188+ cmd.append(group_name)
2189+ subprocess.check_call(cmd)
2190+
2191+
2192+def lsb_release():
2193+ """Return /etc/lsb-release in a dict"""
2194+ d = {}
2195+ with open('/etc/lsb-release', 'r') as lsb:
2196+ for l in lsb:
2197+ k, v = l.split('=')
2198+ d[k.strip()] = v.strip()
2199+ return d
2200+
2201+
2202+def cmp_pkgrevno(package, revno, pkgcache=None):
2203+ """Compare supplied revno with the revno of the installed package.
2204+
2205+ * 1 => Installed revno is greater than supplied arg
2206+ * 0 => Installed revno is the same as supplied arg
2207+ * -1 => Installed revno is less than supplied arg
2208+
2209+ This function imports apt_cache function from charmhelpers.fetch if
2210+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
2211+ you call this function, or pass an apt_pkg.Cache() instance.
2212+ """
2213+ import apt_pkg
2214+ if not pkgcache:
2215+ from charmhelpers.fetch import apt_cache
2216+ pkgcache = apt_cache()
2217+ pkg = pkgcache[package]
2218+ return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
2219
2220=== added file 'hooks/charmhelpers/core/hugepage.py'
2221--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
2222+++ hooks/charmhelpers/core/hugepage.py 2017-07-18 06:08:45 +0000
2223@@ -0,0 +1,69 @@
2224+# -*- coding: utf-8 -*-
2225+
2226+# Copyright 2014-2015 Canonical Limited.
2227+#
2228+# Licensed under the Apache License, Version 2.0 (the "License");
2229+# you may not use this file except in compliance with the License.
2230+# You may obtain a copy of the License at
2231+#
2232+# http://www.apache.org/licenses/LICENSE-2.0
2233+#
2234+# Unless required by applicable law or agreed to in writing, software
2235+# distributed under the License is distributed on an "AS IS" BASIS,
2236+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2237+# See the License for the specific language governing permissions and
2238+# limitations under the License.
2239+
2240+import yaml
2241+from charmhelpers.core import fstab
2242+from charmhelpers.core import sysctl
2243+from charmhelpers.core.host import (
2244+ add_group,
2245+ add_user_to_group,
2246+ fstab_mount,
2247+ mkdir,
2248+)
2249+from charmhelpers.core.strutils import bytes_from_string
2250+from subprocess import check_output
2251+
2252+
2253+def hugepage_support(user, group='hugetlb', nr_hugepages=256,
2254+ max_map_count=65536, mnt_point='/run/hugepages/kvm',
2255+ pagesize='2MB', mount=True, set_shmmax=False):
2256+ """Enable hugepages on system.
2257+
2258+ Args:
2259+ user (str) -- Username to allow access to hugepages to
2260+ group (str) -- Group name to own hugepages
2261+ nr_hugepages (int) -- Number of pages to reserve
2262+ max_map_count (int) -- Number of Virtual Memory Areas a process can own
2263+ mnt_point (str) -- Directory to mount hugepages on
2264+ pagesize (str) -- Size of hugepages
2265+ mount (bool) -- Whether to Mount hugepages
2266+ """
2267+ group_info = add_group(group)
2268+ gid = group_info.gr_gid
2269+ add_user_to_group(user, group)
2270+ if max_map_count < 2 * nr_hugepages:
2271+ max_map_count = 2 * nr_hugepages
2272+ sysctl_settings = {
2273+ 'vm.nr_hugepages': nr_hugepages,
2274+ 'vm.max_map_count': max_map_count,
2275+ 'vm.hugetlb_shm_group': gid,
2276+ }
2277+ if set_shmmax:
2278+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
2279+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
2280+ if shmmax_minsize > shmmax_current:
2281+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
2282+ sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
2283+ mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
2284+ lfstab = fstab.Fstab()
2285+ fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
2286+ if fstab_entry:
2287+ lfstab.remove_entry(fstab_entry)
2288+ entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
2289+ 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
2290+ lfstab.add_entry(entry)
2291+ if mount:
2292+ fstab_mount(mnt_point)
2293
2294=== added file 'hooks/charmhelpers/core/kernel.py'
2295--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
2296+++ hooks/charmhelpers/core/kernel.py 2017-07-18 06:08:45 +0000
2297@@ -0,0 +1,72 @@
2298+#!/usr/bin/env python
2299+# -*- coding: utf-8 -*-
2300+
2301+# Copyright 2014-2015 Canonical Limited.
2302+#
2303+# Licensed under the Apache License, Version 2.0 (the "License");
2304+# you may not use this file except in compliance with the License.
2305+# You may obtain a copy of the License at
2306+#
2307+# http://www.apache.org/licenses/LICENSE-2.0
2308+#
2309+# Unless required by applicable law or agreed to in writing, software
2310+# distributed under the License is distributed on an "AS IS" BASIS,
2311+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2312+# See the License for the specific language governing permissions and
2313+# limitations under the License.
2314+
2315+import re
2316+import subprocess
2317+
2318+from charmhelpers.osplatform import get_platform
2319+from charmhelpers.core.hookenv import (
2320+ log,
2321+ INFO
2322+)
2323+
2324+__platform__ = get_platform()
2325+if __platform__ == "ubuntu":
2326+ from charmhelpers.core.kernel_factory.ubuntu import (
2327+ persistent_modprobe,
2328+ update_initramfs,
2329+ ) # flake8: noqa -- ignore F401 for this import
2330+elif __platform__ == "centos":
2331+ from charmhelpers.core.kernel_factory.centos import (
2332+ persistent_modprobe,
2333+ update_initramfs,
2334+ ) # flake8: noqa -- ignore F401 for this import
2335+
2336+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
2337+
2338+
2339+def modprobe(module, persist=True):
2340+ """Load a kernel module and configure for auto-load on reboot."""
2341+ cmd = ['modprobe', module]
2342+
2343+ log('Loading kernel module %s' % module, level=INFO)
2344+
2345+ subprocess.check_call(cmd)
2346+ if persist:
2347+ persistent_modprobe(module)
2348+
2349+
2350+def rmmod(module, force=False):
2351+ """Remove a module from the linux kernel"""
2352+ cmd = ['rmmod']
2353+ if force:
2354+ cmd.append('-f')
2355+ cmd.append(module)
2356+ log('Removing kernel module %s' % module, level=INFO)
2357+ return subprocess.check_call(cmd)
2358+
2359+
2360+def lsmod():
2361+ """Shows what kernel modules are currently loaded"""
2362+ return subprocess.check_output(['lsmod'],
2363+ universal_newlines=True)
2364+
2365+
2366+def is_module_loaded(module):
2367+ """Checks if a kernel module is already loaded"""
2368+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
2369+ return len(matches) > 0
2370
2371=== added directory 'hooks/charmhelpers/core/kernel_factory'
2372=== added file 'hooks/charmhelpers/core/kernel_factory/__init__.py'
2373=== added file 'hooks/charmhelpers/core/kernel_factory/centos.py'
2374--- hooks/charmhelpers/core/kernel_factory/centos.py 1970-01-01 00:00:00 +0000
2375+++ hooks/charmhelpers/core/kernel_factory/centos.py 2017-07-18 06:08:45 +0000
2376@@ -0,0 +1,17 @@
2377+import subprocess
2378+import os
2379+
2380+
2381+def persistent_modprobe(module):
2382+ """Load a kernel module and configure for auto-load on reboot."""
2383+ if not os.path.exists('/etc/rc.modules'):
2384+ open('/etc/rc.modules', 'a')
2385+ os.chmod('/etc/rc.modules', 111)
2386+ with open('/etc/rc.modules', 'r+') as modules:
2387+ if module not in modules.read():
2388+ modules.write('modprobe %s\n' % module)
2389+
2390+
2391+def update_initramfs(version='all'):
2392+ """Updates an initramfs image."""
2393+ return subprocess.check_call(["dracut", "-f", version])
2394
2395=== added file 'hooks/charmhelpers/core/kernel_factory/ubuntu.py'
2396--- hooks/charmhelpers/core/kernel_factory/ubuntu.py 1970-01-01 00:00:00 +0000
2397+++ hooks/charmhelpers/core/kernel_factory/ubuntu.py 2017-07-18 06:08:45 +0000
2398@@ -0,0 +1,13 @@
2399+import subprocess
2400+
2401+
2402+def persistent_modprobe(module):
2403+ """Load a kernel module and configure for auto-load on reboot."""
2404+ with open('/etc/modules', 'r+') as modules:
2405+ if module not in modules.read():
2406+ modules.write(module + "\n")
2407+
2408+
2409+def update_initramfs(version='all'):
2410+ """Updates an initramfs image."""
2411+ return subprocess.check_call(["update-initramfs", "-k", version, "-u"])
2412
2413=== modified file 'hooks/charmhelpers/core/services/__init__.py'
2414--- hooks/charmhelpers/core/services/__init__.py 2015-04-08 05:57:06 +0000
2415+++ hooks/charmhelpers/core/services/__init__.py 2017-07-18 06:08:45 +0000
2416@@ -1,18 +1,16 @@
2417 # Copyright 2014-2015 Canonical Limited.
2418 #
2419-# This file is part of charm-helpers.
2420-#
2421-# charm-helpers is free software: you can redistribute it and/or modify
2422-# it under the terms of the GNU Lesser General Public License version 3 as
2423-# published by the Free Software Foundation.
2424-#
2425-# charm-helpers is distributed in the hope that it will be useful,
2426-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2427-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2428-# GNU Lesser General Public License for more details.
2429-#
2430-# You should have received a copy of the GNU Lesser General Public License
2431-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2432+# Licensed under the Apache License, Version 2.0 (the "License");
2433+# you may not use this file except in compliance with the License.
2434+# You may obtain a copy of the License at
2435+#
2436+# http://www.apache.org/licenses/LICENSE-2.0
2437+#
2438+# Unless required by applicable law or agreed to in writing, software
2439+# distributed under the License is distributed on an "AS IS" BASIS,
2440+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2441+# See the License for the specific language governing permissions and
2442+# limitations under the License.
2443
2444 from .base import * # NOQA
2445 from .helpers import * # NOQA
2446
2447=== modified file 'hooks/charmhelpers/core/services/base.py'
2448--- hooks/charmhelpers/core/services/base.py 2015-04-08 05:57:06 +0000
2449+++ hooks/charmhelpers/core/services/base.py 2017-07-18 06:08:45 +0000
2450@@ -1,23 +1,21 @@
2451 # Copyright 2014-2015 Canonical Limited.
2452 #
2453-# This file is part of charm-helpers.
2454-#
2455-# charm-helpers is free software: you can redistribute it and/or modify
2456-# it under the terms of the GNU Lesser General Public License version 3 as
2457-# published by the Free Software Foundation.
2458-#
2459-# charm-helpers is distributed in the hope that it will be useful,
2460-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2461-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2462-# GNU Lesser General Public License for more details.
2463-#
2464-# You should have received a copy of the GNU Lesser General Public License
2465-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2466+# Licensed under the Apache License, Version 2.0 (the "License");
2467+# you may not use this file except in compliance with the License.
2468+# You may obtain a copy of the License at
2469+#
2470+# http://www.apache.org/licenses/LICENSE-2.0
2471+#
2472+# Unless required by applicable law or agreed to in writing, software
2473+# distributed under the License is distributed on an "AS IS" BASIS,
2474+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2475+# See the License for the specific language governing permissions and
2476+# limitations under the License.
2477
2478 import os
2479-import re
2480 import json
2481-from collections import Iterable
2482+from inspect import getargspec
2483+from collections import Iterable, OrderedDict
2484
2485 from charmhelpers.core import host
2486 from charmhelpers.core import hookenv
2487@@ -119,7 +117,7 @@
2488 """
2489 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
2490 self._ready = None
2491- self.services = {}
2492+ self.services = OrderedDict()
2493 for service in services or []:
2494 service_name = service['service']
2495 self.services[service_name] = service
2496@@ -128,15 +126,18 @@
2497 """
2498 Handle the current hook by doing The Right Thing with the registered services.
2499 """
2500- hook_name = hookenv.hook_name()
2501- if hook_name == 'stop':
2502- self.stop_services()
2503- else:
2504- self.provide_data()
2505- self.reconfigure_services()
2506- cfg = hookenv.config()
2507- if cfg.implicit_save:
2508- cfg.save()
2509+ hookenv._run_atstart()
2510+ try:
2511+ hook_name = hookenv.hook_name()
2512+ if hook_name == 'stop':
2513+ self.stop_services()
2514+ else:
2515+ self.reconfigure_services()
2516+ self.provide_data()
2517+ except SystemExit as x:
2518+ if x.code is None or x.code == 0:
2519+ hookenv._run_atexit()
2520+ hookenv._run_atexit()
2521
2522 def provide_data(self):
2523 """
2524@@ -145,15 +146,36 @@
2525 A provider must have a `name` attribute, which indicates which relation
2526 to set data on, and a `provide_data()` method, which returns a dict of
2527 data to set.
2528+
2529+ The `provide_data()` method can optionally accept two parameters:
2530+
2531+ * ``remote_service`` The name of the remote service that the data will
2532+ be provided to. The `provide_data()` method will be called once
2533+ for each connected service (not unit). This allows the method to
2534+ tailor its data to the given service.
2535+ * ``service_ready`` Whether or not the service definition had all of
2536+ its requirements met, and thus the ``data_ready`` callbacks run.
2537+
2538+ Note that the ``provided_data`` methods are now called **after** the
2539+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
2540+ a chance to generate any data necessary for the providing to the remote
2541+ services.
2542 """
2543- hook_name = hookenv.hook_name()
2544- for service in self.services.values():
2545+ for service_name, service in self.services.items():
2546+ service_ready = self.is_ready(service_name)
2547 for provider in service.get('provided_data', []):
2548- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
2549- data = provider.provide_data()
2550- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
2551- if _ready:
2552- hookenv.relation_set(None, data)
2553+ for relid in hookenv.relation_ids(provider.name):
2554+ units = hookenv.related_units(relid)
2555+ if not units:
2556+ continue
2557+ remote_service = units[0].split('/')[0]
2558+ argspec = getargspec(provider.provide_data)
2559+ if len(argspec.args) > 1:
2560+ data = provider.provide_data(remote_service, service_ready)
2561+ else:
2562+ data = provider.provide_data()
2563+ if data:
2564+ hookenv.relation_set(relid, data)
2565
2566 def reconfigure_services(self, *service_names):
2567 """
2568
2569=== modified file 'hooks/charmhelpers/core/services/helpers.py'
2570--- hooks/charmhelpers/core/services/helpers.py 2015-04-08 05:57:06 +0000
2571+++ hooks/charmhelpers/core/services/helpers.py 2017-07-18 06:08:45 +0000
2572@@ -1,22 +1,22 @@
2573 # Copyright 2014-2015 Canonical Limited.
2574 #
2575-# This file is part of charm-helpers.
2576-#
2577-# charm-helpers is free software: you can redistribute it and/or modify
2578-# it under the terms of the GNU Lesser General Public License version 3 as
2579-# published by the Free Software Foundation.
2580-#
2581-# charm-helpers is distributed in the hope that it will be useful,
2582-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2583-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2584-# GNU Lesser General Public License for more details.
2585-#
2586-# You should have received a copy of the GNU Lesser General Public License
2587-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2588+# Licensed under the Apache License, Version 2.0 (the "License");
2589+# you may not use this file except in compliance with the License.
2590+# You may obtain a copy of the License at
2591+#
2592+# http://www.apache.org/licenses/LICENSE-2.0
2593+#
2594+# Unless required by applicable law or agreed to in writing, software
2595+# distributed under the License is distributed on an "AS IS" BASIS,
2596+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2597+# See the License for the specific language governing permissions and
2598+# limitations under the License.
2599
2600 import os
2601 import yaml
2602+
2603 from charmhelpers.core import hookenv
2604+from charmhelpers.core import host
2605 from charmhelpers.core import templating
2606
2607 from charmhelpers.core.services.base import ManagerCallback
2608@@ -239,28 +239,51 @@
2609 action.
2610
2611 :param str source: The template source file, relative to
2612- `$CHARM_DIR/templates`
2613+ `$CHARM_DIR/templates`
2614
2615- :param str target: The target to write the rendered template to
2616+ :param str target: The target to write the rendered template to (or None)
2617 :param str owner: The owner of the rendered file
2618 :param str group: The group of the rendered file
2619 :param int perms: The permissions of the rendered file
2620+ :param partial on_change_action: functools partial to be executed when
2621+ rendered file changes
2622+ :param jinja2 loader template_loader: A jinja2 template loader
2623+
2624+ :return str: The rendered template
2625 """
2626 def __init__(self, source, target,
2627- owner='root', group='root', perms=0o444):
2628+ owner='root', group='root', perms=0o444,
2629+ on_change_action=None, template_loader=None):
2630 self.source = source
2631 self.target = target
2632 self.owner = owner
2633 self.group = group
2634 self.perms = perms
2635+ self.on_change_action = on_change_action
2636+ self.template_loader = template_loader
2637
2638 def __call__(self, manager, service_name, event_name):
2639+ pre_checksum = ''
2640+ if self.on_change_action and os.path.isfile(self.target):
2641+ pre_checksum = host.file_hash(self.target)
2642 service = manager.get_service(service_name)
2643- context = {}
2644+ context = {'ctx': {}}
2645 for ctx in service.get('required_data', []):
2646 context.update(ctx)
2647- templating.render(self.source, self.target, context,
2648- self.owner, self.group, self.perms)
2649+ context['ctx'].update(ctx)
2650+
2651+ result = templating.render(self.source, self.target, context,
2652+ self.owner, self.group, self.perms,
2653+ template_loader=self.template_loader)
2654+ if self.on_change_action:
2655+ if pre_checksum == host.file_hash(self.target):
2656+ hookenv.log(
2657+ 'No change detected: {}'.format(self.target),
2658+ hookenv.DEBUG)
2659+ else:
2660+ self.on_change_action()
2661+
2662+ return result
2663
2664
2665 # Convenience aliases for templates
2666
2667=== modified file 'hooks/charmhelpers/core/strutils.py'
2668--- hooks/charmhelpers/core/strutils.py 2015-04-08 05:57:06 +0000
2669+++ hooks/charmhelpers/core/strutils.py 2017-07-18 06:08:45 +0000
2670@@ -3,21 +3,20 @@
2671
2672 # Copyright 2014-2015 Canonical Limited.
2673 #
2674-# This file is part of charm-helpers.
2675-#
2676-# charm-helpers is free software: you can redistribute it and/or modify
2677-# it under the terms of the GNU Lesser General Public License version 3 as
2678-# published by the Free Software Foundation.
2679-#
2680-# charm-helpers is distributed in the hope that it will be useful,
2681-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2682-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2683-# GNU Lesser General Public License for more details.
2684-#
2685-# You should have received a copy of the GNU Lesser General Public License
2686-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2687+# Licensed under the Apache License, Version 2.0 (the "License");
2688+# you may not use this file except in compliance with the License.
2689+# You may obtain a copy of the License at
2690+#
2691+# http://www.apache.org/licenses/LICENSE-2.0
2692+#
2693+# Unless required by applicable law or agreed to in writing, software
2694+# distributed under the License is distributed on an "AS IS" BASIS,
2695+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2696+# See the License for the specific language governing permissions and
2697+# limitations under the License.
2698
2699 import six
2700+import re
2701
2702
2703 def bool_from_string(value):
2704@@ -33,10 +32,92 @@
2705
2706 value = value.strip().lower()
2707
2708- if value in ['y', 'yes', 'true', 't']:
2709+ if value in ['y', 'yes', 'true', 't', 'on']:
2710 return True
2711- elif value in ['n', 'no', 'false', 'f']:
2712+ elif value in ['n', 'no', 'false', 'f', 'off']:
2713 return False
2714
2715 msg = "Unable to interpret string value '%s' as boolean" % (value)
2716 raise ValueError(msg)
2717+
2718+
2719+def bytes_from_string(value):
2720+ """Interpret human readable string value as bytes.
2721+
2722+ Returns int
2723+ """
2724+ BYTE_POWER = {
2725+ 'K': 1,
2726+ 'KB': 1,
2727+ 'M': 2,
2728+ 'MB': 2,
2729+ 'G': 3,
2730+ 'GB': 3,
2731+ 'T': 4,
2732+ 'TB': 4,
2733+ 'P': 5,
2734+ 'PB': 5,
2735+ }
2736+ if isinstance(value, six.string_types):
2737+ value = six.text_type(value)
2738+ else:
2739+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
2740+ raise ValueError(msg)
2741+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
2742+ if not matches:
2743+ msg = "Unable to interpret string value '%s' as bytes" % (value)
2744+ raise ValueError(msg)
2745+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
2746+
2747+
2748+class BasicStringComparator(object):
2749+ """Provides a class that will compare strings from an iterator type object.
2750+ Used to provide > and < comparisons on strings that may not necessarily be
2751+ alphanumerically ordered. e.g. OpenStack or Ubuntu releases AFTER the
2752+ z-wrap.
2753+ """
2754+
2755+ _list = None
2756+
2757+ def __init__(self, item):
2758+ if self._list is None:
2759+ raise Exception("Must define the _list in the class definition!")
2760+ try:
2761+ self.index = self._list.index(item)
2762+ except Exception:
2763+ raise KeyError("Item '{}' is not in list '{}'"
2764+ .format(item, self._list))
2765+
2766+ def __eq__(self, other):
2767+ assert isinstance(other, str) or isinstance(other, self.__class__)
2768+ return self.index == self._list.index(other)
2769+
2770+ def __ne__(self, other):
2771+ return not self.__eq__(other)
2772+
2773+ def __lt__(self, other):
2774+ assert isinstance(other, str) or isinstance(other, self.__class__)
2775+ return self.index < self._list.index(other)
2776+
2777+ def __ge__(self, other):
2778+ return not self.__lt__(other)
2779+
2780+ def __gt__(self, other):
2781+ assert isinstance(other, str) or isinstance(other, self.__class__)
2782+ return self.index > self._list.index(other)
2783+
2784+ def __le__(self, other):
2785+ return not self.__gt__(other)
2786+
2787+ def __str__(self):
2788+ """Always give back the item at the index so it can be used in
2789+ comparisons like:
2790+
2791+ s_mitaka = CompareOpenStack('mitaka')
2792+ s_newton = CompareOpenstack('newton')
2793+
2794+ assert s_newton > s_mitaka
2795+
2796+ @returns: <string>
2797+ """
2798+ return self._list[self.index]
2799
2800=== modified file 'hooks/charmhelpers/core/sysctl.py'
2801--- hooks/charmhelpers/core/sysctl.py 2015-04-08 05:57:06 +0000
2802+++ hooks/charmhelpers/core/sysctl.py 2017-07-18 06:08:45 +0000
2803@@ -3,19 +3,17 @@
2804
2805 # Copyright 2014-2015 Canonical Limited.
2806 #
2807-# This file is part of charm-helpers.
2808-#
2809-# charm-helpers is free software: you can redistribute it and/or modify
2810-# it under the terms of the GNU Lesser General Public License version 3 as
2811-# published by the Free Software Foundation.
2812-#
2813-# charm-helpers is distributed in the hope that it will be useful,
2814-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2815-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2816-# GNU Lesser General Public License for more details.
2817-#
2818-# You should have received a copy of the GNU Lesser General Public License
2819-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2820+# Licensed under the Apache License, Version 2.0 (the "License");
2821+# you may not use this file except in compliance with the License.
2822+# You may obtain a copy of the License at
2823+#
2824+# http://www.apache.org/licenses/LICENSE-2.0
2825+#
2826+# Unless required by applicable law or agreed to in writing, software
2827+# distributed under the License is distributed on an "AS IS" BASIS,
2828+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2829+# See the License for the specific language governing permissions and
2830+# limitations under the License.
2831
2832 import yaml
2833
2834
2835=== modified file 'hooks/charmhelpers/core/templating.py'
2836--- hooks/charmhelpers/core/templating.py 2015-04-08 05:57:06 +0000
2837+++ hooks/charmhelpers/core/templating.py 2017-07-18 06:08:45 +0000
2838@@ -1,33 +1,33 @@
2839 # Copyright 2014-2015 Canonical Limited.
2840 #
2841-# This file is part of charm-helpers.
2842-#
2843-# charm-helpers is free software: you can redistribute it and/or modify
2844-# it under the terms of the GNU Lesser General Public License version 3 as
2845-# published by the Free Software Foundation.
2846-#
2847-# charm-helpers is distributed in the hope that it will be useful,
2848-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2849-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2850-# GNU Lesser General Public License for more details.
2851-#
2852-# You should have received a copy of the GNU Lesser General Public License
2853-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2854+# Licensed under the Apache License, Version 2.0 (the "License");
2855+# you may not use this file except in compliance with the License.
2856+# You may obtain a copy of the License at
2857+#
2858+# http://www.apache.org/licenses/LICENSE-2.0
2859+#
2860+# Unless required by applicable law or agreed to in writing, software
2861+# distributed under the License is distributed on an "AS IS" BASIS,
2862+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2863+# See the License for the specific language governing permissions and
2864+# limitations under the License.
2865
2866 import os
2867+import sys
2868
2869 from charmhelpers.core import host
2870 from charmhelpers.core import hookenv
2871
2872
2873 def render(source, target, context, owner='root', group='root',
2874- perms=0o444, templates_dir=None, encoding='UTF-8'):
2875+ perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
2876 """
2877 Render a template.
2878
2879 The `source` path, if not absolute, is relative to the `templates_dir`.
2880
2881- The `target` path should be absolute.
2882+ The `target` path should be absolute. It can also be `None`, in which
2883+ case no file will be written.
2884
2885 The context should be a dict containing the values to be replaced in the
2886 template.
2887@@ -36,8 +36,12 @@
2888
2889 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
2890
2891- Note: Using this requires python-jinja2; if it is not installed, calling
2892- this will attempt to use charmhelpers.fetch.apt_install to install it.
2893+ The rendered template will be written to the file as well as being returned
2894+ as a string.
2895+
2896+ Note: Using this requires python-jinja2 or python3-jinja2; if it is not
2897+ installed, calling this will attempt to use charmhelpers.fetch.apt_install
2898+ to install it.
2899 """
2900 try:
2901 from jinja2 import FileSystemLoader, Environment, exceptions
2902@@ -49,20 +53,32 @@
2903 'charmhelpers.fetch to install it',
2904 level=hookenv.ERROR)
2905 raise
2906- apt_install('python-jinja2', fatal=True)
2907+ if sys.version_info.major == 2:
2908+ apt_install('python-jinja2', fatal=True)
2909+ else:
2910+ apt_install('python3-jinja2', fatal=True)
2911 from jinja2 import FileSystemLoader, Environment, exceptions
2912
2913- if templates_dir is None:
2914- templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
2915- loader = Environment(loader=FileSystemLoader(templates_dir))
2916+ if template_loader:
2917+ template_env = Environment(loader=template_loader)
2918+ else:
2919+ if templates_dir is None:
2920+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
2921+ template_env = Environment(loader=FileSystemLoader(templates_dir))
2922 try:
2923 source = source
2924- template = loader.get_template(source)
2925+ template = template_env.get_template(source)
2926 except exceptions.TemplateNotFound as e:
2927 hookenv.log('Could not load template %s from %s.' %
2928 (source, templates_dir),
2929 level=hookenv.ERROR)
2930 raise e
2931 content = template.render(context)
2932- host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
2933- host.write_file(target, content.encode(encoding), owner, group, perms)
2934+ if target is not None:
2935+ target_dir = os.path.dirname(target)
2936+ if not os.path.exists(target_dir):
2937+ # This is a terrible default directory permission, as the file
2938+ # or its siblings will often contain secrets.
2939+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
2940+ host.write_file(target, content.encode(encoding), owner, group, perms)
2941+ return content
2942
2943=== modified file 'hooks/charmhelpers/core/unitdata.py'
2944--- hooks/charmhelpers/core/unitdata.py 2015-04-08 05:57:06 +0000
2945+++ hooks/charmhelpers/core/unitdata.py 2017-07-18 06:08:45 +0000
2946@@ -3,20 +3,17 @@
2947 #
2948 # Copyright 2014-2015 Canonical Limited.
2949 #
2950-# This file is part of charm-helpers.
2951-#
2952-# charm-helpers is free software: you can redistribute it and/or modify
2953-# it under the terms of the GNU Lesser General Public License version 3 as
2954-# published by the Free Software Foundation.
2955-#
2956-# charm-helpers is distributed in the hope that it will be useful,
2957-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2958-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2959-# GNU Lesser General Public License for more details.
2960-#
2961-# You should have received a copy of the GNU Lesser General Public License
2962-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2963-#
2964+# Licensed under the Apache License, Version 2.0 (the "License");
2965+# you may not use this file except in compliance with the License.
2966+# You may obtain a copy of the License at
2967+#
2968+# http://www.apache.org/licenses/LICENSE-2.0
2969+#
2970+# Unless required by applicable law or agreed to in writing, software
2971+# distributed under the License is distributed on an "AS IS" BASIS,
2972+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2973+# See the License for the specific language governing permissions and
2974+# limitations under the License.
2975 #
2976 # Authors:
2977 # Kapil Thangavelu <kapil.foss@gmail.com>
2978@@ -152,6 +149,7 @@
2979 import collections
2980 import contextlib
2981 import datetime
2982+import itertools
2983 import json
2984 import os
2985 import pprint
2986@@ -164,8 +162,7 @@
2987 class Storage(object):
2988 """Simple key value database for local unit state within charms.
2989
2990- Modifications are automatically committed at hook exit. That's
2991- currently regardless of exit code.
2992+ Modifications are not persisted unless :meth:`flush` is called.
2993
2994 To support dicts, lists, integer, floats, and booleans values
2995 are automatically json encoded/decoded.
2996@@ -173,8 +170,11 @@
2997 def __init__(self, path=None):
2998 self.db_path = path
2999 if path is None:
3000- self.db_path = os.path.join(
3001- os.environ.get('CHARM_DIR', ''), '.unit-state.db')
3002+ if 'UNIT_STATE_DB' in os.environ:
3003+ self.db_path = os.environ['UNIT_STATE_DB']
3004+ else:
3005+ self.db_path = os.path.join(
3006+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
3007 self.conn = sqlite3.connect('%s' % self.db_path)
3008 self.cursor = self.conn.cursor()
3009 self.revision = None
3010@@ -189,15 +189,8 @@
3011 self.conn.close()
3012 self._closed = True
3013
3014- def _scoped_query(self, stmt, params=None):
3015- if params is None:
3016- params = []
3017- return stmt, params
3018-
3019 def get(self, key, default=None, record=False):
3020- self.cursor.execute(
3021- *self._scoped_query(
3022- 'select data from kv where key=?', [key]))
3023+ self.cursor.execute('select data from kv where key=?', [key])
3024 result = self.cursor.fetchone()
3025 if not result:
3026 return default
3027@@ -206,33 +199,81 @@
3028 return json.loads(result[0])
3029
3030 def getrange(self, key_prefix, strip=False):
3031- stmt = "select key, data from kv where key like '%s%%'" % key_prefix
3032- self.cursor.execute(*self._scoped_query(stmt))
3033+ """
3034+ Get a range of keys starting with a common prefix as a mapping of
3035+ keys to values.
3036+
3037+ :param str key_prefix: Common prefix among all keys
3038+ :param bool strip: Optionally strip the common prefix from the key
3039+ names in the returned dict
3040+ :return dict: A (possibly empty) dict of key-value mappings
3041+ """
3042+ self.cursor.execute("select key, data from kv where key like ?",
3043+ ['%s%%' % key_prefix])
3044 result = self.cursor.fetchall()
3045
3046 if not result:
3047- return None
3048+ return {}
3049 if not strip:
3050 key_prefix = ''
3051 return dict([
3052 (k[len(key_prefix):], json.loads(v)) for k, v in result])
3053
3054 def update(self, mapping, prefix=""):
3055+ """
3056+ Set the values of multiple keys at once.
3057+
3058+ :param dict mapping: Mapping of keys to values
3059+ :param str prefix: Optional prefix to apply to all keys in `mapping`
3060+ before setting
3061+ """
3062 for k, v in mapping.items():
3063 self.set("%s%s" % (prefix, k), v)
3064
3065 def unset(self, key):
3066+ """
3067+ Remove a key from the database entirely.
3068+ """
3069 self.cursor.execute('delete from kv where key=?', [key])
3070 if self.revision and self.cursor.rowcount:
3071 self.cursor.execute(
3072 'insert into kv_revisions values (?, ?, ?)',
3073 [key, self.revision, json.dumps('DELETED')])
3074
3075+ def unsetrange(self, keys=None, prefix=""):
3076+ """
3077+ Remove a range of keys starting with a common prefix, from the database
3078+ entirely.
3079+
3080+ :param list keys: List of keys to remove.
3081+ :param str prefix: Optional prefix to apply to all keys in ``keys``
3082+ before removing.
3083+ """
3084+ if keys is not None:
3085+ keys = ['%s%s' % (prefix, key) for key in keys]
3086+ self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
3087+ if self.revision and self.cursor.rowcount:
3088+ self.cursor.execute(
3089+ 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
3090+ list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
3091+ else:
3092+ self.cursor.execute('delete from kv where key like ?',
3093+ ['%s%%' % prefix])
3094+ if self.revision and self.cursor.rowcount:
3095+ self.cursor.execute(
3096+ 'insert into kv_revisions values (?, ?, ?)',
3097+ ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
3098+
3099 def set(self, key, value):
3100+ """
3101+ Set a value in the database.
3102+
3103+ :param str key: Key to set the value for
3104+ :param value: Any JSON-serializable value to be set
3105+ """
3106 serialized = json.dumps(value)
3107
3108- self.cursor.execute(
3109- 'select data from kv where key=?', [key])
3110+ self.cursor.execute('select data from kv where key=?', [key])
3111 exists = self.cursor.fetchone()
3112
3113 # Skip mutations to the same value
3114
3115=== modified file 'hooks/charmhelpers/fetch/__init__.py'
3116--- hooks/charmhelpers/fetch/__init__.py 2015-04-08 05:57:06 +0000
3117+++ hooks/charmhelpers/fetch/__init__.py 2017-07-18 06:08:45 +0000
3118@@ -1,32 +1,24 @@
3119 # Copyright 2014-2015 Canonical Limited.
3120 #
3121-# This file is part of charm-helpers.
3122-#
3123-# charm-helpers is free software: you can redistribute it and/or modify
3124-# it under the terms of the GNU Lesser General Public License version 3 as
3125-# published by the Free Software Foundation.
3126-#
3127-# charm-helpers is distributed in the hope that it will be useful,
3128-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3129-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3130-# GNU Lesser General Public License for more details.
3131-#
3132-# You should have received a copy of the GNU Lesser General Public License
3133-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3134+# Licensed under the Apache License, Version 2.0 (the "License");
3135+# you may not use this file except in compliance with the License.
3136+# You may obtain a copy of the License at
3137+#
3138+# http://www.apache.org/licenses/LICENSE-2.0
3139+#
3140+# Unless required by applicable law or agreed to in writing, software
3141+# distributed under the License is distributed on an "AS IS" BASIS,
3142+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3143+# See the License for the specific language governing permissions and
3144+# limitations under the License.
3145
3146 import importlib
3147-from tempfile import NamedTemporaryFile
3148-import time
3149+from charmhelpers.osplatform import get_platform
3150 from yaml import safe_load
3151-from charmhelpers.core.host import (
3152- lsb_release
3153-)
3154-import subprocess
3155 from charmhelpers.core.hookenv import (
3156 config,
3157 log,
3158 )
3159-import os
3160
3161 import six
3162 if six.PY3:
3163@@ -35,63 +27,6 @@
3164 from urlparse import urlparse, urlunparse
3165
3166
3167-CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
3168-deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
3169-"""
3170-PROPOSED_POCKET = """# Proposed
3171-deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
3172-"""
3173-CLOUD_ARCHIVE_POCKETS = {
3174- # Folsom
3175- 'folsom': 'precise-updates/folsom',
3176- 'precise-folsom': 'precise-updates/folsom',
3177- 'precise-folsom/updates': 'precise-updates/folsom',
3178- 'precise-updates/folsom': 'precise-updates/folsom',
3179- 'folsom/proposed': 'precise-proposed/folsom',
3180- 'precise-folsom/proposed': 'precise-proposed/folsom',
3181- 'precise-proposed/folsom': 'precise-proposed/folsom',
3182- # Grizzly
3183- 'grizzly': 'precise-updates/grizzly',
3184- 'precise-grizzly': 'precise-updates/grizzly',
3185- 'precise-grizzly/updates': 'precise-updates/grizzly',
3186- 'precise-updates/grizzly': 'precise-updates/grizzly',
3187- 'grizzly/proposed': 'precise-proposed/grizzly',
3188- 'precise-grizzly/proposed': 'precise-proposed/grizzly',
3189- 'precise-proposed/grizzly': 'precise-proposed/grizzly',
3190- # Havana
3191- 'havana': 'precise-updates/havana',
3192- 'precise-havana': 'precise-updates/havana',
3193- 'precise-havana/updates': 'precise-updates/havana',
3194- 'precise-updates/havana': 'precise-updates/havana',
3195- 'havana/proposed': 'precise-proposed/havana',
3196- 'precise-havana/proposed': 'precise-proposed/havana',
3197- 'precise-proposed/havana': 'precise-proposed/havana',
3198- # Icehouse
3199- 'icehouse': 'precise-updates/icehouse',
3200- 'precise-icehouse': 'precise-updates/icehouse',
3201- 'precise-icehouse/updates': 'precise-updates/icehouse',
3202- 'precise-updates/icehouse': 'precise-updates/icehouse',
3203- 'icehouse/proposed': 'precise-proposed/icehouse',
3204- 'precise-icehouse/proposed': 'precise-proposed/icehouse',
3205- 'precise-proposed/icehouse': 'precise-proposed/icehouse',
3206- # Juno
3207- 'juno': 'trusty-updates/juno',
3208- 'trusty-juno': 'trusty-updates/juno',
3209- 'trusty-juno/updates': 'trusty-updates/juno',
3210- 'trusty-updates/juno': 'trusty-updates/juno',
3211- 'juno/proposed': 'trusty-proposed/juno',
3212- 'trusty-juno/proposed': 'trusty-proposed/juno',
3213- 'trusty-proposed/juno': 'trusty-proposed/juno',
3214- # Kilo
3215- 'kilo': 'trusty-updates/kilo',
3216- 'trusty-kilo': 'trusty-updates/kilo',
3217- 'trusty-kilo/updates': 'trusty-updates/kilo',
3218- 'trusty-updates/kilo': 'trusty-updates/kilo',
3219- 'kilo/proposed': 'trusty-proposed/kilo',
3220- 'trusty-kilo/proposed': 'trusty-proposed/kilo',
3221- 'trusty-proposed/kilo': 'trusty-proposed/kilo',
3222-}
3223-
3224 # The order of this list is very important. Handlers should be listed in from
3225 # least- to most-specific URL matching.
3226 FETCH_HANDLERS = (
3227@@ -100,10 +35,6 @@
3228 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
3229 )
3230
3231-APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
3232-APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
3233-APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
3234-
3235
3236 class SourceConfigError(Exception):
3237 pass
3238@@ -117,6 +48,13 @@
3239 pass
3240
3241
3242+class GPGKeyError(Exception):
3243+ """Exception occurs when a GPG key cannot be fetched or used. The message
3244+ indicates what the problem is.
3245+ """
3246+ pass
3247+
3248+
3249 class BaseFetchHandler(object):
3250
3251 """Base class for FetchHandler implementations in fetch plugins"""
3252@@ -141,172 +79,39 @@
3253 return urlunparse(parts)
3254
3255
3256-def filter_installed_packages(packages):
3257- """Returns a list of packages that require installation"""
3258- cache = apt_cache()
3259- _pkgs = []
3260- for package in packages:
3261- try:
3262- p = cache[package]
3263- p.current_ver or _pkgs.append(package)
3264- except KeyError:
3265- log('Package {} has no installation candidate.'.format(package),
3266- level='WARNING')
3267- _pkgs.append(package)
3268- return _pkgs
3269-
3270-
3271-def apt_cache(in_memory=True):
3272- """Build and return an apt cache"""
3273- import apt_pkg
3274- apt_pkg.init()
3275- if in_memory:
3276- apt_pkg.config.set("Dir::Cache::pkgcache", "")
3277- apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
3278- return apt_pkg.Cache()
3279-
3280-
3281-def apt_install(packages, options=None, fatal=False):
3282- """Install one or more packages"""
3283- if options is None:
3284- options = ['--option=Dpkg::Options::=--force-confold']
3285-
3286- cmd = ['apt-get', '--assume-yes']
3287- cmd.extend(options)
3288- cmd.append('install')
3289- if isinstance(packages, six.string_types):
3290- cmd.append(packages)
3291- else:
3292- cmd.extend(packages)
3293- log("Installing {} with options: {}".format(packages,
3294- options))
3295- _run_apt_command(cmd, fatal)
3296-
3297-
3298-def apt_upgrade(options=None, fatal=False, dist=False):
3299- """Upgrade all packages"""
3300- if options is None:
3301- options = ['--option=Dpkg::Options::=--force-confold']
3302-
3303- cmd = ['apt-get', '--assume-yes']
3304- cmd.extend(options)
3305- if dist:
3306- cmd.append('dist-upgrade')
3307- else:
3308- cmd.append('upgrade')
3309- log("Upgrading with options: {}".format(options))
3310- _run_apt_command(cmd, fatal)
3311-
3312-
3313-def apt_update(fatal=False):
3314- """Update local apt cache"""
3315- cmd = ['apt-get', 'update']
3316- _run_apt_command(cmd, fatal)
3317-
3318-
3319-def apt_purge(packages, fatal=False):
3320- """Purge one or more packages"""
3321- cmd = ['apt-get', '--assume-yes', 'purge']
3322- if isinstance(packages, six.string_types):
3323- cmd.append(packages)
3324- else:
3325- cmd.extend(packages)
3326- log("Purging {}".format(packages))
3327- _run_apt_command(cmd, fatal)
3328-
3329-
3330-def apt_hold(packages, fatal=False):
3331- """Hold one or more packages"""
3332- cmd = ['apt-mark', 'hold']
3333- if isinstance(packages, six.string_types):
3334- cmd.append(packages)
3335- else:
3336- cmd.extend(packages)
3337- log("Holding {}".format(packages))
3338-
3339- if fatal:
3340- subprocess.check_call(cmd)
3341- else:
3342- subprocess.call(cmd)
3343-
3344-
3345-def add_source(source, key=None):
3346- """Add a package source to this system.
3347-
3348- @param source: a URL or sources.list entry, as supported by
3349- add-apt-repository(1). Examples::
3350-
3351- ppa:charmers/example
3352- deb https://stub:key@private.example.com/ubuntu trusty main
3353-
3354- In addition:
3355- 'proposed:' may be used to enable the standard 'proposed'
3356- pocket for the release.
3357- 'cloud:' may be used to activate official cloud archive pockets,
3358- such as 'cloud:icehouse'
3359- 'distro' may be used as a noop
3360-
3361- @param key: A key to be added to the system's APT keyring and used
3362- to verify the signatures on packages. Ideally, this should be an
3363- ASCII format GPG public key including the block headers. A GPG key
3364- id may also be used, but be aware that only insecure protocols are
3365- available to retrieve the actual public key from a public keyserver
3366- placing your Juju environment at risk. ppa and cloud archive keys
3367- are securely added automtically, so sould not be provided.
3368- """
3369- if source is None:
3370- log('Source is not present. Skipping')
3371- return
3372-
3373- if (source.startswith('ppa:') or
3374- source.startswith('http') or
3375- source.startswith('deb ') or
3376- source.startswith('cloud-archive:')):
3377- subprocess.check_call(['add-apt-repository', '--yes', source])
3378- elif source.startswith('cloud:'):
3379- apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
3380- fatal=True)
3381- pocket = source.split(':')[-1]
3382- if pocket not in CLOUD_ARCHIVE_POCKETS:
3383- raise SourceConfigError(
3384- 'Unsupported cloud: source option %s' %
3385- pocket)
3386- actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
3387- with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
3388- apt.write(CLOUD_ARCHIVE.format(actual_pocket))
3389- elif source == 'proposed':
3390- release = lsb_release()['DISTRIB_CODENAME']
3391- with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
3392- apt.write(PROPOSED_POCKET.format(release))
3393- elif source == 'distro':
3394- pass
3395- else:
3396- log("Unknown source: {!r}".format(source))
3397-
3398- if key:
3399- if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
3400- with NamedTemporaryFile('w+') as key_file:
3401- key_file.write(key)
3402- key_file.flush()
3403- key_file.seek(0)
3404- subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
3405- else:
3406- # Note that hkp: is in no way a secure protocol. Using a
3407- # GPG key id is pointless from a security POV unless you
3408- # absolutely trust your network and DNS.
3409- subprocess.check_call(['apt-key', 'adv', '--keyserver',
3410- 'hkp://keyserver.ubuntu.com:80', '--recv',
3411- key])
3412+__platform__ = get_platform()
3413+module = "charmhelpers.fetch.%s" % __platform__
3414+fetch = importlib.import_module(module)
3415+
3416+filter_installed_packages = fetch.filter_installed_packages
3417+install = fetch.apt_install
3418+upgrade = fetch.apt_upgrade
3419+update = _fetch_update = fetch.apt_update
3420+purge = fetch.apt_purge
3421+add_source = fetch.add_source
3422+
3423+if __platform__ == "ubuntu":
3424+ apt_cache = fetch.apt_cache
3425+ apt_install = fetch.apt_install
3426+ apt_update = fetch.apt_update
3427+ apt_upgrade = fetch.apt_upgrade
3428+ apt_purge = fetch.apt_purge
3429+ apt_mark = fetch.apt_mark
3430+ apt_hold = fetch.apt_hold
3431+ apt_unhold = fetch.apt_unhold
3432+ import_key = fetch.import_key
3433+ get_upstream_version = fetch.get_upstream_version
3434+elif __platform__ == "centos":
3435+ yum_search = fetch.yum_search
3436
3437
3438 def configure_sources(update=False,
3439 sources_var='install_sources',
3440 keys_var='install_keys'):
3441- """
3442- Configure multiple sources from charm configuration.
3443+ """Configure multiple sources from charm configuration.
3444
3445 The lists are encoded as yaml fragments in the configuration.
3446- The frament needs to be included as a string. Sources and their
3447+ The fragment needs to be included as a string. Sources and their
3448 corresponding keys are of the types supported by add_source().
3449
3450 Example config:
3451@@ -338,12 +143,11 @@
3452 for source, key in zip(sources, keys):
3453 add_source(source, key)
3454 if update:
3455- apt_update(fatal=True)
3456+ _fetch_update(fatal=True)
3457
3458
3459 def install_remote(source, *args, **kwargs):
3460- """
3461- Install a file tree from a remote source
3462+ """Install a file tree from a remote source.
3463
3464 The specified source should be a url of the form:
3465 scheme://[host]/path[#[option=value][&...]]
3466@@ -366,18 +170,17 @@
3467 # We ONLY check for True here because can_handle may return a string
3468 # explaining why it can't handle a given source.
3469 handlers = [h for h in plugins() if h.can_handle(source) is True]
3470- installed_to = None
3471 for handler in handlers:
3472 try:
3473- installed_to = handler.install(source, *args, **kwargs)
3474- except UnhandledSource:
3475- pass
3476- if not installed_to:
3477- raise UnhandledSource("No handler found for source {}".format(source))
3478- return installed_to
3479+ return handler.install(source, *args, **kwargs)
3480+ except UnhandledSource as e:
3481+ log('Install source attempt unsuccessful: {}'.format(e),
3482+ level='WARNING')
3483+ raise UnhandledSource("No handler found for source {}".format(source))
3484
3485
3486 def install_from_config(config_var_name):
3487+ """Install a file from config."""
3488 charm_config = config()
3489 source = charm_config[config_var_name]
3490 return install_remote(source)
3491@@ -394,46 +197,9 @@
3492 importlib.import_module(package),
3493 classname)
3494 plugin_list.append(handler_class())
3495- except (ImportError, AttributeError):
3496+ except NotImplementedError:
3497 # Skip missing plugins so that they can be ommitted from
3498 # installation if desired
3499 log("FetchHandler {} not found, skipping plugin".format(
3500 handler_name))
3501 return plugin_list
3502-
3503-
3504-def _run_apt_command(cmd, fatal=False):
3505- """
3506- Run an APT command, checking output and retrying if the fatal flag is set
3507- to True.
3508-
3509- :param: cmd: str: The apt command to run.
3510- :param: fatal: bool: Whether the command's output should be checked and
3511- retried.
3512- """
3513- env = os.environ.copy()
3514-
3515- if 'DEBIAN_FRONTEND' not in env:
3516- env['DEBIAN_FRONTEND'] = 'noninteractive'
3517-
3518- if fatal:
3519- retry_count = 0
3520- result = None
3521-
3522- # If the command is considered "fatal", we need to retry if the apt
3523- # lock was not acquired.
3524-
3525- while result is None or result == APT_NO_LOCK:
3526- try:
3527- result = subprocess.check_call(cmd, env=env)
3528- except subprocess.CalledProcessError as e:
3529- retry_count = retry_count + 1
3530- if retry_count > APT_NO_LOCK_RETRY_COUNT:
3531- raise
3532- result = e.returncode
3533- log("Couldn't acquire DPKG lock. Will retry in {} seconds."
3534- "".format(APT_NO_LOCK_RETRY_DELAY))
3535- time.sleep(APT_NO_LOCK_RETRY_DELAY)
3536-
3537- else:
3538- subprocess.call(cmd, env=env)
3539
3540=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
3541--- hooks/charmhelpers/fetch/archiveurl.py 2015-04-08 05:57:06 +0000
3542+++ hooks/charmhelpers/fetch/archiveurl.py 2017-07-18 06:08:45 +0000
3543@@ -1,18 +1,16 @@
3544 # Copyright 2014-2015 Canonical Limited.
3545 #
3546-# This file is part of charm-helpers.
3547-#
3548-# charm-helpers is free software: you can redistribute it and/or modify
3549-# it under the terms of the GNU Lesser General Public License version 3 as
3550-# published by the Free Software Foundation.
3551-#
3552-# charm-helpers is distributed in the hope that it will be useful,
3553-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3554-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3555-# GNU Lesser General Public License for more details.
3556-#
3557-# You should have received a copy of the GNU Lesser General Public License
3558-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3559+# Licensed under the Apache License, Version 2.0 (the "License");
3560+# you may not use this file except in compliance with the License.
3561+# You may obtain a copy of the License at
3562+#
3563+# http://www.apache.org/licenses/LICENSE-2.0
3564+#
3565+# Unless required by applicable law or agreed to in writing, software
3566+# distributed under the License is distributed on an "AS IS" BASIS,
3567+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3568+# See the License for the specific language governing permissions and
3569+# limitations under the License.
3570
3571 import os
3572 import hashlib
3573@@ -77,6 +75,8 @@
3574 def can_handle(self, source):
3575 url_parts = self.parse_url(source)
3576 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
3577+ # XXX: Why is this returning a boolean and a string? It's
3578+ # doomed to fail since "bool(can_handle('foo://'))" will be True.
3579 return "Wrong source type"
3580 if get_archive_handler(self.base_url(source)):
3581 return True
3582@@ -106,7 +106,7 @@
3583 install_opener(opener)
3584 response = urlopen(source)
3585 try:
3586- with open(dest, 'w') as dest_file:
3587+ with open(dest, 'wb') as dest_file:
3588 dest_file.write(response.read())
3589 except Exception as e:
3590 if os.path.isfile(dest):
3591@@ -155,7 +155,11 @@
3592 else:
3593 algorithms = hashlib.algorithms_available
3594 if key in algorithms:
3595- check_hash(dld_file, value, key)
3596+ if len(value) != 1:
3597+ raise TypeError(
3598+ "Expected 1 hash value, not %d" % len(value))
3599+ expected = value[0]
3600+ check_hash(dld_file, expected, key)
3601 if checksum:
3602 check_hash(dld_file, checksum, hash_type)
3603 return extract(dld_file, dest)
3604
3605=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
3606--- hooks/charmhelpers/fetch/bzrurl.py 2015-04-08 05:57:06 +0000
3607+++ hooks/charmhelpers/fetch/bzrurl.py 2017-07-18 06:08:45 +0000
3608@@ -1,78 +1,76 @@
3609 # Copyright 2014-2015 Canonical Limited.
3610 #
3611-# This file is part of charm-helpers.
3612-#
3613-# charm-helpers is free software: you can redistribute it and/or modify
3614-# it under the terms of the GNU Lesser General Public License version 3 as
3615-# published by the Free Software Foundation.
3616-#
3617-# charm-helpers is distributed in the hope that it will be useful,
3618-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3619-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3620-# GNU Lesser General Public License for more details.
3621-#
3622-# You should have received a copy of the GNU Lesser General Public License
3623-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3624+# Licensed under the Apache License, Version 2.0 (the "License");
3625+# you may not use this file except in compliance with the License.
3626+# You may obtain a copy of the License at
3627+#
3628+# http://www.apache.org/licenses/LICENSE-2.0
3629+#
3630+# Unless required by applicable law or agreed to in writing, software
3631+# distributed under the License is distributed on an "AS IS" BASIS,
3632+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3633+# See the License for the specific language governing permissions and
3634+# limitations under the License.
3635
3636 import os
3637+from subprocess import check_call
3638 from charmhelpers.fetch import (
3639 BaseFetchHandler,
3640- UnhandledSource
3641+ UnhandledSource,
3642+ filter_installed_packages,
3643+ install,
3644 )
3645 from charmhelpers.core.host import mkdir
3646
3647-import six
3648-if six.PY3:
3649- raise ImportError('bzrlib does not support Python3')
3650
3651-try:
3652- from bzrlib.branch import Branch
3653- from bzrlib import bzrdir, workingtree, errors
3654-except ImportError:
3655- from charmhelpers.fetch import apt_install
3656- apt_install("python-bzrlib")
3657- from bzrlib.branch import Branch
3658- from bzrlib import bzrdir, workingtree, errors
3659+if filter_installed_packages(['bzr']) != []:
3660+ install(['bzr'])
3661+ if filter_installed_packages(['bzr']) != []:
3662+ raise NotImplementedError('Unable to install bzr')
3663
3664
3665 class BzrUrlFetchHandler(BaseFetchHandler):
3666- """Handler for bazaar branches via generic and lp URLs"""
3667+ """Handler for bazaar branches via generic and lp URLs."""
3668+
3669 def can_handle(self, source):
3670 url_parts = self.parse_url(source)
3671- if url_parts.scheme not in ('bzr+ssh', 'lp'):
3672+ if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
3673 return False
3674+ elif not url_parts.scheme:
3675+ return os.path.exists(os.path.join(source, '.bzr'))
3676 else:
3677 return True
3678
3679- def branch(self, source, dest):
3680- url_parts = self.parse_url(source)
3681- # If we use lp:branchname scheme we need to load plugins
3682+ def branch(self, source, dest, revno=None):
3683 if not self.can_handle(source):
3684 raise UnhandledSource("Cannot handle {}".format(source))
3685- if url_parts.scheme == "lp":
3686- from bzrlib.plugin import load_plugins
3687- load_plugins()
3688- try:
3689- local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
3690- except errors.AlreadyControlDirError:
3691- local_branch = Branch.open(dest)
3692- try:
3693- remote_branch = Branch.open(source)
3694- remote_branch.push(local_branch)
3695- tree = workingtree.WorkingTree.open(dest)
3696- tree.update()
3697- except Exception as e:
3698- raise e
3699+ cmd_opts = []
3700+ if revno:
3701+ cmd_opts += ['-r', str(revno)]
3702+ if os.path.exists(dest):
3703+ cmd = ['bzr', 'pull']
3704+ cmd += cmd_opts
3705+ cmd += ['--overwrite', '-d', dest, source]
3706+ else:
3707+ cmd = ['bzr', 'branch']
3708+ cmd += cmd_opts
3709+ cmd += [source, dest]
3710+ check_call(cmd)
3711
3712- def install(self, source):
3713+ def install(self, source, dest=None, revno=None):
3714 url_parts = self.parse_url(source)
3715 branch_name = url_parts.path.strip("/").split("/")[-1]
3716- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3717- branch_name)
3718- if not os.path.exists(dest_dir):
3719- mkdir(dest_dir, perms=0o755)
3720+ if dest:
3721+ dest_dir = os.path.join(dest, branch_name)
3722+ else:
3723+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3724+ branch_name)
3725+
3726+ if dest and not os.path.exists(dest):
3727+ mkdir(dest, perms=0o755)
3728+
3729 try:
3730- self.branch(source, dest_dir)
3731+ self.branch(source, dest_dir, revno)
3732 except OSError as e:
3733 raise UnhandledSource(e.strerror)
3734 return dest_dir
3735
3736=== added file 'hooks/charmhelpers/fetch/centos.py'
3737--- hooks/charmhelpers/fetch/centos.py 1970-01-01 00:00:00 +0000
3738+++ hooks/charmhelpers/fetch/centos.py 2017-07-18 06:08:45 +0000
3739@@ -0,0 +1,171 @@
3740+# Copyright 2014-2015 Canonical Limited.
3741+#
3742+# Licensed under the Apache License, Version 2.0 (the "License");
3743+# you may not use this file except in compliance with the License.
3744+# You may obtain a copy of the License at
3745+#
3746+# http://www.apache.org/licenses/LICENSE-2.0
3747+#
3748+# Unless required by applicable law or agreed to in writing, software
3749+# distributed under the License is distributed on an "AS IS" BASIS,
3750+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3751+# See the License for the specific language governing permissions and
3752+# limitations under the License.
3753+
3754+import subprocess
3755+import os
3756+import time
3757+import six
3758+import yum
3759+
3760+from tempfile import NamedTemporaryFile
3761+from charmhelpers.core.hookenv import log
3762+
3763+YUM_NO_LOCK = 1 # The return code for "couldn't acquire lock" in YUM.
3764+YUM_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
3765+YUM_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
3766+
3767+
3768+def filter_installed_packages(packages):
3769+ """Return a list of packages that require installation."""
3770+ yb = yum.YumBase()
3771+ package_list = yb.doPackageLists()
3772+ temp_cache = {p.base_package_name: 1 for p in package_list['installed']}
3773+
3774+ _pkgs = [p for p in packages if not temp_cache.get(p, False)]
3775+ return _pkgs
3776+
3777+
3778+def install(packages, options=None, fatal=False):
3779+ """Install one or more packages."""
3780+ cmd = ['yum', '--assumeyes']
3781+ if options is not None:
3782+ cmd.extend(options)
3783+ cmd.append('install')
3784+ if isinstance(packages, six.string_types):
3785+ cmd.append(packages)
3786+ else:
3787+ cmd.extend(packages)
3788+ log("Installing {} with options: {}".format(packages,
3789+ options))
3790+ _run_yum_command(cmd, fatal)
3791+
3792+
3793+def upgrade(options=None, fatal=False, dist=False):
3794+ """Upgrade all packages."""
3795+ cmd = ['yum', '--assumeyes']
3796+ if options is not None:
3797+ cmd.extend(options)
3798+ cmd.append('upgrade')
3799+ log("Upgrading with options: {}".format(options))
3800+ _run_yum_command(cmd, fatal)
3801+
3802+
3803+def update(fatal=False):
3804+ """Update local yum cache."""
3805+ cmd = ['yum', '--assumeyes', 'update']
3806+ log("Update with fatal: {}".format(fatal))
3807+ _run_yum_command(cmd, fatal)
3808+
3809+
3810+def purge(packages, fatal=False):
3811+ """Purge one or more packages."""
3812+ cmd = ['yum', '--assumeyes', 'remove']
3813+ if isinstance(packages, six.string_types):
3814+ cmd.append(packages)
3815+ else:
3816+ cmd.extend(packages)
3817+ log("Purging {}".format(packages))
3818+ _run_yum_command(cmd, fatal)
3819+
3820+
3821+def yum_search(packages):
3822+ """Search for a package."""
3823+ output = {}
3824+ cmd = ['yum', 'search']
3825+ if isinstance(packages, six.string_types):
3826+ cmd.append(packages)
3827+ else:
3828+ cmd.extend(packages)
3829+ log("Searching for {}".format(packages))
3830+ result = subprocess.check_output(cmd)
3831+ for package in list(packages):
3832+ output[package] = package in result
3833+ return output
3834+
3835+
3836+def add_source(source, key=None):
3837+ """Add a package source to this system.
3838+
3839+ @param source: a URL with a rpm package
3840+
3841+ @param key: A key to be added to the system's keyring and used
3842+ to verify the signatures on packages. Ideally, this should be an
3843+ ASCII format GPG public key including the block headers. A GPG key
3844+ id may also be used, but be aware that only insecure protocols are
3845+ available to retrieve the actual public key from a public keyserver
3846+ placing your Juju environment at risk.
3847+ """
3848+ if source is None:
3849+ log('Source is not present. Skipping')
3850+ return
3851+
3852+ if source.startswith('http'):
3853+ directory = '/etc/yum.repos.d/'
3854+ for filename in os.listdir(directory):
3855+ with open(directory + filename, 'r') as rpm_file:
3856+ if source in rpm_file.read():
3857+ break
3858+ else:
3859+ log("Add source: {!r}".format(source))
3860+ # write in the charms.repo
3861+ with open(directory + 'Charms.repo', 'a') as rpm_file:
3862+ rpm_file.write('[%s]\n' % source[7:].replace('/', '_'))
3863+ rpm_file.write('name=%s\n' % source[7:])
3864+ rpm_file.write('baseurl=%s\n\n' % source)
3865+ else:
3866+ log("Unknown source: {!r}".format(source))
3867+
3868+ if key:
3869+ if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
3870+ with NamedTemporaryFile('w+') as key_file:
3871+ key_file.write(key)
3872+ key_file.flush()
3873+ key_file.seek(0)
3874+ subprocess.check_call(['rpm', '--import', key_file.name])
3875+ else:
3876+ subprocess.check_call(['rpm', '--import', key])
3877+
3878+
3879+def _run_yum_command(cmd, fatal=False):
3880+ """Run an YUM command.
3881+
3882+ Checks the output and retry if the fatal flag is set to True.
3883+
3884+ :param: cmd: str: The yum command to run.
3885+ :param: fatal: bool: Whether the command's output should be checked and
3886+ retried.
3887+ """
3888+ env = os.environ.copy()
3889+
3890+ if fatal:
3891+ retry_count = 0
3892+ result = None
3893+
3894+ # If the command is considered "fatal", we need to retry if the yum
3895+ # lock was not acquired.
3896+
3897+ while result is None or result == YUM_NO_LOCK:
3898+ try:
3899+ result = subprocess.check_call(cmd, env=env)
3900+ except subprocess.CalledProcessError as e:
3901+ retry_count = retry_count + 1
3902+ if retry_count > YUM_NO_LOCK_RETRY_COUNT:
3903+ raise
3904+ result = e.returncode
3905+ log("Couldn't acquire YUM lock. Will retry in {} seconds."
3906+ "".format(YUM_NO_LOCK_RETRY_DELAY))
3907+ time.sleep(YUM_NO_LOCK_RETRY_DELAY)
3908+
3909+ else:
3910+ subprocess.call(cmd, env=env)
3911
3912=== modified file 'hooks/charmhelpers/fetch/giturl.py'
3913--- hooks/charmhelpers/fetch/giturl.py 2015-04-08 05:57:06 +0000
3914+++ hooks/charmhelpers/fetch/giturl.py 2017-07-18 06:08:45 +0000
3915@@ -1,58 +1,58 @@
3916 # Copyright 2014-2015 Canonical Limited.
3917 #
3918-# This file is part of charm-helpers.
3919-#
3920-# charm-helpers is free software: you can redistribute it and/or modify
3921-# it under the terms of the GNU Lesser General Public License version 3 as
3922-# published by the Free Software Foundation.
3923-#
3924-# charm-helpers is distributed in the hope that it will be useful,
3925-# but WITHOUT ANY WARRANTY; without even the implied warranty of
3926-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3927-# GNU Lesser General Public License for more details.
3928-#
3929-# You should have received a copy of the GNU Lesser General Public License
3930-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3931+# Licensed under the Apache License, Version 2.0 (the "License");
3932+# you may not use this file except in compliance with the License.
3933+# You may obtain a copy of the License at
3934+#
3935+# http://www.apache.org/licenses/LICENSE-2.0
3936+#
3937+# Unless required by applicable law or agreed to in writing, software
3938+# distributed under the License is distributed on an "AS IS" BASIS,
3939+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3940+# See the License for the specific language governing permissions and
3941+# limitations under the License.
3942
3943 import os
3944+from subprocess import check_call, CalledProcessError
3945 from charmhelpers.fetch import (
3946 BaseFetchHandler,
3947- UnhandledSource
3948+ UnhandledSource,
3949+ filter_installed_packages,
3950+ install,
3951 )
3952-from charmhelpers.core.host import mkdir
3953-
3954-import six
3955-if six.PY3:
3956- raise ImportError('GitPython does not support Python 3')
3957-
3958-try:
3959- from git import Repo
3960-except ImportError:
3961- from charmhelpers.fetch import apt_install
3962- apt_install("python-git")
3963- from git import Repo
3964-
3965-from git.exc import GitCommandError # noqa E402
3966+
3967+if filter_installed_packages(['git']) != []:
3968+ install(['git'])
3969+ if filter_installed_packages(['git']) != []:
3970+ raise NotImplementedError('Unable to install git')
3971
3972
3973 class GitUrlFetchHandler(BaseFetchHandler):
3974- """Handler for git branches via generic and github URLs"""
3975+ """Handler for git branches via generic and github URLs."""
3976+
3977 def can_handle(self, source):
3978 url_parts = self.parse_url(source)
3979 # TODO (mattyw) no support for ssh git@ yet
3980- if url_parts.scheme not in ('http', 'https', 'git'):
3981+ if url_parts.scheme not in ('http', 'https', 'git', ''):
3982 return False
3983+ elif not url_parts.scheme:
3984+ return os.path.exists(os.path.join(source, '.git'))
3985 else:
3986 return True
3987
3988- def clone(self, source, dest, branch):
3989+ def clone(self, source, dest, branch="master", depth=None):
3990 if not self.can_handle(source):
3991 raise UnhandledSource("Cannot handle {}".format(source))
3992
3993- repo = Repo.clone_from(source, dest)
3994- repo.git.checkout(branch)
3995+ if os.path.exists(dest):
3996+ cmd = ['git', '-C', dest, 'pull', source, branch]
3997+ else:
3998+ cmd = ['git', 'clone', source, dest, '--branch', branch]
3999+ if depth:
4000+ cmd.extend(['--depth', depth])
4001+ check_call(cmd)
4002
4003- def install(self, source, branch="master", dest=None):
4004+ def install(self, source, branch="master", dest=None, depth=None):
4005 url_parts = self.parse_url(source)
4006 branch_name = url_parts.path.strip("/").split("/")[-1]
4007 if dest:
4008@@ -60,12 +60,10 @@
4009 else:
4010 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
4011 branch_name)
4012- if not os.path.exists(dest_dir):
4013- mkdir(dest_dir, perms=0o755)
4014 try:
4015- self.clone(source, dest_dir, branch)
4016- except GitCommandError as e:
4017- raise UnhandledSource(e.message)
4018+ self.clone(source, dest_dir, branch, depth)
4019+ except CalledProcessError as e:
4020+ raise UnhandledSource(e)
4021 except OSError as e:
4022 raise UnhandledSource(e.strerror)
4023 return dest_dir
4024
4025=== added file 'hooks/charmhelpers/fetch/snap.py'
4026--- hooks/charmhelpers/fetch/snap.py 1970-01-01 00:00:00 +0000
4027+++ hooks/charmhelpers/fetch/snap.py 2017-07-18 06:08:45 +0000
4028@@ -0,0 +1,122 @@
4029+# Copyright 2014-2017 Canonical Limited.
4030+#
4031+# Licensed under the Apache License, Version 2.0 (the "License");
4032+# you may not use this file except in compliance with the License.
4033+# You may obtain a copy of the License at
4034+#
4035+# http://www.apache.org/licenses/LICENSE-2.0
4036+#
4037+# Unless required by applicable law or agreed to in writing, software
4038+# distributed under the License is distributed on an "AS IS" BASIS,
4039+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4040+# See the License for the specific language governing permissions and
4041+# limitations under the License.
4042+"""
4043+Charm helpers snap for classic charms.
4044+
4045+If writing reactive charms, use the snap layer:
4046+https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html
4047+"""
4048+import subprocess
4049+from os import environ
4050+from time import sleep
4051+from charmhelpers.core.hookenv import log
4052+
4053+__author__ = 'Joseph Borg <joseph.borg@canonical.com>'
4054+
4055+SNAP_NO_LOCK = 1 # The return code for "couldn't acquire lock" in Snap (hopefully this will be improved).
4056+SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks.
4057+SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
4058+
4059+
4060+class CouldNotAcquireLockException(Exception):
4061+ pass
4062+
4063+
4064+def _snap_exec(commands):
4065+ """
4066+ Execute snap commands.
4067+
4068+ :param commands: List commands
4069+ :return: Integer exit code
4070+ """
4071+ assert type(commands) == list
4072+
4073+ retry_count = 0
4074+ return_code = None
4075+
4076+ while return_code is None or return_code == SNAP_NO_LOCK:
4077+ try:
4078+ return_code = subprocess.check_call(['snap'] + commands, env=environ)
4079+ except subprocess.CalledProcessError as e:
4080+ retry_count += + 1
4081+ if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
4082+ raise CouldNotAcquireLockException('Could not aquire lock after %s attempts' % SNAP_NO_LOCK_RETRY_COUNT)
4083+ return_code = e.returncode
4084+ log('Snap failed to acquire lock, trying again in %s seconds.' % SNAP_NO_LOCK_RETRY_DELAY, level='WARN')
4085+ sleep(SNAP_NO_LOCK_RETRY_DELAY)
4086+
4087+ return return_code
4088+
4089+
4090+def snap_install(packages, *flags):
4091+ """
4092+ Install a snap package.
4093+
4094+ :param packages: String or List String package name
4095+ :param flags: List String flags to pass to install command
4096+ :return: Integer return code from snap
4097+ """
4098+ if type(packages) is not list:
4099+ packages = [packages]
4100+
4101+ flags = list(flags)
4102+
4103+ message = 'Installing snap(s) "%s"' % ', '.join(packages)
4104+ if flags:
4105+ message += ' with option(s) "%s"' % ', '.join(flags)
4106+
4107+ log(message, level='INFO')
4108+ return _snap_exec(['install'] + flags + packages)
4109+
4110+
4111+def snap_remove(packages, *flags):
4112+ """
4113+ Remove a snap package.
4114+
4115+ :param packages: String or List String package name
4116+ :param flags: List String flags to pass to remove command
4117+ :return: Integer return code from snap
4118+ """
4119+ if type(packages) is not list:
4120+ packages = [packages]
4121+
4122+ flags = list(flags)
4123+
4124+ message = 'Removing snap(s) "%s"' % ', '.join(packages)
4125+ if flags:
4126+ message += ' with options "%s"' % ', '.join(flags)
4127+
4128+ log(message, level='INFO')
4129+ return _snap_exec(['remove'] + flags + packages)
4130+
4131+
4132+def snap_refresh(packages, *flags):
4133+ """
4134+ Refresh / Update snap package.
4135+
4136+ :param packages: String or List String package name
4137+ :param flags: List String flags to pass to refresh command
4138+ :return: Integer return code from snap
4139+ """
4140+ if type(packages) is not list:
4141+ packages = [packages]
4142+
4143+ flags = list(flags)
4144+
4145+ message = 'Refreshing snap(s) "%s"' % ', '.join(packages)
4146+ if flags:
4147+ message += ' with options "%s"' % ', '.join(flags)
4148+
4149+ log(message, level='INFO')
4150+ return _snap_exec(['refresh'] + flags + packages)
4151
4152=== added file 'hooks/charmhelpers/fetch/ubuntu.py'
4153--- hooks/charmhelpers/fetch/ubuntu.py 1970-01-01 00:00:00 +0000
4154+++ hooks/charmhelpers/fetch/ubuntu.py 2017-07-18 06:08:45 +0000
4155@@ -0,0 +1,568 @@
4156+# Copyright 2014-2015 Canonical Limited.
4157+#
4158+# Licensed under the Apache License, Version 2.0 (the "License");
4159+# you may not use this file except in compliance with the License.
4160+# You may obtain a copy of the License at
4161+#
4162+# http://www.apache.org/licenses/LICENSE-2.0
4163+#
4164+# Unless required by applicable law or agreed to in writing, software
4165+# distributed under the License is distributed on an "AS IS" BASIS,
4166+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4167+# See the License for the specific language governing permissions and
4168+# limitations under the License.
4169+
4170+from collections import OrderedDict
4171+import os
4172+import platform
4173+import re
4174+import six
4175+import time
4176+import subprocess
4177+from tempfile import NamedTemporaryFile
4178+
4179+from charmhelpers.core.host import (
4180+ lsb_release
4181+)
4182+from charmhelpers.core.hookenv import (
4183+ log,
4184+ DEBUG,
4185+)
4186+from charmhelpers.fetch import SourceConfigError, GPGKeyError
4187+
4188+PROPOSED_POCKET = (
4189+ "# Proposed\n"
4190+ "deb http://archive.ubuntu.com/ubuntu {}-proposed main universe "
4191+ "multiverse restricted\n")
4192+PROPOSED_PORTS_POCKET = (
4193+ "# Proposed\n"
4194+ "deb http://ports.ubuntu.com/ubuntu-ports {}-proposed main universe "
4195+ "multiverse restricted\n")
4196+# Only supports 64bit and ppc64 at the moment.
4197+ARCH_TO_PROPOSED_POCKET = {
4198+ 'x86_64': PROPOSED_POCKET,
4199+ 'ppc64le': PROPOSED_PORTS_POCKET,
4200+ 'aarch64': PROPOSED_PORTS_POCKET,
4201+}
4202+CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
4203+CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
4204+CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
4205+deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
4206+"""
4207+CLOUD_ARCHIVE_POCKETS = {
4208+ # Folsom
4209+ 'folsom': 'precise-updates/folsom',
4210+ 'folsom/updates': 'precise-updates/folsom',
4211+ 'precise-folsom': 'precise-updates/folsom',
4212+ 'precise-folsom/updates': 'precise-updates/folsom',
4213+ 'precise-updates/folsom': 'precise-updates/folsom',
4214+ 'folsom/proposed': 'precise-proposed/folsom',
4215+ 'precise-folsom/proposed': 'precise-proposed/folsom',
4216+ 'precise-proposed/folsom': 'precise-proposed/folsom',
4217+ # Grizzly
4218+ 'grizzly': 'precise-updates/grizzly',
4219+ 'grizzly/updates': 'precise-updates/grizzly',
4220+ 'precise-grizzly': 'precise-updates/grizzly',
4221+ 'precise-grizzly/updates': 'precise-updates/grizzly',
4222+ 'precise-updates/grizzly': 'precise-updates/grizzly',
4223+ 'grizzly/proposed': 'precise-proposed/grizzly',
4224+ 'precise-grizzly/proposed': 'precise-proposed/grizzly',
4225+ 'precise-proposed/grizzly': 'precise-proposed/grizzly',
4226+ # Havana
4227+ 'havana': 'precise-updates/havana',
4228+ 'havana/updates': 'precise-updates/havana',
4229+ 'precise-havana': 'precise-updates/havana',
4230+ 'precise-havana/updates': 'precise-updates/havana',
4231+ 'precise-updates/havana': 'precise-updates/havana',
4232+ 'havana/proposed': 'precise-proposed/havana',
4233+ 'precise-havana/proposed': 'precise-proposed/havana',
4234+ 'precise-proposed/havana': 'precise-proposed/havana',
4235+ # Icehouse
4236+ 'icehouse': 'precise-updates/icehouse',
4237+ 'icehouse/updates': 'precise-updates/icehouse',
4238+ 'precise-icehouse': 'precise-updates/icehouse',
4239+ 'precise-icehouse/updates': 'precise-updates/icehouse',
4240+ 'precise-updates/icehouse': 'precise-updates/icehouse',
4241+ 'icehouse/proposed': 'precise-proposed/icehouse',
4242+ 'precise-icehouse/proposed': 'precise-proposed/icehouse',
4243+ 'precise-proposed/icehouse': 'precise-proposed/icehouse',
4244+ # Juno
4245+ 'juno': 'trusty-updates/juno',
4246+ 'juno/updates': 'trusty-updates/juno',
4247+ 'trusty-juno': 'trusty-updates/juno',
4248+ 'trusty-juno/updates': 'trusty-updates/juno',
4249+ 'trusty-updates/juno': 'trusty-updates/juno',
4250+ 'juno/proposed': 'trusty-proposed/juno',
4251+ 'trusty-juno/proposed': 'trusty-proposed/juno',
4252+ 'trusty-proposed/juno': 'trusty-proposed/juno',
4253+ # Kilo
4254+ 'kilo': 'trusty-updates/kilo',
4255+ 'kilo/updates': 'trusty-updates/kilo',
4256+ 'trusty-kilo': 'trusty-updates/kilo',
4257+ 'trusty-kilo/updates': 'trusty-updates/kilo',
4258+ 'trusty-updates/kilo': 'trusty-updates/kilo',
4259+ 'kilo/proposed': 'trusty-proposed/kilo',
4260+ 'trusty-kilo/proposed': 'trusty-proposed/kilo',
4261+ 'trusty-proposed/kilo': 'trusty-proposed/kilo',
4262+ # Liberty
4263+ 'liberty': 'trusty-updates/liberty',
4264+ 'liberty/updates': 'trusty-updates/liberty',
4265+ 'trusty-liberty': 'trusty-updates/liberty',
4266+ 'trusty-liberty/updates': 'trusty-updates/liberty',
4267+ 'trusty-updates/liberty': 'trusty-updates/liberty',
4268+ 'liberty/proposed': 'trusty-proposed/liberty',
4269+ 'trusty-liberty/proposed': 'trusty-proposed/liberty',
4270+ 'trusty-proposed/liberty': 'trusty-proposed/liberty',
4271+ # Mitaka
4272+ 'mitaka': 'trusty-updates/mitaka',
4273+ 'mitaka/updates': 'trusty-updates/mitaka',
4274+ 'trusty-mitaka': 'trusty-updates/mitaka',
4275+ 'trusty-mitaka/updates': 'trusty-updates/mitaka',
4276+ 'trusty-updates/mitaka': 'trusty-updates/mitaka',
4277+ 'mitaka/proposed': 'trusty-proposed/mitaka',
4278+ 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
4279+ 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
4280+ # Newton
4281+ 'newton': 'xenial-updates/newton',
4282+ 'newton/updates': 'xenial-updates/newton',
4283+ 'xenial-newton': 'xenial-updates/newton',
4284+ 'xenial-newton/updates': 'xenial-updates/newton',
4285+ 'xenial-updates/newton': 'xenial-updates/newton',
4286+ 'newton/proposed': 'xenial-proposed/newton',
4287+ 'xenial-newton/proposed': 'xenial-proposed/newton',
4288+ 'xenial-proposed/newton': 'xenial-proposed/newton',
4289+ # Ocata
4290+ 'ocata': 'xenial-updates/ocata',
4291+ 'ocata/updates': 'xenial-updates/ocata',
4292+ 'xenial-ocata': 'xenial-updates/ocata',
4293+ 'xenial-ocata/updates': 'xenial-updates/ocata',
4294+ 'xenial-updates/ocata': 'xenial-updates/ocata',
4295+ 'ocata/proposed': 'xenial-proposed/ocata',
4296+ 'xenial-ocata/proposed': 'xenial-proposed/ocata',
4297+ 'xenial-ocata/newton': 'xenial-proposed/ocata',
4298+ # Pike
4299+ 'pike': 'xenial-updates/pike',
4300+ 'xenial-pike': 'xenial-updates/pike',
4301+ 'xenial-pike/updates': 'xenial-updates/pike',
4302+ 'xenial-updates/pike': 'xenial-updates/pike',
4303+ 'pike/proposed': 'xenial-proposed/pike',
4304+ 'xenial-pike/proposed': 'xenial-proposed/pike',
4305+ 'xenial-pike/newton': 'xenial-proposed/pike',
4306+ # Queens
4307+ 'queens': 'xenial-updates/queens',
4308+ 'xenial-queens': 'xenial-updates/queens',
4309+ 'xenial-queens/updates': 'xenial-updates/queens',
4310+ 'xenial-updates/queens': 'xenial-updates/queens',
4311+ 'queens/proposed': 'xenial-proposed/queens',
4312+ 'xenial-queens/proposed': 'xenial-proposed/queens',
4313+ 'xenial-queens/newton': 'xenial-proposed/queens',
4314+}
4315+
4316+
4317+APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
4318+CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
4319+CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times.
4320+
4321+
4322+def filter_installed_packages(packages):
4323+ """Return a list of packages that require installation."""
4324+ cache = apt_cache()
4325+ _pkgs = []
4326+ for package in packages:
4327+ try:
4328+ p = cache[package]
4329+ p.current_ver or _pkgs.append(package)
4330+ except KeyError:
4331+ log('Package {} has no installation candidate.'.format(package),
4332+ level='WARNING')
4333+ _pkgs.append(package)
4334+ return _pkgs
4335+
4336+
4337+def apt_cache(in_memory=True, progress=None):
4338+ """Build and return an apt cache."""
4339+ from apt import apt_pkg
4340+ apt_pkg.init()
4341+ if in_memory:
4342+ apt_pkg.config.set("Dir::Cache::pkgcache", "")
4343+ apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
4344+ return apt_pkg.Cache(progress)
4345+
4346+
4347+def apt_install(packages, options=None, fatal=False):
4348+ """Install one or more packages."""
4349+ if options is None:
4350+ options = ['--option=Dpkg::Options::=--force-confold']
4351+
4352+ cmd = ['apt-get', '--assume-yes']
4353+ cmd.extend(options)
4354+ cmd.append('install')
4355+ if isinstance(packages, six.string_types):
4356+ cmd.append(packages)
4357+ else:
4358+ cmd.extend(packages)
4359+ log("Installing {} with options: {}".format(packages,
4360+ options))
4361+ _run_apt_command(cmd, fatal)
4362+
4363+
4364+def apt_upgrade(options=None, fatal=False, dist=False):
4365+ """Upgrade all packages."""
4366+ if options is None:
4367+ options = ['--option=Dpkg::Options::=--force-confold']
4368+
4369+ cmd = ['apt-get', '--assume-yes']
4370+ cmd.extend(options)
4371+ if dist:
4372+ cmd.append('dist-upgrade')
4373+ else:
4374+ cmd.append('upgrade')
4375+ log("Upgrading with options: {}".format(options))
4376+ _run_apt_command(cmd, fatal)
4377+
4378+
4379+def apt_update(fatal=False):
4380+ """Update local apt cache."""
4381+ cmd = ['apt-get', 'update']
4382+ _run_apt_command(cmd, fatal)
4383+
4384+
4385+def apt_purge(packages, fatal=False):
4386+ """Purge one or more packages."""
4387+ cmd = ['apt-get', '--assume-yes', 'purge']
4388+ if isinstance(packages, six.string_types):
4389+ cmd.append(packages)
4390+ else:
4391+ cmd.extend(packages)
4392+ log("Purging {}".format(packages))
4393+ _run_apt_command(cmd, fatal)
4394+
4395+
4396+def apt_mark(packages, mark, fatal=False):
4397+ """Flag one or more packages using apt-mark."""
4398+ log("Marking {} as {}".format(packages, mark))
4399+ cmd = ['apt-mark', mark]
4400+ if isinstance(packages, six.string_types):
4401+ cmd.append(packages)
4402+ else:
4403+ cmd.extend(packages)
4404+
4405+ if fatal:
4406+ subprocess.check_call(cmd, universal_newlines=True)
4407+ else:
4408+ subprocess.call(cmd, universal_newlines=True)
4409+
4410+
4411+def apt_hold(packages, fatal=False):
4412+ return apt_mark(packages, 'hold', fatal=fatal)
4413+
4414+
4415+def apt_unhold(packages, fatal=False):
4416+ return apt_mark(packages, 'unhold', fatal=fatal)
4417+
4418+
4419+def import_key(keyid):
4420+ """Import a key in either ASCII Armor or Radix64 format.
4421+
4422+ `keyid` is either the keyid to fetch from a PGP server, or
4423+ the key in ASCII armor foramt.
4424+
4425+ :param keyid: String of key (or key id).
4426+ :raises: GPGKeyError if the key could not be imported
4427+ """
4428+ key = keyid.strip()
4429+ if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
4430+ key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
4431+ log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
4432+ log("Importing ASCII Armor PGP key", level=DEBUG)
4433+ with NamedTemporaryFile() as keyfile:
4434+ with open(keyfile.name, 'w') as fd:
4435+ fd.write(key)
4436+ fd.write("\n")
4437+ cmd = ['apt-key', 'add', keyfile.name]
4438+ try:
4439+ subprocess.check_call(cmd)
4440+ except subprocess.CalledProcessError:
4441+ error = "Error importing PGP key '{}'".format(key)
4442+ log(error)
4443+ raise GPGKeyError(error)
4444+ else:
4445+ log("PGP key found (looks like Radix64 format)", level=DEBUG)
4446+ log("Importing PGP key from keyserver", level=DEBUG)
4447+ cmd = ['apt-key', 'adv', '--keyserver',
4448+ 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
4449+ try:
4450+ subprocess.check_call(cmd)
4451+ except subprocess.CalledProcessError:
4452+ error = "Error importing PGP key '{}'".format(key)
4453+ log(error)
4454+ raise GPGKeyError(error)
4455+
4456+
4457+def add_source(source, key=None, fail_invalid=False):
4458+ """Add a package source to this system.
4459+
4460+ @param source: a URL or sources.list entry, as supported by
4461+ add-apt-repository(1). Examples::
4462+
4463+ ppa:charmers/example
4464+ deb https://stub:key@private.example.com/ubuntu trusty main
4465+
4466+ In addition:
4467+ 'proposed:' may be used to enable the standard 'proposed'
4468+ pocket for the release.
4469+ 'cloud:' may be used to activate official cloud archive pockets,
4470+ such as 'cloud:icehouse'
4471+ 'distro' may be used as a noop
4472+
4473+ Full list of source specifications supported by the function are:
4474+
4475+ 'distro': A NOP; i.e. it has no effect.
4476+ 'proposed': the proposed deb spec [2] is wrtten to
4477+ /etc/apt/sources.list/proposed
4478+ 'distro-proposed': adds <version>-proposed to the debs [2]
4479+ 'ppa:<ppa-name>': add-apt-repository --yes <ppa_name>
4480+ 'deb <deb-spec>': add-apt-repository --yes deb <deb-spec>
4481+ 'http://....': add-apt-repository --yes http://...
4482+ 'cloud-archive:<spec>': add-apt-repository -yes cloud-archive:<spec>
4483+ 'cloud:<release>[-staging]': specify a Cloud Archive pocket <release> with
4484+ optional staging version. If staging is used then the staging PPA [2]
4485+ with be used. If staging is NOT used then the cloud archive [3] will be
4486+ added, and the 'ubuntu-cloud-keyring' package will be added for the
4487+ current distro.
4488+
4489+ Otherwise the source is not recognised and this is logged to the juju log.
4490+ However, no error is raised, unless sys_error_on_exit is True.
4491+
4492+ [1] deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
4493+ where {} is replaced with the derived pocket name.
4494+ [2] deb http://archive.ubuntu.com/ubuntu {}-proposed \
4495+ main universe multiverse restricted
4496+ where {} is replaced with the lsb_release codename (e.g. xenial)
4497+ [3] deb http://ubuntu-cloud.archive.canonical.com/ubuntu <pocket>
4498+ to /etc/apt/sources.list.d/cloud-archive-list
4499+
4500+ @param key: A key to be added to the system's APT keyring and used
4501+ to verify the signatures on packages. Ideally, this should be an
4502+ ASCII format GPG public key including the block headers. A GPG key
4503+ id may also be used, but be aware that only insecure protocols are
4504+ available to retrieve the actual public key from a public keyserver
4505+ placing your Juju environment at risk. ppa and cloud archive keys
4506+ are securely added automtically, so sould not be provided.
4507+
4508+ @param fail_invalid: (boolean) if True, then the function raises a
4509+ SourceConfigError is there is no matching installation source.
4510+
4511+ @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a
4512+ valid pocket in CLOUD_ARCHIVE_POCKETS
4513+ """
4514+ _mapping = OrderedDict([
4515+ (r"^distro$", lambda: None), # This is a NOP
4516+ (r"^(?:proposed|distro-proposed)$", _add_proposed),
4517+ (r"^cloud-archive:(.*)$", _add_apt_repository),
4518+ (r"^((?:deb |http:|https:|ppa:).*)$", _add_apt_repository),
4519+ (r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging),
4520+ (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
4521+ (r"^cloud:(.*)$", _add_cloud_pocket),
4522+ ])
4523+ if source is None:
4524+ source = ''
4525+ for r, fn in six.iteritems(_mapping):
4526+ m = re.match(r, source)
4527+ if m:
4528+ # call the assoicated function with the captured groups
4529+ # raises SourceConfigError on error.
4530+ fn(*m.groups())
4531+ if key:
4532+ try:
4533+ import_key(key)
4534+ except GPGKeyError as e:
4535+ raise SourceConfigError(str(e))
4536+ break
4537+ else:
4538+ # nothing matched. log an error and maybe sys.exit
4539+ err = "Unknown source: {!r}".format(source)
4540+ log(err)
4541+ if fail_invalid:
4542+ raise SourceConfigError(err)
4543+
4544+
4545+def _add_proposed():
4546+ """Add the PROPOSED_POCKET as /etc/apt/source.list.d/proposed.list
4547+
4548+ Uses lsb_release()['DISTRIB_CODENAME'] to determine the correct staza for
4549+ the deb line.
4550+
4551+ For intel architecutres PROPOSED_POCKET is used for the release, but for
4552+ other architectures PROPOSED_PORTS_POCKET is used for the release.
4553+ """
4554+ release = lsb_release()['DISTRIB_CODENAME']
4555+ arch = platform.machine()
4556+ if arch not in six.iterkeys(ARCH_TO_PROPOSED_POCKET):
4557+ raise SourceConfigError("Arch {} not supported for (distro-)proposed"
4558+ .format(arch))
4559+ with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
4560+ apt.write(ARCH_TO_PROPOSED_POCKET[arch].format(release))
4561+
4562+
4563+def _add_apt_repository(spec):
4564+ """Add the spec using add_apt_repository
4565+
4566+ :param spec: the parameter to pass to add_apt_repository
4567+ """
4568+ _run_with_retries(['add-apt-repository', '--yes', spec])
4569+
4570+
4571+def _add_cloud_pocket(pocket):
4572+ """Add a cloud pocket as /etc/apt/sources.d/cloud-archive.list
4573+
4574+ Note that this overwrites the existing file if there is one.
4575+
4576+ This function also converts the simple pocket in to the actual pocket using
4577+ the CLOUD_ARCHIVE_POCKETS mapping.
4578+
4579+ :param pocket: string representing the pocket to add a deb spec for.
4580+ :raises: SourceConfigError if the cloud pocket doesn't exist or the
4581+ requested release doesn't match the current distro version.
4582+ """
4583+ apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
4584+ fatal=True)
4585+ if pocket not in CLOUD_ARCHIVE_POCKETS:
4586+ raise SourceConfigError(
4587+ 'Unsupported cloud: source option %s' %
4588+ pocket)
4589+ actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
4590+ with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
4591+ apt.write(CLOUD_ARCHIVE.format(actual_pocket))
4592+
4593+
4594+def _add_cloud_staging(cloud_archive_release, openstack_release):
4595+ """Add the cloud staging repository which is in
4596+ ppa:ubuntu-cloud-archive/<openstack_release>-staging
4597+
4598+ This function checks that the cloud_archive_release matches the current
4599+ codename for the distro that charm is being installed on.
4600+
4601+ :param cloud_archive_release: string, codename for the release.
4602+ :param openstack_release: String, codename for the openstack release.
4603+ :raises: SourceConfigError if the cloud_archive_release doesn't match the
4604+ current version of the os.
4605+ """
4606+ _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
4607+ ppa = 'ppa:ubuntu-cloud-archive/{}-staging'.format(openstack_release)
4608+ cmd = 'add-apt-repository -y {}'.format(ppa)
4609+ _run_with_retries(cmd.split(' '))
4610+
4611+
4612+def _add_cloud_distro_check(cloud_archive_release, openstack_release):
4613+ """Add the cloud pocket, but also check the cloud_archive_release against
4614+ the current distro, and use the openstack_release as the full lookup.
4615+
4616+ This just calls _add_cloud_pocket() with the openstack_release as pocket
4617+ to get the correct cloud-archive.list for dpkg to work with.
4618+
4619+ :param cloud_archive_release:String, codename for the distro release.
4620+ :param openstack_release: String, spec for the release to look up in the
4621+ CLOUD_ARCHIVE_POCKETS
4622+ :raises: SourceConfigError if this is the wrong distro, or the pocket spec
4623+ doesn't exist.
4624+ """
4625+ _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
4626+ _add_cloud_pocket("{}-{}".format(cloud_archive_release, openstack_release))
4627+
4628+
4629+def _verify_is_ubuntu_rel(release, os_release):
4630+ """Verify that the release is in the same as the current ubuntu release.
4631+
4632+ :param release: String, lowercase for the release.
4633+ :param os_release: String, the os_release being asked for
4634+ :raises: SourceConfigError if the release is not the same as the ubuntu
4635+ release.
4636+ """
4637+ ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
4638+ if release != ubuntu_rel:
4639+ raise SourceConfigError(
4640+ 'Invalid Cloud Archive release specified: {}-{} on this Ubuntu'
4641+ 'version ({})'.format(release, os_release, ubuntu_rel))
4642+
4643+
4644+def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
4645+ retry_message="", cmd_env=None):
4646+ """Run a command and retry until success or max_retries is reached.
4647+
4648+ :param: cmd: str: The apt command to run.
4649+ :param: max_retries: int: The number of retries to attempt on a fatal
4650+ command. Defaults to CMD_RETRY_COUNT.
4651+ :param: retry_exitcodes: tuple: Optional additional exit codes to retry.
4652+ Defaults to retry on exit code 1.
4653+ :param: retry_message: str: Optional log prefix emitted during retries.
4654+ :param: cmd_env: dict: Environment variables to add to the command run.
4655+ """
4656+
4657+ env = None
4658+ kwargs = {}
4659+ if cmd_env:
4660+ env = os.environ.copy()
4661+ env.update(cmd_env)
4662+ kwargs['env'] = env
4663+
4664+ if not retry_message:
4665+ retry_message = "Failed executing '{}'".format(" ".join(cmd))
4666+ retry_message += ". Will retry in {} seconds".format(CMD_RETRY_DELAY)
4667+
4668+ retry_count = 0
4669+ result = None
4670+
4671+ retry_results = (None,) + retry_exitcodes
4672+ while result in retry_results:
4673+ try:
4674+ # result = subprocess.check_call(cmd, env=env)
4675+ result = subprocess.check_call(cmd, **kwargs)
4676+ except subprocess.CalledProcessError as e:
4677+ retry_count = retry_count + 1
4678+ if retry_count > max_retries:
4679+ raise
4680+ result = e.returncode
4681+ log(retry_message)
4682+ time.sleep(CMD_RETRY_DELAY)
4683+
4684+
4685+def _run_apt_command(cmd, fatal=False):
4686+ """Run an apt command with optional retries.
4687+
4688+ :param: cmd: str: The apt command to run.
4689+ :param: fatal: bool: Whether the command's output should be checked and
4690+ retried.
4691+ """
4692+ # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment.
4693+ cmd_env = {
4694+ 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')}
4695+
4696+ if fatal:
4697+ _run_with_retries(
4698+ cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,),
4699+ retry_message="Couldn't acquire DPKG lock")
4700+ else:
4701+ env = os.environ.copy()
4702+ env.update(cmd_env)
4703+ subprocess.call(cmd, env=env)
4704+
4705+
4706+def get_upstream_version(package):
4707+ """Determine upstream version based on installed package
4708+
4709+ @returns None (if not installed) or the upstream version
4710+ """
4711+ import apt_pkg
4712+ cache = apt_cache()
4713+ try:
4714+ pkg = cache[package]
4715+ except:
4716+ # the package is unknown to the current apt cache.
4717+ return None
4718+
4719+ if not pkg.current_ver:
4720+ # package is known, but no version is currently installed.
4721+ return None
4722+
4723+ return apt_pkg.upstream_version(pkg.current_ver.ver_str)
4724
4725=== added file 'hooks/charmhelpers/osplatform.py'
4726--- hooks/charmhelpers/osplatform.py 1970-01-01 00:00:00 +0000
4727+++ hooks/charmhelpers/osplatform.py 2017-07-18 06:08:45 +0000
4728@@ -0,0 +1,25 @@
4729+import platform
4730+
4731+
4732+def get_platform():
4733+ """Return the current OS platform.
4734+
4735+ For example: if current os platform is Ubuntu then a string "ubuntu"
4736+ will be returned (which is the name of the module).
4737+ This string is used to decide which platform module should be imported.
4738+ """
4739+ # linux_distribution is deprecated and will be removed in Python 3.7
4740+ # Warings *not* disabled, as we certainly need to fix this.
4741+ tuple_platform = platform.linux_distribution()
4742+ current_platform = tuple_platform[0]
4743+ if "Ubuntu" in current_platform:
4744+ return "ubuntu"
4745+ elif "CentOS" in current_platform:
4746+ return "centos"
4747+ elif "debian" in current_platform:
4748+ # Stock Python does not detect Ubuntu and instead returns debian.
4749+ # Or at least it does in some build environments like Travis CI
4750+ return "ubuntu"
4751+ else:
4752+ raise RuntimeError("This module is not supported on {}."
4753+ .format(current_platform))
4754
4755=== modified file 'hooks/install'
4756--- hooks/install 2015-04-22 05:59:37 +0000
4757+++ hooks/install 2017-07-18 06:08:45 +0000
4758@@ -11,16 +11,11 @@
4759
4760 def install():
4761 hookenv.log('Installing thruk-agent')
4762- # add steps for installing dependencies and packages here
4763- # e.g.: from charmhelpers import fetch
4764- # fetch.apt_install(fetch.filter_installed_packages(['nginx']))
4765 config = hookenv.config()
4766- ppa = config.get('source')
4767- if ppa is not None:
4768- add_source(ppa)
4769- apt_update()
4770-
4771- apt_install(["thruk", "pwgen", "apache2-utils"])
4772+ add_source(config.get('source'), config.get('key', None))
4773+ apt_update(fatal=True)
4774+ package_list = ["thruk", "pwgen", "apache2-utils"]
4775+ apt_install(packages=package_list, fatal=True)
4776
4777 if __name__ == "__main__":
4778 install()
4779
4780=== modified file 'hooks/services.py'
4781--- hooks/services.py 2015-05-26 00:30:33 +0000
4782+++ hooks/services.py 2017-07-18 06:08:45 +0000
4783@@ -22,6 +22,7 @@
4784 source='thruk_local.conf',
4785 target='/etc/thruk/thruk_local.conf'),
4786 actions.log_start,
4787+ actions.update_ppa,
4788 actions.fix_livestatus_perms,
4789 actions.thruk_set_password,
4790 actions.notify_thrukmaster_relation,

Subscribers

People subscribed via source and target branches