Merge lp:~chad.smith/landscape-client-charm/add-apt-repository-retries into lp:landscape-client-charm

Proposed by Chad Smith
Status: Merged
Approved by: Chad Smith
Approved revision: 66
Merged at revision: 63
Proposed branch: lp:~chad.smith/landscape-client-charm/add-apt-repository-retries
Merge into: lp:landscape-client-charm
Diff against target: 4122 lines (+2461/-798)
30 files modified
charm-helpers-sync.yaml (+1/-0)
hooks/charmhelpers/__init__.py (+11/-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 (+392/-63)
hooks/charmhelpers/core/host.py (+622/-154)
hooks/charmhelpers/core/host_factory/centos.py (+56/-0)
hooks/charmhelpers/core/host_factory/ubuntu.py (+56/-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 (+23/-22)
hooks/charmhelpers/core/services/helpers.py (+42/-19)
hooks/charmhelpers/core/strutils.py (+41/-13)
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 (+46/-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 (+364/-0)
hooks/charmhelpers/osplatform.py (+25/-0)
hooks/install.py (+4/-2)
To merge this branch: bzr merge lp:~chad.smith/landscape-client-charm/add-apt-repository-retries
Reviewer Review Type Date Requested Status
🤖 Landscape Builder test results Approve
Eric Snow (community) Approve
Review via email: mp+318960@code.launchpad.net

Commit message

Sync charmhelpers for add_source retries to avoid hook errors on network timeouts. Because of charmhelpers sync fetch._run_apt_command moved to fetch.ubuntu._run_apt_command and fetch has new dependency on osplatform

Description of the change

We just added retries to the fetch.add_source function from in Charm helpers per https://code.launchpad.net/~chad.smith/charm-helpers/retry-add-apt-repository/+merge/318951

This branch only syncs latest charmhelpers per "make sync"

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :
review: Approve (test results)
Revision history for this message
Eric Snow (ericsnowcurrently) :
review: Approve
65. By Chad Smith

add charmhelpers.osplatform dependency and resync

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :
review: Approve (test results)
66. By Chad Smith

import has moved in latest charmhelpers out to fetch.ubuntu

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :
review: Approve (test results)

Preview Diff

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

Subscribers

People subscribed via source and target branches

to all changes: