Merge lp:~sajoupa/charm-haproxy/update-charmhelpers into lp:~haproxy-team/charm-haproxy/trunk-old-use-git

Proposed by Laurent Sesquès
Status: Merged
Merged at revision: 110
Proposed branch: lp:~sajoupa/charm-haproxy/update-charmhelpers
Merge into: lp:~haproxy-team/charm-haproxy/trunk-old-use-git
Diff against target: 4542 lines (+2685/-887)
33 files modified
charm-helpers.yaml (+2/-1)
hooks/charmhelpers/__init__.py (+11/-13)
hooks/charmhelpers/contrib/__init__.py (+11/-13)
hooks/charmhelpers/contrib/charmsupport/__init__.py (+11/-13)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+106/-34)
hooks/charmhelpers/contrib/charmsupport/volumes.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 (+448/-63)
hooks/charmhelpers/core/host.py (+625/-155)
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 (+52/-30)
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/-296)
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)
To merge this branch: bzr merge lp:~sajoupa/charm-haproxy/update-charmhelpers
Reviewer Review Type Date Requested Status
Alberto Donato (community) Approve
Review via email: mp+318785@code.launchpad.net

Description of the change

Updating charmhelpers, which should now set the nrpe relation "primary" key to True.

To post a comment you must log in.
109. By Alberto Donato

Merge lp:~ack/charm-haproxy/disable-non-pci-compliant-ciphers.

110. By Laurent Sesquès

update charmhelpers

111. By Laurent Sesquès

add osplatform to charm-helpers fetched modules

Revision history for this message
Laurent Sesquès (sajoupa) wrote :

Tested successfully that the charm builds, deploys, and sets its nrpe-external-master primary=True:

$ bzr branch lp:~sajoupa/charm-haproxy/update-charmhelpers charm-haproxy
$ cd charm-haproxy
$ charm build
$ cd builds
$ juju deploy ./haproxy
$ juju deploy cs:nrpe
$ juju add-relation haproxy:nrpe-external-master nrpe:nrpe-external-master
$ juju run --unit haproxy/0 "relation-ids nrpe-external-master"
nrpe-external-master:2
$ juju run --unit nrpe/0 "relation-get -r nrpe-external-master:2 primary haproxy/0"
True

Revision history for this message
Alberto Donato (ack) wrote :

+1, thanks for testing

review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches

to all changes: