Merge ~james-page/ntp-charm:add-update-status into ntp-charm:master

Proposed by James Page
Status: Merged
Merge reported by: Paul Gear
Merged at revision: not available
Proposed branch: ~james-page/ntp-charm:add-update-status
Merge into: ntp-charm:master
Diff against target: 3114 lines (+1404/-785)
39 files modified
.gitignore (+1/-0)
charm-helpers-sync.yaml (+1/-0)
hooks/charmhelpers/__init__.py (+9/-11)
hooks/charmhelpers/contrib/__init__.py (+9/-11)
hooks/charmhelpers/contrib/charmsupport/__init__.py (+9/-11)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+38/-14)
hooks/charmhelpers/contrib/charmsupport/volumes.py (+9/-11)
hooks/charmhelpers/contrib/templating/__init__.py (+9/-11)
hooks/charmhelpers/contrib/templating/jinja.py (+9/-11)
hooks/charmhelpers/core/__init__.py (+9/-11)
hooks/charmhelpers/core/decorators.py (+9/-11)
hooks/charmhelpers/core/files.py (+9/-11)
hooks/charmhelpers/core/fstab.py (+9/-11)
hooks/charmhelpers/core/hookenv.py (+95/-18)
hooks/charmhelpers/core/host.py (+240/-119)
hooks/charmhelpers/core/host_factory/__init__.py (+0/-0)
hooks/charmhelpers/core/host_factory/centos.py (+56/-0)
hooks/charmhelpers/core/host_factory/ubuntu.py (+56/-0)
hooks/charmhelpers/core/hugepage.py (+9/-11)
hooks/charmhelpers/core/kernel.py (+30/-26)
hooks/charmhelpers/core/kernel_factory/__init__.py (+0/-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 (+9/-11)
hooks/charmhelpers/core/services/base.py (+9/-11)
hooks/charmhelpers/core/services/helpers.py (+20/-16)
hooks/charmhelpers/core/strutils.py (+9/-11)
hooks/charmhelpers/core/sysctl.py (+9/-11)
hooks/charmhelpers/core/templating.py (+30/-21)
hooks/charmhelpers/core/unitdata.py (+9/-12)
hooks/charmhelpers/fetch/__init__.py (+39/-298)
hooks/charmhelpers/fetch/archiveurl.py (+10/-12)
hooks/charmhelpers/fetch/bzrurl.py (+46/-48)
hooks/charmhelpers/fetch/centos.py (+171/-0)
hooks/charmhelpers/fetch/giturl.py (+30/-34)
hooks/charmhelpers/fetch/ubuntu.py (+336/-0)
hooks/charmhelpers/osplatform.py (+19/-0)
hooks/ntp_hooks.py (+11/-2)
hooks/update-status (+1/-0)
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Review via email: mp+308705@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

This looks familiar...

Again, mostly charmhelpers sync and the new hook looks good.

Someone needs to land this on the git branch since I'm not here.

review: Approve
Revision history for this message
Paul Gear (paulgear) wrote :

Merged; one question about future work (when/if we rewrite this as a reactive charm), it is common practice to use hookenv.status_set('blocked', ...) to indicate what the reactive states are waiting on during deployment. This patch prevents that. Would you be comfortable with assess_status() being called only after the charm is fully installed, and during the update-status hook?

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2new file mode 100644
3index 0000000..ba077a4
4--- /dev/null
5+++ b/.gitignore
6@@ -0,0 +1 @@
7+bin
8diff --git a/charm-helpers-sync.yaml b/charm-helpers-sync.yaml
9index a1cbbae..6d2e9d2 100644
10--- a/charm-helpers-sync.yaml
11+++ b/charm-helpers-sync.yaml
12@@ -1,6 +1,7 @@
13 branch: lp:charm-helpers
14 destination: hooks/charmhelpers
15 include:
16+ - osplatform
17 - core
18 - fetch
19 - contrib.templating.jinja
20diff --git a/hooks/charmhelpers/__init__.py b/hooks/charmhelpers/__init__.py
21index f72e7f8..4886788 100644
22--- a/hooks/charmhelpers/__init__.py
23+++ b/hooks/charmhelpers/__init__.py
24@@ -1,18 +1,16 @@
25 # Copyright 2014-2015 Canonical Limited.
26 #
27-# This file is part of charm-helpers.
28+# Licensed under the Apache License, Version 2.0 (the "License");
29+# you may not use this file except in compliance with the License.
30+# You may obtain a copy of the License at
31 #
32-# charm-helpers is free software: you can redistribute it and/or modify
33-# it under the terms of the GNU Lesser General Public License version 3 as
34-# published by the Free Software Foundation.
35+# http://www.apache.org/licenses/LICENSE-2.0
36 #
37-# charm-helpers is distributed in the hope that it will be useful,
38-# but WITHOUT ANY WARRANTY; without even the implied warranty of
39-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
40-# GNU Lesser General Public License for more details.
41-#
42-# You should have received a copy of the GNU Lesser General Public License
43-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
44+# Unless required by applicable law or agreed to in writing, software
45+# distributed under the License is distributed on an "AS IS" BASIS,
46+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
47+# See the License for the specific language governing permissions and
48+# limitations under the License.
49
50 # Bootstrap charm-helpers, installing its dependencies if necessary using
51 # only standard libraries.
52diff --git a/hooks/charmhelpers/contrib/__init__.py b/hooks/charmhelpers/contrib/__init__.py
53index d1400a0..d7567b8 100644
54--- a/hooks/charmhelpers/contrib/__init__.py
55+++ b/hooks/charmhelpers/contrib/__init__.py
56@@ -1,15 +1,13 @@
57 # Copyright 2014-2015 Canonical Limited.
58 #
59-# This file is part of charm-helpers.
60+# Licensed under the Apache License, Version 2.0 (the "License");
61+# you may not use this file except in compliance with the License.
62+# You may obtain a copy of the License at
63 #
64-# charm-helpers is free software: you can redistribute it and/or modify
65-# it under the terms of the GNU Lesser General Public License version 3 as
66-# published by the Free Software Foundation.
67+# http://www.apache.org/licenses/LICENSE-2.0
68 #
69-# charm-helpers is distributed in the hope that it will be useful,
70-# but WITHOUT ANY WARRANTY; without even the implied warranty of
71-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
72-# GNU Lesser General Public License for more details.
73-#
74-# You should have received a copy of the GNU Lesser General Public License
75-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
76+# Unless required by applicable law or agreed to in writing, software
77+# distributed under the License is distributed on an "AS IS" BASIS,
78+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
79+# See the License for the specific language governing permissions and
80+# limitations under the License.
81diff --git a/hooks/charmhelpers/contrib/charmsupport/__init__.py b/hooks/charmhelpers/contrib/charmsupport/__init__.py
82index d1400a0..d7567b8 100644
83--- a/hooks/charmhelpers/contrib/charmsupport/__init__.py
84+++ b/hooks/charmhelpers/contrib/charmsupport/__init__.py
85@@ -1,15 +1,13 @@
86 # Copyright 2014-2015 Canonical Limited.
87 #
88-# This file is part of charm-helpers.
89+# Licensed under the Apache License, Version 2.0 (the "License");
90+# you may not use this file except in compliance with the License.
91+# You may obtain a copy of the License at
92 #
93-# charm-helpers is free software: you can redistribute it and/or modify
94-# it under the terms of the GNU Lesser General Public License version 3 as
95-# published by the Free Software Foundation.
96+# http://www.apache.org/licenses/LICENSE-2.0
97 #
98-# charm-helpers is distributed in the hope that it will be useful,
99-# but WITHOUT ANY WARRANTY; without even the implied warranty of
100-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
101-# GNU Lesser General Public License for more details.
102-#
103-# You should have received a copy of the GNU Lesser General Public License
104-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
105+# Unless required by applicable law or agreed to in writing, software
106+# distributed under the License is distributed on an "AS IS" BASIS,
107+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
108+# See the License for the specific language governing permissions and
109+# limitations under the License.
110diff --git a/hooks/charmhelpers/contrib/charmsupport/nrpe.py b/hooks/charmhelpers/contrib/charmsupport/nrpe.py
111index 65b1a27..1410512 100644
112--- a/hooks/charmhelpers/contrib/charmsupport/nrpe.py
113+++ b/hooks/charmhelpers/contrib/charmsupport/nrpe.py
114@@ -1,18 +1,16 @@
115 # Copyright 2014-2015 Canonical Limited.
116 #
117-# This file is part of charm-helpers.
118+# Licensed under the Apache License, Version 2.0 (the "License");
119+# you may not use this file except in compliance with the License.
120+# You may obtain a copy of the License at
121 #
122-# charm-helpers is free software: you can redistribute it and/or modify
123-# it under the terms of the GNU Lesser General Public License version 3 as
124-# published by the Free Software Foundation.
125+# http://www.apache.org/licenses/LICENSE-2.0
126 #
127-# charm-helpers is distributed in the hope that it will be useful,
128-# but WITHOUT ANY WARRANTY; without even the implied warranty of
129-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
130-# GNU Lesser General Public License for more details.
131-#
132-# You should have received a copy of the GNU Lesser General Public License
133-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
134+# Unless required by applicable law or agreed to in writing, software
135+# distributed under the License is distributed on an "AS IS" BASIS,
136+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
137+# See the License for the specific language governing permissions and
138+# limitations under the License.
139
140 """Compatibility with the nrpe-external-master charm"""
141 # Copyright 2012 Canonical Ltd.
142@@ -40,6 +38,7 @@ from charmhelpers.core.hookenv import (
143 )
144
145 from charmhelpers.core.host import service
146+from charmhelpers.core import host
147
148 # This module adds compatibility with the nrpe-external-master and plain nrpe
149 # subordinate charms. To use it in your charm:
150@@ -110,6 +109,13 @@ from charmhelpers.core.host import service
151 # def local_monitors_relation_changed():
152 # update_nrpe_config()
153 #
154+# 4.a If your charm is a subordinate charm set primary=False
155+#
156+# from charmsupport.nrpe import NRPE
157+# (...)
158+# def update_nrpe_config():
159+# nrpe_compat = NRPE(primary=False)
160+#
161 # 5. ln -s hooks.py nrpe-external-master-relation-changed
162 # ln -s hooks.py local-monitors-relation-changed
163
164@@ -222,9 +228,10 @@ class NRPE(object):
165 nagios_exportdir = '/var/lib/nagios/export'
166 nrpe_confdir = '/etc/nagios/nrpe.d'
167
168- def __init__(self, hostname=None):
169+ def __init__(self, hostname=None, primary=True):
170 super(NRPE, self).__init__()
171 self.config = config()
172+ self.primary = primary
173 self.nagios_context = self.config['nagios_context']
174 if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
175 self.nagios_servicegroups = self.config['nagios_servicegroups']
176@@ -240,6 +247,12 @@ class NRPE(object):
177 else:
178 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
179 self.checks = []
180+ # Iff in an nrpe-external-master relation hook, set primary status
181+ relation = relation_ids('nrpe-external-master')
182+ if relation:
183+ log("Setting charm primary status {}".format(primary))
184+ for rid in relation_ids('nrpe-external-master'):
185+ relation_set(relation_id=rid, relation_settings={'primary': self.primary})
186
187 def add_check(self, *args, **kwargs):
188 self.checks.append(Check(*args, **kwargs))
189@@ -296,7 +309,7 @@ def get_nagios_hostcontext(relation_name='nrpe-external-master'):
190 :param str relation_name: Name of relation nrpe sub joined to
191 """
192 for rel in relations_of_type(relation_name):
193- if 'nagios_hostname' in rel:
194+ if 'nagios_host_context' in rel:
195 return rel['nagios_host_context']
196
197
198@@ -334,9 +347,20 @@ def add_init_service_checks(nrpe, services, unit_name):
199 :param str unit_name: Unit name to use in check description
200 """
201 for svc in services:
202+ # Don't add a check for these services from neutron-gateway
203+ if svc in ['ext-port', 'os-charm-phy-nic-mtu']:
204+ next
205+
206 upstart_init = '/etc/init/%s.conf' % svc
207 sysv_init = '/etc/init.d/%s' % svc
208- if os.path.exists(upstart_init):
209+
210+ if host.init_is_systemd():
211+ nrpe.add_check(
212+ shortname=svc,
213+ description='process check {%s}' % unit_name,
214+ check_cmd='check_systemd.py %s' % svc
215+ )
216+ elif os.path.exists(upstart_init):
217 nrpe.add_check(
218 shortname=svc,
219 description='process check {%s}' % unit_name,
220diff --git a/hooks/charmhelpers/contrib/charmsupport/volumes.py b/hooks/charmhelpers/contrib/charmsupport/volumes.py
221index 320961b..7ea43f0 100644
222--- a/hooks/charmhelpers/contrib/charmsupport/volumes.py
223+++ b/hooks/charmhelpers/contrib/charmsupport/volumes.py
224@@ -1,18 +1,16 @@
225 # Copyright 2014-2015 Canonical Limited.
226 #
227-# This file is part of charm-helpers.
228+# Licensed under the Apache License, Version 2.0 (the "License");
229+# you may not use this file except in compliance with the License.
230+# You may obtain a copy of the License at
231 #
232-# charm-helpers is free software: you can redistribute it and/or modify
233-# it under the terms of the GNU Lesser General Public License version 3 as
234-# published by the Free Software Foundation.
235+# http://www.apache.org/licenses/LICENSE-2.0
236 #
237-# charm-helpers is distributed in the hope that it will be useful,
238-# but WITHOUT ANY WARRANTY; without even the implied warranty of
239-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
240-# GNU Lesser General Public License for more details.
241-#
242-# You should have received a copy of the GNU Lesser General Public License
243-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
244+# Unless required by applicable law or agreed to in writing, software
245+# distributed under the License is distributed on an "AS IS" BASIS,
246+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
247+# See the License for the specific language governing permissions and
248+# limitations under the License.
249
250 '''
251 Functions for managing volumes in juju units. One volume is supported per unit.
252diff --git a/hooks/charmhelpers/contrib/templating/__init__.py b/hooks/charmhelpers/contrib/templating/__init__.py
253index d1400a0..d7567b8 100644
254--- a/hooks/charmhelpers/contrib/templating/__init__.py
255+++ b/hooks/charmhelpers/contrib/templating/__init__.py
256@@ -1,15 +1,13 @@
257 # Copyright 2014-2015 Canonical Limited.
258 #
259-# This file is part of charm-helpers.
260+# Licensed under the Apache License, Version 2.0 (the "License");
261+# you may not use this file except in compliance with the License.
262+# You may obtain a copy of the License at
263 #
264-# charm-helpers is free software: you can redistribute it and/or modify
265-# it under the terms of the GNU Lesser General Public License version 3 as
266-# published by the Free Software Foundation.
267+# http://www.apache.org/licenses/LICENSE-2.0
268 #
269-# charm-helpers is distributed in the hope that it will be useful,
270-# but WITHOUT ANY WARRANTY; without even the implied warranty of
271-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
272-# GNU Lesser General Public License for more details.
273-#
274-# You should have received a copy of the GNU Lesser General Public License
275-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
276+# Unless required by applicable law or agreed to in writing, software
277+# distributed under the License is distributed on an "AS IS" BASIS,
278+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
279+# See the License for the specific language governing permissions and
280+# limitations under the License.
281diff --git a/hooks/charmhelpers/contrib/templating/jinja.py b/hooks/charmhelpers/contrib/templating/jinja.py
282index c5efb16..38d4fba 100644
283--- a/hooks/charmhelpers/contrib/templating/jinja.py
284+++ b/hooks/charmhelpers/contrib/templating/jinja.py
285@@ -1,18 +1,16 @@
286 # Copyright 2014-2015 Canonical Limited.
287 #
288-# This file is part of charm-helpers.
289+# Licensed under the Apache License, Version 2.0 (the "License");
290+# you may not use this file except in compliance with the License.
291+# You may obtain a copy of the License at
292 #
293-# charm-helpers is free software: you can redistribute it and/or modify
294-# it under the terms of the GNU Lesser General Public License version 3 as
295-# published by the Free Software Foundation.
296+# http://www.apache.org/licenses/LICENSE-2.0
297 #
298-# charm-helpers is distributed in the hope that it will be useful,
299-# but WITHOUT ANY WARRANTY; without even the implied warranty of
300-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
301-# GNU Lesser General Public License for more details.
302-#
303-# You should have received a copy of the GNU Lesser General Public License
304-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
305+# Unless required by applicable law or agreed to in writing, software
306+# distributed under the License is distributed on an "AS IS" BASIS,
307+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
308+# See the License for the specific language governing permissions and
309+# limitations under the License.
310
311 """
312 Templating using the python-jinja2 package.
313diff --git a/hooks/charmhelpers/core/__init__.py b/hooks/charmhelpers/core/__init__.py
314index d1400a0..d7567b8 100644
315--- a/hooks/charmhelpers/core/__init__.py
316+++ b/hooks/charmhelpers/core/__init__.py
317@@ -1,15 +1,13 @@
318 # Copyright 2014-2015 Canonical Limited.
319 #
320-# This file is part of charm-helpers.
321+# Licensed under the Apache License, Version 2.0 (the "License");
322+# you may not use this file except in compliance with the License.
323+# You may obtain a copy of the License at
324 #
325-# charm-helpers is free software: you can redistribute it and/or modify
326-# it under the terms of the GNU Lesser General Public License version 3 as
327-# published by the Free Software Foundation.
328+# http://www.apache.org/licenses/LICENSE-2.0
329 #
330-# charm-helpers is distributed in the hope that it will be useful,
331-# but WITHOUT ANY WARRANTY; without even the implied warranty of
332-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
333-# GNU Lesser General Public License for more details.
334-#
335-# You should have received a copy of the GNU Lesser General Public License
336-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
337+# Unless required by applicable law or agreed to in writing, software
338+# distributed under the License is distributed on an "AS IS" BASIS,
339+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
340+# See the License for the specific language governing permissions and
341+# limitations under the License.
342diff --git a/hooks/charmhelpers/core/decorators.py b/hooks/charmhelpers/core/decorators.py
343index bb05620..6ad41ee 100644
344--- a/hooks/charmhelpers/core/decorators.py
345+++ b/hooks/charmhelpers/core/decorators.py
346@@ -1,18 +1,16 @@
347 # Copyright 2014-2015 Canonical Limited.
348 #
349-# This file is part of charm-helpers.
350+# Licensed under the Apache License, Version 2.0 (the "License");
351+# you may not use this file except in compliance with the License.
352+# You may obtain a copy of the License at
353 #
354-# charm-helpers is free software: you can redistribute it and/or modify
355-# it under the terms of the GNU Lesser General Public License version 3 as
356-# published by the Free Software Foundation.
357+# http://www.apache.org/licenses/LICENSE-2.0
358 #
359-# charm-helpers is distributed in the hope that it will be useful,
360-# but WITHOUT ANY WARRANTY; without even the implied warranty of
361-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
362-# GNU Lesser General Public License for more details.
363-#
364-# You should have received a copy of the GNU Lesser General Public License
365-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
366+# Unless required by applicable law or agreed to in writing, software
367+# distributed under the License is distributed on an "AS IS" BASIS,
368+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
369+# See the License for the specific language governing permissions and
370+# limitations under the License.
371
372 #
373 # Copyright 2014 Canonical Ltd.
374diff --git a/hooks/charmhelpers/core/files.py b/hooks/charmhelpers/core/files.py
375index 0f12d32..fdd82b7 100644
376--- a/hooks/charmhelpers/core/files.py
377+++ b/hooks/charmhelpers/core/files.py
378@@ -3,19 +3,17 @@
379
380 # Copyright 2014-2015 Canonical Limited.
381 #
382-# This file is part of charm-helpers.
383+# Licensed under the Apache License, Version 2.0 (the "License");
384+# you may not use this file except in compliance with the License.
385+# You may obtain a copy of the License at
386 #
387-# charm-helpers is free software: you can redistribute it and/or modify
388-# it under the terms of the GNU Lesser General Public License version 3 as
389-# published by the Free Software Foundation.
390+# http://www.apache.org/licenses/LICENSE-2.0
391 #
392-# charm-helpers is distributed in the hope that it will be useful,
393-# but WITHOUT ANY WARRANTY; without even the implied warranty of
394-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
395-# GNU Lesser General Public License for more details.
396-#
397-# You should have received a copy of the GNU Lesser General Public License
398-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
399+# Unless required by applicable law or agreed to in writing, software
400+# distributed under the License is distributed on an "AS IS" BASIS,
401+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
402+# See the License for the specific language governing permissions and
403+# limitations under the License.
404
405 __author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
406
407diff --git a/hooks/charmhelpers/core/fstab.py b/hooks/charmhelpers/core/fstab.py
408index 3056fba..d9fa915 100644
409--- a/hooks/charmhelpers/core/fstab.py
410+++ b/hooks/charmhelpers/core/fstab.py
411@@ -3,19 +3,17 @@
412
413 # Copyright 2014-2015 Canonical Limited.
414 #
415-# This file is part of charm-helpers.
416+# Licensed under the Apache License, Version 2.0 (the "License");
417+# you may not use this file except in compliance with the License.
418+# You may obtain a copy of the License at
419 #
420-# charm-helpers is free software: you can redistribute it and/or modify
421-# it under the terms of the GNU Lesser General Public License version 3 as
422-# published by the Free Software Foundation.
423+# http://www.apache.org/licenses/LICENSE-2.0
424 #
425-# charm-helpers is distributed in the hope that it will be useful,
426-# but WITHOUT ANY WARRANTY; without even the implied warranty of
427-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
428-# GNU Lesser General Public License for more details.
429-#
430-# You should have received a copy of the GNU Lesser General Public License
431-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
432+# Unless required by applicable law or agreed to in writing, software
433+# distributed under the License is distributed on an "AS IS" BASIS,
434+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
435+# See the License for the specific language governing permissions and
436+# limitations under the License.
437
438 import io
439 import os
440diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py
441index 454b52a..996e81c 100644
442--- a/hooks/charmhelpers/core/hookenv.py
443+++ b/hooks/charmhelpers/core/hookenv.py
444@@ -1,18 +1,16 @@
445 # Copyright 2014-2015 Canonical Limited.
446 #
447-# This file is part of charm-helpers.
448+# Licensed under the Apache License, Version 2.0 (the "License");
449+# you may not use this file except in compliance with the License.
450+# You may obtain a copy of the License at
451 #
452-# charm-helpers is free software: you can redistribute it and/or modify
453-# it under the terms of the GNU Lesser General Public License version 3 as
454-# published by the Free Software Foundation.
455+# http://www.apache.org/licenses/LICENSE-2.0
456 #
457-# charm-helpers is distributed in the hope that it will be useful,
458-# but WITHOUT ANY WARRANTY; without even the implied warranty of
459-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
460-# GNU Lesser General Public License for more details.
461-#
462-# You should have received a copy of the GNU Lesser General Public License
463-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
464+# Unless required by applicable law or agreed to in writing, software
465+# distributed under the License is distributed on an "AS IS" BASIS,
466+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
467+# See the License for the specific language governing permissions and
468+# limitations under the License.
469
470 "Interactions with the Juju environment"
471 # Copyright 2013 Canonical Ltd.
472@@ -492,7 +490,7 @@ def relation_types():
473
474 @cached
475 def peer_relation_id():
476- '''Get a peer relation id if a peer relation has been joined, else None.'''
477+ '''Get the peers relation id if a peers relation has been joined, else None.'''
478 md = metadata()
479 section = md.get('peers')
480 if section:
481@@ -517,12 +515,12 @@ def relation_to_interface(relation_name):
482 def relation_to_role_and_interface(relation_name):
483 """
484 Given the name of a relation, return the role and the name of the interface
485- that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
486+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
487
488 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
489 """
490 _metadata = metadata()
491- for role in ('provides', 'requires', 'peer'):
492+ for role in ('provides', 'requires', 'peers'):
493 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
494 if interface:
495 return role, interface
496@@ -534,7 +532,7 @@ def role_and_interface_to_relations(role, interface_name):
497 """
498 Given a role and interface name, return a list of relation names for the
499 current charm that use that interface under that role (where role is one
500- of ``provides``, ``requires``, or ``peer``).
501+ of ``provides``, ``requires``, or ``peers``).
502
503 :returns: A list of relation names.
504 """
505@@ -555,7 +553,7 @@ def interface_to_relations(interface_name):
506 :returns: A list of relation names.
507 """
508 results = []
509- for role in ('provides', 'requires', 'peer'):
510+ for role in ('provides', 'requires', 'peers'):
511 results.extend(role_and_interface_to_relations(role, interface_name))
512 return results
513
514@@ -637,7 +635,7 @@ def unit_private_ip():
515
516
517 @cached
518-def storage_get(attribute="", storage_id=""):
519+def storage_get(attribute=None, storage_id=None):
520 """Get storage attributes"""
521 _args = ['storage-get', '--format=json']
522 if storage_id:
523@@ -651,7 +649,7 @@ def storage_get(attribute="", storage_id=""):
524
525
526 @cached
527-def storage_list(storage_name=""):
528+def storage_list(storage_name=None):
529 """List the storage IDs for the unit"""
530 _args = ['storage-list', '--format=json']
531 if storage_name:
532@@ -845,6 +843,20 @@ def translate_exc(from_exc, to_exc):
533 return inner_translate_exc1
534
535
536+def application_version_set(version):
537+ """Charm authors may trigger this command from any hook to output what
538+ version of the application is running. This could be a package version,
539+ for instance postgres version 9.5. It could also be a build number or
540+ version control revision identifier, for instance git sha 6fb7ba68. """
541+
542+ cmd = ['application-version-set']
543+ cmd.append(version)
544+ try:
545+ subprocess.check_call(cmd)
546+ except OSError:
547+ log("Application Version: {}".format(version))
548+
549+
550 @translate_exc(from_exc=OSError, to_exc=NotImplementedError)
551 def is_leader():
552 """Does the current unit hold the juju leadership
553@@ -878,6 +890,58 @@ def leader_set(settings=None, **kwargs):
554 subprocess.check_call(cmd)
555
556
557+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
558+def payload_register(ptype, klass, pid):
559+ """ is used while a hook is running to let Juju know that a
560+ payload has been started."""
561+ cmd = ['payload-register']
562+ for x in [ptype, klass, pid]:
563+ cmd.append(x)
564+ subprocess.check_call(cmd)
565+
566+
567+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
568+def payload_unregister(klass, pid):
569+ """ is used while a hook is running to let Juju know
570+ that a payload has been manually stopped. The <class> and <id> provided
571+ must match a payload that has been previously registered with juju using
572+ payload-register."""
573+ cmd = ['payload-unregister']
574+ for x in [klass, pid]:
575+ cmd.append(x)
576+ subprocess.check_call(cmd)
577+
578+
579+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
580+def payload_status_set(klass, pid, status):
581+ """is used to update the current status of a registered payload.
582+ The <class> and <id> provided must match a payload that has been previously
583+ registered with juju using payload-register. The <status> must be one of the
584+ follow: starting, started, stopping, stopped"""
585+ cmd = ['payload-status-set']
586+ for x in [klass, pid, status]:
587+ cmd.append(x)
588+ subprocess.check_call(cmd)
589+
590+
591+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
592+def resource_get(name):
593+ """used to fetch the resource path of the given name.
594+
595+ <name> must match a name of defined resource in metadata.yaml
596+
597+ returns either a path or False if resource not available
598+ """
599+ if not name:
600+ return False
601+
602+ cmd = ['resource-get', name]
603+ try:
604+ return subprocess.check_output(cmd).decode('UTF-8')
605+ except subprocess.CalledProcessError:
606+ return False
607+
608+
609 @cached
610 def juju_version():
611 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
612@@ -942,3 +1006,16 @@ def _run_atexit():
613 for callback, args, kwargs in reversed(_atexit):
614 callback(*args, **kwargs)
615 del _atexit[:]
616+
617+
618+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
619+def network_get_primary_address(binding):
620+ '''
621+ Retrieve the primary network address for a named binding
622+
623+ :param binding: string. The name of a relation of extra-binding
624+ :return: string. The primary IP address for the named binding
625+ :raise: NotImplementedError if run on Juju < 2.0
626+ '''
627+ cmd = ['network-get', '--primary-address', binding]
628+ return subprocess.check_output(cmd).decode('UTF-8').strip()
629diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py
630index 0d9c64f..0f1b2f3 100644
631--- a/hooks/charmhelpers/core/host.py
632+++ b/hooks/charmhelpers/core/host.py
633@@ -1,18 +1,16 @@
634 # Copyright 2014-2015 Canonical Limited.
635 #
636-# This file is part of charm-helpers.
637+# Licensed under the Apache License, Version 2.0 (the "License");
638+# you may not use this file except in compliance with the License.
639+# You may obtain a copy of the License at
640 #
641-# charm-helpers is free software: you can redistribute it and/or modify
642-# it under the terms of the GNU Lesser General Public License version 3 as
643-# published by the Free Software Foundation.
644+# http://www.apache.org/licenses/LICENSE-2.0
645 #
646-# charm-helpers is distributed in the hope that it will be useful,
647-# but WITHOUT ANY WARRANTY; without even the implied warranty of
648-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
649-# GNU Lesser General Public License for more details.
650-#
651-# You should have received a copy of the GNU Lesser General Public License
652-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
653+# Unless required by applicable law or agreed to in writing, software
654+# distributed under the License is distributed on an "AS IS" BASIS,
655+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
656+# See the License for the specific language governing permissions and
657+# limitations under the License.
658
659 """Tools for working with the host system"""
660 # Copyright 2012 Canonical Ltd.
661@@ -30,13 +28,31 @@ import random
662 import string
663 import subprocess
664 import hashlib
665-from contextlib import contextmanager
666-from collections import OrderedDict
667-
668+import functools
669+import itertools
670 import six
671
672+from contextlib import contextmanager
673+from collections import OrderedDict
674 from .hookenv import log
675 from .fstab import Fstab
676+from charmhelpers.osplatform import get_platform
677+
678+__platform__ = get_platform()
679+if __platform__ == "ubuntu":
680+ from charmhelpers.core.host_factory.ubuntu import (
681+ service_available,
682+ add_new_group,
683+ lsb_release,
684+ cmp_pkgrevno,
685+ ) # flake8: noqa -- ignore F401 for this import
686+elif __platform__ == "centos":
687+ from charmhelpers.core.host_factory.centos import (
688+ service_available,
689+ add_new_group,
690+ lsb_release,
691+ cmp_pkgrevno,
692+ ) # flake8: noqa -- ignore F401 for this import
693
694
695 def service_start(service_name):
696@@ -67,10 +83,14 @@ def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
697 """Pause a system service.
698
699 Stop it, and prevent it from starting again at boot."""
700- stopped = service_stop(service_name)
701+ stopped = True
702+ if service_running(service_name):
703+ stopped = service_stop(service_name)
704 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
705 sysv_file = os.path.join(initd_dir, service_name)
706- if os.path.exists(upstart_file):
707+ if init_is_systemd():
708+ service('disable', service_name)
709+ elif os.path.exists(upstart_file):
710 override_path = os.path.join(
711 init_dir, '{}.override'.format(service_name))
712 with open(override_path, 'w') as fh:
713@@ -78,9 +98,9 @@ def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
714 elif os.path.exists(sysv_file):
715 subprocess.check_call(["update-rc.d", service_name, "disable"])
716 else:
717- # XXX: Support SystemD too
718 raise ValueError(
719- "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
720+ "Unable to detect {0} as SystemD, Upstart {1} or"
721+ " SysV {2}".format(
722 service_name, upstart_file, sysv_file))
723 return stopped
724
725@@ -92,7 +112,9 @@ def service_resume(service_name, init_dir="/etc/init",
726 Reenable starting again at boot. Start the service"""
727 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
728 sysv_file = os.path.join(initd_dir, service_name)
729- if os.path.exists(upstart_file):
730+ if init_is_systemd():
731+ service('enable', service_name)
732+ elif os.path.exists(upstart_file):
733 override_path = os.path.join(
734 init_dir, '{}.override'.format(service_name))
735 if os.path.exists(override_path):
736@@ -100,56 +122,95 @@ def service_resume(service_name, init_dir="/etc/init",
737 elif os.path.exists(sysv_file):
738 subprocess.check_call(["update-rc.d", service_name, "enable"])
739 else:
740- # XXX: Support SystemD too
741 raise ValueError(
742- "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
743+ "Unable to detect {0} as SystemD, Upstart {1} or"
744+ " SysV {2}".format(
745 service_name, upstart_file, sysv_file))
746
747- started = service_start(service_name)
748+ started = service_running(service_name)
749+ if not started:
750+ started = service_start(service_name)
751 return started
752
753
754 def service(action, service_name):
755 """Control a system service"""
756- cmd = ['service', service_name, action]
757+ if init_is_systemd():
758+ cmd = ['systemctl', action, service_name]
759+ else:
760+ cmd = ['service', service_name, action]
761 return subprocess.call(cmd) == 0
762
763
764-def service_running(service):
765+_UPSTART_CONF = "/etc/init/{}.conf"
766+_INIT_D_CONF = "/etc/init.d/{}"
767+
768+
769+def service_running(service_name):
770 """Determine whether a system service is running"""
771- try:
772- output = subprocess.check_output(
773- ['service', service, 'status'],
774- stderr=subprocess.STDOUT).decode('UTF-8')
775- except subprocess.CalledProcessError:
776- return False
777+ if init_is_systemd():
778+ return service('is-active', service_name)
779 else:
780- if ("start/running" in output or "is running" in output):
781- return True
782- else:
783- return False
784+ if os.path.exists(_UPSTART_CONF.format(service_name)):
785+ try:
786+ output = subprocess.check_output(
787+ ['status', service_name],
788+ stderr=subprocess.STDOUT).decode('UTF-8')
789+ except subprocess.CalledProcessError:
790+ return False
791+ else:
792+ # This works for upstart scripts where the 'service' command
793+ # returns a consistent string to represent running
794+ # 'start/running'
795+ if ("start/running" in output or
796+ "is running" in output or
797+ "up and running" in output):
798+ return True
799+ elif os.path.exists(_INIT_D_CONF.format(service_name)):
800+ # Check System V scripts init script return codes
801+ return service('status', service_name)
802+ return False
803
804
805-def service_available(service_name):
806- """Determine whether a system service is available"""
807- try:
808- subprocess.check_output(
809- ['service', service_name, 'status'],
810- stderr=subprocess.STDOUT).decode('UTF-8')
811- except subprocess.CalledProcessError as e:
812- return b'unrecognized service' not in e.output
813- else:
814- return True
815+SYSTEMD_SYSTEM = '/run/systemd/system'
816+
817+
818+def init_is_systemd():
819+ """Return True if the host system uses systemd, False otherwise."""
820+ return os.path.isdir(SYSTEMD_SYSTEM)
821+
822
823+def adduser(username, password=None, shell='/bin/bash',
824+ system_user=False, primary_group=None,
825+ secondary_groups=None, uid=None, home_dir=None):
826+ """Add a user to the system.
827
828-def adduser(username, password=None, shell='/bin/bash', system_user=False):
829- """Add a user to the system"""
830+ Will log but otherwise succeed if the user already exists.
831+
832+ :param str username: Username to create
833+ :param str password: Password for user; if ``None``, create a system user
834+ :param str shell: The default shell for the user
835+ :param bool system_user: Whether to create a login or system user
836+ :param str primary_group: Primary group for user; defaults to username
837+ :param list secondary_groups: Optional list of additional groups
838+ :param int uid: UID for user being created
839+ :param str home_dir: Home directory for user
840+
841+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
842+ """
843 try:
844 user_info = pwd.getpwnam(username)
845 log('user {0} already exists!'.format(username))
846+ if uid:
847+ user_info = pwd.getpwuid(int(uid))
848+ log('user with uid {0} already exists!'.format(uid))
849 except KeyError:
850 log('creating user {0}'.format(username))
851 cmd = ['useradd']
852+ if uid:
853+ cmd.extend(['--uid', str(uid)])
854+ if home_dir:
855+ cmd.extend(['--home', str(home_dir)])
856 if system_user or password is None:
857 cmd.append('--system')
858 else:
859@@ -158,6 +219,16 @@ def adduser(username, password=None, shell='/bin/bash', system_user=False):
860 '--shell', shell,
861 '--password', password,
862 ])
863+ if not primary_group:
864+ try:
865+ grp.getgrnam(username)
866+ primary_group = username # avoid "group exists" error
867+ except KeyError:
868+ pass
869+ if primary_group:
870+ cmd.extend(['-g', primary_group])
871+ if secondary_groups:
872+ cmd.extend(['-G', ','.join(secondary_groups)])
873 cmd.append(username)
874 subprocess.check_call(cmd)
875 user_info = pwd.getpwnam(username)
876@@ -174,22 +245,56 @@ def user_exists(username):
877 return user_exists
878
879
880-def add_group(group_name, system_group=False):
881- """Add a group to the system"""
882+def uid_exists(uid):
883+ """Check if a uid exists"""
884+ try:
885+ pwd.getpwuid(uid)
886+ uid_exists = True
887+ except KeyError:
888+ uid_exists = False
889+ return uid_exists
890+
891+
892+def group_exists(groupname):
893+ """Check if a group exists"""
894+ try:
895+ grp.getgrnam(groupname)
896+ group_exists = True
897+ except KeyError:
898+ group_exists = False
899+ return group_exists
900+
901+
902+def gid_exists(gid):
903+ """Check if a gid exists"""
904+ try:
905+ grp.getgrgid(gid)
906+ gid_exists = True
907+ except KeyError:
908+ gid_exists = False
909+ return gid_exists
910+
911+
912+def add_group(group_name, system_group=False, gid=None):
913+ """Add a group to the system
914+
915+ Will log but otherwise succeed if the group already exists.
916+
917+ :param str group_name: group to create
918+ :param bool system_group: Create system group
919+ :param int gid: GID for user being created
920+
921+ :returns: The password database entry struct, as returned by `grp.getgrnam`
922+ """
923 try:
924 group_info = grp.getgrnam(group_name)
925 log('group {0} already exists!'.format(group_name))
926+ if gid:
927+ group_info = grp.getgrgid(gid)
928+ log('group with gid {0} already exists!'.format(gid))
929 except KeyError:
930 log('creating group {0}'.format(group_name))
931- cmd = ['addgroup']
932- if system_group:
933- cmd.append('--system')
934- else:
935- cmd.extend([
936- '--group',
937- ])
938- cmd.append(group_name)
939- subprocess.check_call(cmd)
940+ add_new_group(group_name, system_group, gid)
941 group_info = grp.getgrnam(group_name)
942 return group_info
943
944@@ -255,14 +360,12 @@ def write_file(path, content, owner='root', group='root', perms=0o444):
945
946
947 def fstab_remove(mp):
948- """Remove the given mountpoint entry from /etc/fstab
949- """
950+ """Remove the given mountpoint entry from /etc/fstab"""
951 return Fstab.remove_by_mountpoint(mp)
952
953
954 def fstab_add(dev, mp, fs, options=None):
955- """Adds the given device entry to the /etc/fstab file
956- """
957+ """Adds the given device entry to the /etc/fstab file"""
958 return Fstab.add(dev, mp, fs, options=options)
959
960
961@@ -318,8 +421,7 @@ def fstab_mount(mountpoint):
962
963
964 def file_hash(path, hash_type='md5'):
965- """
966- Generate a hash checksum of the contents of 'path' or None if not found.
967+ """Generate a hash checksum of the contents of 'path' or None if not found.
968
969 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
970 such as md5, sha1, sha256, sha512, etc.
971@@ -334,10 +436,9 @@ def file_hash(path, hash_type='md5'):
972
973
974 def path_hash(path):
975- """
976- Generate a hash checksum of all files matching 'path'. Standard wildcards
977- like '*' and '?' are supported, see documentation for the 'glob' module for
978- more information.
979+ """Generate a hash checksum of all files matching 'path'. Standard
980+ wildcards like '*' and '?' are supported, see documentation for the 'glob'
981+ module for more information.
982
983 :return: dict: A { filename: hash } dictionary for all matched files.
984 Empty if none found.
985@@ -349,8 +450,7 @@ def path_hash(path):
986
987
988 def check_hash(path, checksum, hash_type='md5'):
989- """
990- Validate a file using a cryptographic checksum.
991+ """Validate a file using a cryptographic checksum.
992
993 :param str checksum: Value of the checksum used to validate the file.
994 :param str hash_type: Hash algorithm used to generate `checksum`.
995@@ -365,10 +465,11 @@ def check_hash(path, checksum, hash_type='md5'):
996
997
998 class ChecksumError(ValueError):
999+ """A class derived from Value error to indicate the checksum failed."""
1000 pass
1001
1002
1003-def restart_on_change(restart_map, stopstart=False):
1004+def restart_on_change(restart_map, stopstart=False, restart_functions=None):
1005 """Restart services based on configuration files changing
1006
1007 This function is used a decorator, for example::
1008@@ -386,35 +487,56 @@ def restart_on_change(restart_map, stopstart=False):
1009 restarted if any file matching the pattern got changed, created
1010 or removed. Standard wildcards are supported, see documentation
1011 for the 'glob' module for more information.
1012+
1013+ @param restart_map: {path_file_name: [service_name, ...]
1014+ @param stopstart: DEFAULT false; whether to stop, start OR restart
1015+ @param restart_functions: nonstandard functions to use to restart services
1016+ {svc: func, ...}
1017+ @returns result from decorated function
1018 """
1019 def wrap(f):
1020+ @functools.wraps(f)
1021 def wrapped_f(*args, **kwargs):
1022- checksums = {path: path_hash(path) for path in restart_map}
1023- f(*args, **kwargs)
1024- restarts = []
1025- for path in restart_map:
1026- if path_hash(path) != checksums[path]:
1027- restarts += restart_map[path]
1028- services_list = list(OrderedDict.fromkeys(restarts))
1029- if not stopstart:
1030- for service_name in services_list:
1031- service('restart', service_name)
1032- else:
1033- for action in ['stop', 'start']:
1034- for service_name in services_list:
1035- service(action, service_name)
1036+ return restart_on_change_helper(
1037+ (lambda: f(*args, **kwargs)), restart_map, stopstart,
1038+ restart_functions)
1039 return wrapped_f
1040 return wrap
1041
1042
1043-def lsb_release():
1044- """Return /etc/lsb-release in a dict"""
1045- d = {}
1046- with open('/etc/lsb-release', 'r') as lsb:
1047- for l in lsb:
1048- k, v = l.split('=')
1049- d[k.strip()] = v.strip()
1050- return d
1051+def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
1052+ restart_functions=None):
1053+ """Helper function to perform the restart_on_change function.
1054+
1055+ This is provided for decorators to restart services if files described
1056+ in the restart_map have changed after an invocation of lambda_f().
1057+
1058+ @param lambda_f: function to call.
1059+ @param restart_map: {file: [service, ...]}
1060+ @param stopstart: whether to stop, start or restart a service
1061+ @param restart_functions: nonstandard functions to use to restart services
1062+ {svc: func, ...}
1063+ @returns result of lambda_f()
1064+ """
1065+ if restart_functions is None:
1066+ restart_functions = {}
1067+ checksums = {path: path_hash(path) for path in restart_map}
1068+ r = lambda_f()
1069+ # create a list of lists of the services to restart
1070+ restarts = [restart_map[path]
1071+ for path in restart_map
1072+ if path_hash(path) != checksums[path]]
1073+ # create a flat list of ordered services without duplicates from lists
1074+ services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
1075+ if services_list:
1076+ actions = ('stop', 'start') if stopstart else ('restart',)
1077+ for service_name in services_list:
1078+ if service_name in restart_functions:
1079+ restart_functions[service_name](service_name)
1080+ else:
1081+ for action in actions:
1082+ service(action, service_name)
1083+ return r
1084
1085
1086 def pwgen(length=None):
1087@@ -470,7 +592,7 @@ def get_bond_master(interface):
1088
1089
1090 def list_nics(nic_type=None):
1091- '''Return a list of nics of given type(s)'''
1092+ """Return a list of nics of given type(s)"""
1093 if isinstance(nic_type, six.string_types):
1094 int_types = [nic_type]
1095 else:
1096@@ -512,12 +634,13 @@ def list_nics(nic_type=None):
1097
1098
1099 def set_nic_mtu(nic, mtu):
1100- '''Set MTU on a network interface'''
1101+ """Set the Maximum Transmission Unit (MTU) on a network interface."""
1102 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
1103 subprocess.check_call(cmd)
1104
1105
1106 def get_nic_mtu(nic):
1107+ """Return the Maximum Transmission Unit (MTU) for a network interface."""
1108 cmd = ['ip', 'addr', 'show', nic]
1109 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1110 mtu = ""
1111@@ -529,6 +652,7 @@ def get_nic_mtu(nic):
1112
1113
1114 def get_nic_hwaddr(nic):
1115+ """Return the Media Access Control (MAC) for a network interface."""
1116 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1117 ip_output = subprocess.check_output(cmd).decode('UTF-8')
1118 hwaddr = ""
1119@@ -538,39 +662,28 @@ def get_nic_hwaddr(nic):
1120 return hwaddr
1121
1122
1123-def cmp_pkgrevno(package, revno, pkgcache=None):
1124- '''Compare supplied revno with the revno of the installed package
1125-
1126- * 1 => Installed revno is greater than supplied arg
1127- * 0 => Installed revno is the same as supplied arg
1128- * -1 => Installed revno is less than supplied arg
1129-
1130- This function imports apt_cache function from charmhelpers.fetch if
1131- the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1132- you call this function, or pass an apt_pkg.Cache() instance.
1133- '''
1134- import apt_pkg
1135- if not pkgcache:
1136- from charmhelpers.fetch import apt_cache
1137- pkgcache = apt_cache()
1138- pkg = pkgcache[package]
1139- return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
1140-
1141-
1142 @contextmanager
1143-def chdir(d):
1144+def chdir(directory):
1145+ """Change the current working directory to a different directory for a code
1146+ block and return the previous directory after the block exits. Useful to
1147+ run commands from a specificed directory.
1148+
1149+ :param str directory: The directory path to change to for this context.
1150+ """
1151 cur = os.getcwd()
1152 try:
1153- yield os.chdir(d)
1154+ yield os.chdir(directory)
1155 finally:
1156 os.chdir(cur)
1157
1158
1159 def chownr(path, owner, group, follow_links=True, chowntopdir=False):
1160- """
1161- Recursively change user and group ownership of files and directories
1162+ """Recursively change user and group ownership of files and directories
1163 in given path. Doesn't chown path itself by default, only its children.
1164
1165+ :param str path: The string path to start changing ownership.
1166+ :param str owner: The owner string to use when looking up the uid.
1167+ :param str group: The group string to use when looking up the gid.
1168 :param bool follow_links: Also Chown links if True
1169 :param bool chowntopdir: Also chown path itself if True
1170 """
1171@@ -594,15 +707,23 @@ def chownr(path, owner, group, follow_links=True, chowntopdir=False):
1172
1173
1174 def lchownr(path, owner, group):
1175+ """Recursively change user and group ownership of files and directories
1176+ in a given path, not following symbolic links. See the documentation for
1177+ 'os.lchown' for more information.
1178+
1179+ :param str path: The string path to start changing ownership.
1180+ :param str owner: The owner string to use when looking up the uid.
1181+ :param str group: The group string to use when looking up the gid.
1182+ """
1183 chownr(path, owner, group, follow_links=False)
1184
1185
1186 def get_total_ram():
1187- '''The total amount of system RAM in bytes.
1188+ """The total amount of system RAM in bytes.
1189
1190 This is what is reported by the OS, and may be overcommitted when
1191 there are multiple containers hosted on the same machine.
1192- '''
1193+ """
1194 with open('/proc/meminfo', 'r') as f:
1195 for line in f.readlines():
1196 if line:
1197diff --git a/hooks/charmhelpers/core/host_factory/__init__.py b/hooks/charmhelpers/core/host_factory/__init__.py
1198new file mode 100644
1199index 0000000..e69de29
1200--- /dev/null
1201+++ b/hooks/charmhelpers/core/host_factory/__init__.py
1202diff --git a/hooks/charmhelpers/core/host_factory/centos.py b/hooks/charmhelpers/core/host_factory/centos.py
1203new file mode 100644
1204index 0000000..902d469
1205--- /dev/null
1206+++ b/hooks/charmhelpers/core/host_factory/centos.py
1207@@ -0,0 +1,56 @@
1208+import subprocess
1209+import yum
1210+import os
1211+
1212+
1213+def service_available(service_name):
1214+ # """Determine whether a system service is available."""
1215+ if os.path.isdir('/run/systemd/system'):
1216+ cmd = ['systemctl', 'is-enabled', service_name]
1217+ else:
1218+ cmd = ['service', service_name, 'is-enabled']
1219+ return subprocess.call(cmd) == 0
1220+
1221+
1222+def add_new_group(group_name, system_group=False, gid=None):
1223+ cmd = ['groupadd']
1224+ if gid:
1225+ cmd.extend(['--gid', str(gid)])
1226+ if system_group:
1227+ cmd.append('-r')
1228+ cmd.append(group_name)
1229+ subprocess.check_call(cmd)
1230+
1231+
1232+def lsb_release():
1233+ """Return /etc/os-release in a dict."""
1234+ d = {}
1235+ with open('/etc/os-release', 'r') as lsb:
1236+ for l in lsb:
1237+ s = l.split('=')
1238+ if len(s) != 2:
1239+ continue
1240+ d[s[0].strip()] = s[1].strip()
1241+ return d
1242+
1243+
1244+def cmp_pkgrevno(package, revno, pkgcache=None):
1245+ """Compare supplied revno with the revno of the installed package.
1246+
1247+ * 1 => Installed revno is greater than supplied arg
1248+ * 0 => Installed revno is the same as supplied arg
1249+ * -1 => Installed revno is less than supplied arg
1250+
1251+ This function imports YumBase function if the pkgcache argument
1252+ is None.
1253+ """
1254+ if not pkgcache:
1255+ y = yum.YumBase()
1256+ packages = y.doPackageLists()
1257+ pkgcache = {i.Name: i.version for i in packages['installed']}
1258+ pkg = pkgcache[package]
1259+ if pkg > revno:
1260+ return 1
1261+ if pkg < revno:
1262+ return -1
1263+ return 0
1264diff --git a/hooks/charmhelpers/core/host_factory/ubuntu.py b/hooks/charmhelpers/core/host_factory/ubuntu.py
1265new file mode 100644
1266index 0000000..8c66af5
1267--- /dev/null
1268+++ b/hooks/charmhelpers/core/host_factory/ubuntu.py
1269@@ -0,0 +1,56 @@
1270+import subprocess
1271+
1272+
1273+def service_available(service_name):
1274+ """Determine whether a system service is available"""
1275+ try:
1276+ subprocess.check_output(
1277+ ['service', service_name, 'status'],
1278+ stderr=subprocess.STDOUT).decode('UTF-8')
1279+ except subprocess.CalledProcessError as e:
1280+ return b'unrecognized service' not in e.output
1281+ else:
1282+ return True
1283+
1284+
1285+def add_new_group(group_name, system_group=False, gid=None):
1286+ cmd = ['addgroup']
1287+ if gid:
1288+ cmd.extend(['--gid', str(gid)])
1289+ if system_group:
1290+ cmd.append('--system')
1291+ else:
1292+ cmd.extend([
1293+ '--group',
1294+ ])
1295+ cmd.append(group_name)
1296+ subprocess.check_call(cmd)
1297+
1298+
1299+def lsb_release():
1300+ """Return /etc/lsb-release in a dict"""
1301+ d = {}
1302+ with open('/etc/lsb-release', 'r') as lsb:
1303+ for l in lsb:
1304+ k, v = l.split('=')
1305+ d[k.strip()] = v.strip()
1306+ return d
1307+
1308+
1309+def cmp_pkgrevno(package, revno, pkgcache=None):
1310+ """Compare supplied revno with the revno of the installed package.
1311+
1312+ * 1 => Installed revno is greater than supplied arg
1313+ * 0 => Installed revno is the same as supplied arg
1314+ * -1 => Installed revno is less than supplied arg
1315+
1316+ This function imports apt_cache function from charmhelpers.fetch if
1317+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1318+ you call this function, or pass an apt_pkg.Cache() instance.
1319+ """
1320+ import apt_pkg
1321+ if not pkgcache:
1322+ from charmhelpers.fetch import apt_cache
1323+ pkgcache = apt_cache()
1324+ pkg = pkgcache[package]
1325+ return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
1326diff --git a/hooks/charmhelpers/core/hugepage.py b/hooks/charmhelpers/core/hugepage.py
1327index a783ad9..54b5b5e 100644
1328--- a/hooks/charmhelpers/core/hugepage.py
1329+++ b/hooks/charmhelpers/core/hugepage.py
1330@@ -2,19 +2,17 @@
1331
1332 # Copyright 2014-2015 Canonical Limited.
1333 #
1334-# This file is part of charm-helpers.
1335+# Licensed under the Apache License, Version 2.0 (the "License");
1336+# you may not use this file except in compliance with the License.
1337+# You may obtain a copy of the License at
1338 #
1339-# charm-helpers is free software: you can redistribute it and/or modify
1340-# it under the terms of the GNU Lesser General Public License version 3 as
1341-# published by the Free Software Foundation.
1342+# http://www.apache.org/licenses/LICENSE-2.0
1343 #
1344-# charm-helpers is distributed in the hope that it will be useful,
1345-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1346-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1347-# GNU Lesser General Public License for more details.
1348-#
1349-# You should have received a copy of the GNU Lesser General Public License
1350-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1351+# Unless required by applicable law or agreed to in writing, software
1352+# distributed under the License is distributed on an "AS IS" BASIS,
1353+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1354+# See the License for the specific language governing permissions and
1355+# limitations under the License.
1356
1357 import yaml
1358 from charmhelpers.core import fstab
1359diff --git a/hooks/charmhelpers/core/kernel.py b/hooks/charmhelpers/core/kernel.py
1360index 5dc6495..2d40452 100644
1361--- a/hooks/charmhelpers/core/kernel.py
1362+++ b/hooks/charmhelpers/core/kernel.py
1363@@ -3,29 +3,40 @@
1364
1365 # Copyright 2014-2015 Canonical Limited.
1366 #
1367-# This file is part of charm-helpers.
1368+# Licensed under the Apache License, Version 2.0 (the "License");
1369+# you may not use this file except in compliance with the License.
1370+# You may obtain a copy of the License at
1371 #
1372-# charm-helpers is free software: you can redistribute it and/or modify
1373-# it under the terms of the GNU Lesser General Public License version 3 as
1374-# published by the Free Software Foundation.
1375+# http://www.apache.org/licenses/LICENSE-2.0
1376 #
1377-# charm-helpers is distributed in the hope that it will be useful,
1378-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1379-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1380-# GNU Lesser General Public License for more details.
1381-#
1382-# You should have received a copy of the GNU Lesser General Public License
1383-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1384+# Unless required by applicable law or agreed to in writing, software
1385+# distributed under the License is distributed on an "AS IS" BASIS,
1386+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1387+# See the License for the specific language governing permissions and
1388+# limitations under the License.
1389
1390-__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1391+import re
1392+import subprocess
1393
1394+from charmhelpers.osplatform import get_platform
1395 from charmhelpers.core.hookenv import (
1396 log,
1397 INFO
1398 )
1399
1400-from subprocess import check_call, check_output
1401-import re
1402+__platform__ = get_platform()
1403+if __platform__ == "ubuntu":
1404+ from charmhelpers.core.kernel_factory.ubuntu import (
1405+ persistent_modprobe,
1406+ update_initramfs,
1407+ ) # flake8: noqa -- ignore F401 for this import
1408+elif __platform__ == "centos":
1409+ from charmhelpers.core.kernel_factory.centos import (
1410+ persistent_modprobe,
1411+ update_initramfs,
1412+ ) # flake8: noqa -- ignore F401 for this import
1413+
1414+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1415
1416
1417 def modprobe(module, persist=True):
1418@@ -34,11 +45,9 @@ def modprobe(module, persist=True):
1419
1420 log('Loading kernel module %s' % module, level=INFO)
1421
1422- check_call(cmd)
1423+ subprocess.check_call(cmd)
1424 if persist:
1425- with open('/etc/modules', 'r+') as modules:
1426- if module not in modules.read():
1427- modules.write(module)
1428+ persistent_modprobe(module)
1429
1430
1431 def rmmod(module, force=False):
1432@@ -48,21 +57,16 @@ def rmmod(module, force=False):
1433 cmd.append('-f')
1434 cmd.append(module)
1435 log('Removing kernel module %s' % module, level=INFO)
1436- return check_call(cmd)
1437+ return subprocess.check_call(cmd)
1438
1439
1440 def lsmod():
1441 """Shows what kernel modules are currently loaded"""
1442- return check_output(['lsmod'],
1443- universal_newlines=True)
1444+ return subprocess.check_output(['lsmod'],
1445+ universal_newlines=True)
1446
1447
1448 def is_module_loaded(module):
1449 """Checks if a kernel module is already loaded"""
1450 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
1451 return len(matches) > 0
1452-
1453-
1454-def update_initramfs(version='all'):
1455- """Updates an initramfs image"""
1456- return check_call(["update-initramfs", "-k", version, "-u"])
1457diff --git a/hooks/charmhelpers/core/kernel_factory/__init__.py b/hooks/charmhelpers/core/kernel_factory/__init__.py
1458new file mode 100644
1459index 0000000..e69de29
1460--- /dev/null
1461+++ b/hooks/charmhelpers/core/kernel_factory/__init__.py
1462diff --git a/hooks/charmhelpers/core/kernel_factory/centos.py b/hooks/charmhelpers/core/kernel_factory/centos.py
1463new file mode 100644
1464index 0000000..1c402c1
1465--- /dev/null
1466+++ b/hooks/charmhelpers/core/kernel_factory/centos.py
1467@@ -0,0 +1,17 @@
1468+import subprocess
1469+import os
1470+
1471+
1472+def persistent_modprobe(module):
1473+ """Load a kernel module and configure for auto-load on reboot."""
1474+ if not os.path.exists('/etc/rc.modules'):
1475+ open('/etc/rc.modules', 'a')
1476+ os.chmod('/etc/rc.modules', 111)
1477+ with open('/etc/rc.modules', 'r+') as modules:
1478+ if module not in modules.read():
1479+ modules.write('modprobe %s\n' % module)
1480+
1481+
1482+def update_initramfs(version='all'):
1483+ """Updates an initramfs image."""
1484+ return subprocess.check_call(["dracut", "-f", version])
1485diff --git a/hooks/charmhelpers/core/kernel_factory/ubuntu.py b/hooks/charmhelpers/core/kernel_factory/ubuntu.py
1486new file mode 100644
1487index 0000000..2155964
1488--- /dev/null
1489+++ b/hooks/charmhelpers/core/kernel_factory/ubuntu.py
1490@@ -0,0 +1,13 @@
1491+import subprocess
1492+
1493+
1494+def persistent_modprobe(module):
1495+ """Load a kernel module and configure for auto-load on reboot."""
1496+ with open('/etc/modules', 'r+') as modules:
1497+ if module not in modules.read():
1498+ modules.write(module)
1499+
1500+
1501+def update_initramfs(version='all'):
1502+ """Updates an initramfs image."""
1503+ return subprocess.check_call(["update-initramfs", "-k", version, "-u"])
1504diff --git a/hooks/charmhelpers/core/services/__init__.py b/hooks/charmhelpers/core/services/__init__.py
1505index 0928158..61fd074 100644
1506--- a/hooks/charmhelpers/core/services/__init__.py
1507+++ b/hooks/charmhelpers/core/services/__init__.py
1508@@ -1,18 +1,16 @@
1509 # Copyright 2014-2015 Canonical Limited.
1510 #
1511-# This file is part of charm-helpers.
1512+# Licensed under the Apache License, Version 2.0 (the "License");
1513+# you may not use this file except in compliance with the License.
1514+# You may obtain a copy of the License at
1515 #
1516-# charm-helpers is free software: you can redistribute it and/or modify
1517-# it under the terms of the GNU Lesser General Public License version 3 as
1518-# published by the Free Software Foundation.
1519+# http://www.apache.org/licenses/LICENSE-2.0
1520 #
1521-# charm-helpers is distributed in the hope that it will be useful,
1522-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1523-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1524-# GNU Lesser General Public License for more details.
1525-#
1526-# You should have received a copy of the GNU Lesser General Public License
1527-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1528+# Unless required by applicable law or agreed to in writing, software
1529+# distributed under the License is distributed on an "AS IS" BASIS,
1530+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1531+# See the License for the specific language governing permissions and
1532+# limitations under the License.
1533
1534 from .base import * # NOQA
1535 from .helpers import * # NOQA
1536diff --git a/hooks/charmhelpers/core/services/base.py b/hooks/charmhelpers/core/services/base.py
1537index a42660c..ca9dc99 100644
1538--- a/hooks/charmhelpers/core/services/base.py
1539+++ b/hooks/charmhelpers/core/services/base.py
1540@@ -1,18 +1,16 @@
1541 # Copyright 2014-2015 Canonical Limited.
1542 #
1543-# This file is part of charm-helpers.
1544+# Licensed under the Apache License, Version 2.0 (the "License");
1545+# you may not use this file except in compliance with the License.
1546+# You may obtain a copy of the License at
1547 #
1548-# charm-helpers is free software: you can redistribute it and/or modify
1549-# it under the terms of the GNU Lesser General Public License version 3 as
1550-# published by the Free Software Foundation.
1551+# http://www.apache.org/licenses/LICENSE-2.0
1552 #
1553-# charm-helpers is distributed in the hope that it will be useful,
1554-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1555-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1556-# GNU Lesser General Public License for more details.
1557-#
1558-# You should have received a copy of the GNU Lesser General Public License
1559-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1560+# Unless required by applicable law or agreed to in writing, software
1561+# distributed under the License is distributed on an "AS IS" BASIS,
1562+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1563+# See the License for the specific language governing permissions and
1564+# limitations under the License.
1565
1566 import os
1567 import json
1568diff --git a/hooks/charmhelpers/core/services/helpers.py b/hooks/charmhelpers/core/services/helpers.py
1569index 12d768e..3e6e30d 100644
1570--- a/hooks/charmhelpers/core/services/helpers.py
1571+++ b/hooks/charmhelpers/core/services/helpers.py
1572@@ -1,18 +1,16 @@
1573 # Copyright 2014-2015 Canonical Limited.
1574 #
1575-# This file is part of charm-helpers.
1576+# Licensed under the Apache License, Version 2.0 (the "License");
1577+# you may not use this file except in compliance with the License.
1578+# You may obtain a copy of the License at
1579 #
1580-# charm-helpers is free software: you can redistribute it and/or modify
1581-# it under the terms of the GNU Lesser General Public License version 3 as
1582-# published by the Free Software Foundation.
1583+# http://www.apache.org/licenses/LICENSE-2.0
1584 #
1585-# charm-helpers is distributed in the hope that it will be useful,
1586-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1587-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1588-# GNU Lesser General Public License for more details.
1589-#
1590-# You should have received a copy of the GNU Lesser General Public License
1591-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1592+# Unless required by applicable law or agreed to in writing, software
1593+# distributed under the License is distributed on an "AS IS" BASIS,
1594+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1595+# See the License for the specific language governing permissions and
1596+# limitations under the License.
1597
1598 import os
1599 import yaml
1600@@ -243,13 +241,15 @@ class TemplateCallback(ManagerCallback):
1601 :param str source: The template source file, relative to
1602 `$CHARM_DIR/templates`
1603
1604- :param str target: The target to write the rendered template to
1605+ :param str target: The target to write the rendered template to (or None)
1606 :param str owner: The owner of the rendered file
1607 :param str group: The group of the rendered file
1608 :param int perms: The permissions of the rendered file
1609 :param partial on_change_action: functools partial to be executed when
1610 rendered file changes
1611 :param jinja2 loader template_loader: A jinja2 template loader
1612+
1613+ :return str: The rendered template
1614 """
1615 def __init__(self, source, target,
1616 owner='root', group='root', perms=0o444,
1617@@ -267,12 +267,14 @@ class TemplateCallback(ManagerCallback):
1618 if self.on_change_action and os.path.isfile(self.target):
1619 pre_checksum = host.file_hash(self.target)
1620 service = manager.get_service(service_name)
1621- context = {}
1622+ context = {'ctx': {}}
1623 for ctx in service.get('required_data', []):
1624 context.update(ctx)
1625- templating.render(self.source, self.target, context,
1626- self.owner, self.group, self.perms,
1627- template_loader=self.template_loader)
1628+ context['ctx'].update(ctx)
1629+
1630+ result = templating.render(self.source, self.target, context,
1631+ self.owner, self.group, self.perms,
1632+ template_loader=self.template_loader)
1633 if self.on_change_action:
1634 if pre_checksum == host.file_hash(self.target):
1635 hookenv.log(
1636@@ -281,6 +283,8 @@ class TemplateCallback(ManagerCallback):
1637 else:
1638 self.on_change_action()
1639
1640+ return result
1641+
1642
1643 # Convenience aliases for templates
1644 render_template = template = TemplateCallback
1645diff --git a/hooks/charmhelpers/core/strutils.py b/hooks/charmhelpers/core/strutils.py
1646index 7e3f969..dd9b971 100644
1647--- a/hooks/charmhelpers/core/strutils.py
1648+++ b/hooks/charmhelpers/core/strutils.py
1649@@ -3,19 +3,17 @@
1650
1651 # Copyright 2014-2015 Canonical Limited.
1652 #
1653-# This file is part of charm-helpers.
1654+# Licensed under the Apache License, Version 2.0 (the "License");
1655+# you may not use this file except in compliance with the License.
1656+# You may obtain a copy of the License at
1657 #
1658-# charm-helpers is free software: you can redistribute it and/or modify
1659-# it under the terms of the GNU Lesser General Public License version 3 as
1660-# published by the Free Software Foundation.
1661+# http://www.apache.org/licenses/LICENSE-2.0
1662 #
1663-# charm-helpers is distributed in the hope that it will be useful,
1664-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1665-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1666-# GNU Lesser General Public License for more details.
1667-#
1668-# You should have received a copy of the GNU Lesser General Public License
1669-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1670+# Unless required by applicable law or agreed to in writing, software
1671+# distributed under the License is distributed on an "AS IS" BASIS,
1672+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1673+# See the License for the specific language governing permissions and
1674+# limitations under the License.
1675
1676 import six
1677 import re
1678diff --git a/hooks/charmhelpers/core/sysctl.py b/hooks/charmhelpers/core/sysctl.py
1679index 21cc8ab..6e413e3 100644
1680--- a/hooks/charmhelpers/core/sysctl.py
1681+++ b/hooks/charmhelpers/core/sysctl.py
1682@@ -3,19 +3,17 @@
1683
1684 # Copyright 2014-2015 Canonical Limited.
1685 #
1686-# This file is part of charm-helpers.
1687+# Licensed under the Apache License, Version 2.0 (the "License");
1688+# you may not use this file except in compliance with the License.
1689+# You may obtain a copy of the License at
1690 #
1691-# charm-helpers is free software: you can redistribute it and/or modify
1692-# it under the terms of the GNU Lesser General Public License version 3 as
1693-# published by the Free Software Foundation.
1694+# http://www.apache.org/licenses/LICENSE-2.0
1695 #
1696-# charm-helpers is distributed in the hope that it will be useful,
1697-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1698-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1699-# GNU Lesser General Public License for more details.
1700-#
1701-# You should have received a copy of the GNU Lesser General Public License
1702-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1703+# Unless required by applicable law or agreed to in writing, software
1704+# distributed under the License is distributed on an "AS IS" BASIS,
1705+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1706+# See the License for the specific language governing permissions and
1707+# limitations under the License.
1708
1709 import yaml
1710
1711diff --git a/hooks/charmhelpers/core/templating.py b/hooks/charmhelpers/core/templating.py
1712index 239719d..7b801a3 100644
1713--- a/hooks/charmhelpers/core/templating.py
1714+++ b/hooks/charmhelpers/core/templating.py
1715@@ -1,20 +1,19 @@
1716 # Copyright 2014-2015 Canonical Limited.
1717 #
1718-# This file is part of charm-helpers.
1719+# Licensed under the Apache License, Version 2.0 (the "License");
1720+# you may not use this file except in compliance with the License.
1721+# You may obtain a copy of the License at
1722 #
1723-# charm-helpers is free software: you can redistribute it and/or modify
1724-# it under the terms of the GNU Lesser General Public License version 3 as
1725-# published by the Free Software Foundation.
1726+# http://www.apache.org/licenses/LICENSE-2.0
1727 #
1728-# charm-helpers is distributed in the hope that it will be useful,
1729-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1730-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1731-# GNU Lesser General Public License for more details.
1732-#
1733-# You should have received a copy of the GNU Lesser General Public License
1734-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1735+# Unless required by applicable law or agreed to in writing, software
1736+# distributed under the License is distributed on an "AS IS" BASIS,
1737+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1738+# See the License for the specific language governing permissions and
1739+# limitations under the License.
1740
1741 import os
1742+import sys
1743
1744 from charmhelpers.core import host
1745 from charmhelpers.core import hookenv
1746@@ -27,7 +26,8 @@ def render(source, target, context, owner='root', group='root',
1747
1748 The `source` path, if not absolute, is relative to the `templates_dir`.
1749
1750- The `target` path should be absolute.
1751+ The `target` path should be absolute. It can also be `None`, in which
1752+ case no file will be written.
1753
1754 The context should be a dict containing the values to be replaced in the
1755 template.
1756@@ -36,8 +36,12 @@ def render(source, target, context, owner='root', group='root',
1757
1758 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
1759
1760- Note: Using this requires python-jinja2; if it is not installed, calling
1761- this will attempt to use charmhelpers.fetch.apt_install to install it.
1762+ The rendered template will be written to the file as well as being returned
1763+ as a string.
1764+
1765+ Note: Using this requires python-jinja2 or python3-jinja2; if it is not
1766+ installed, calling this will attempt to use charmhelpers.fetch.apt_install
1767+ to install it.
1768 """
1769 try:
1770 from jinja2 import FileSystemLoader, Environment, exceptions
1771@@ -49,7 +53,10 @@ def render(source, target, context, owner='root', group='root',
1772 'charmhelpers.fetch to install it',
1773 level=hookenv.ERROR)
1774 raise
1775- apt_install('python-jinja2', fatal=True)
1776+ if sys.version_info.major == 2:
1777+ apt_install('python-jinja2', fatal=True)
1778+ else:
1779+ apt_install('python3-jinja2', fatal=True)
1780 from jinja2 import FileSystemLoader, Environment, exceptions
1781
1782 if template_loader:
1783@@ -67,9 +74,11 @@ def render(source, target, context, owner='root', group='root',
1784 level=hookenv.ERROR)
1785 raise e
1786 content = template.render(context)
1787- target_dir = os.path.dirname(target)
1788- if not os.path.exists(target_dir):
1789- # This is a terrible default directory permission, as the file
1790- # or its siblings will often contain secrets.
1791- host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1792- host.write_file(target, content.encode(encoding), owner, group, perms)
1793+ if target is not None:
1794+ target_dir = os.path.dirname(target)
1795+ if not os.path.exists(target_dir):
1796+ # This is a terrible default directory permission, as the file
1797+ # or its siblings will often contain secrets.
1798+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1799+ host.write_file(target, content.encode(encoding), owner, group, perms)
1800+ return content
1801diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py
1802index 338104e..54ec969 100644
1803--- a/hooks/charmhelpers/core/unitdata.py
1804+++ b/hooks/charmhelpers/core/unitdata.py
1805@@ -3,20 +3,17 @@
1806 #
1807 # Copyright 2014-2015 Canonical Limited.
1808 #
1809-# This file is part of charm-helpers.
1810+# Licensed under the Apache License, Version 2.0 (the "License");
1811+# you may not use this file except in compliance with the License.
1812+# You may obtain a copy of the License at
1813 #
1814-# charm-helpers is free software: you can redistribute it and/or modify
1815-# it under the terms of the GNU Lesser General Public License version 3 as
1816-# published by the Free Software Foundation.
1817-#
1818-# charm-helpers is distributed in the hope that it will be useful,
1819-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1820-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1821-# GNU Lesser General Public License for more details.
1822-#
1823-# You should have received a copy of the GNU Lesser General Public License
1824-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1825+# http://www.apache.org/licenses/LICENSE-2.0
1826 #
1827+# Unless required by applicable law or agreed to in writing, software
1828+# distributed under the License is distributed on an "AS IS" BASIS,
1829+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1830+# See the License for the specific language governing permissions and
1831+# limitations under the License.
1832 #
1833 # Authors:
1834 # Kapil Thangavelu <kapil.foss@gmail.com>
1835diff --git a/hooks/charmhelpers/fetch/__init__.py b/hooks/charmhelpers/fetch/__init__.py
1836index 5f831c3..ec5e0fe 100644
1837--- a/hooks/charmhelpers/fetch/__init__.py
1838+++ b/hooks/charmhelpers/fetch/__init__.py
1839@@ -1,32 +1,24 @@
1840 # Copyright 2014-2015 Canonical Limited.
1841 #
1842-# This file is part of charm-helpers.
1843+# Licensed under the Apache License, Version 2.0 (the "License");
1844+# you may not use this file except in compliance with the License.
1845+# You may obtain a copy of the License at
1846 #
1847-# charm-helpers is free software: you can redistribute it and/or modify
1848-# it under the terms of the GNU Lesser General Public License version 3 as
1849-# published by the Free Software Foundation.
1850+# http://www.apache.org/licenses/LICENSE-2.0
1851 #
1852-# charm-helpers is distributed in the hope that it will be useful,
1853-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1854-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1855-# GNU Lesser General Public License for more details.
1856-#
1857-# You should have received a copy of the GNU Lesser General Public License
1858-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1859+# Unless required by applicable law or agreed to in writing, software
1860+# distributed under the License is distributed on an "AS IS" BASIS,
1861+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1862+# See the License for the specific language governing permissions and
1863+# limitations under the License.
1864
1865 import importlib
1866-from tempfile import NamedTemporaryFile
1867-import time
1868+from charmhelpers.osplatform import get_platform
1869 from yaml import safe_load
1870-from charmhelpers.core.host import (
1871- lsb_release
1872-)
1873-import subprocess
1874 from charmhelpers.core.hookenv import (
1875 config,
1876 log,
1877 )
1878-import os
1879
1880 import six
1881 if six.PY3:
1882@@ -35,71 +27,6 @@ else:
1883 from urlparse import urlparse, urlunparse
1884
1885
1886-CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
1887-deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
1888-"""
1889-PROPOSED_POCKET = """# Proposed
1890-deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
1891-"""
1892-CLOUD_ARCHIVE_POCKETS = {
1893- # Folsom
1894- 'folsom': 'precise-updates/folsom',
1895- 'precise-folsom': 'precise-updates/folsom',
1896- 'precise-folsom/updates': 'precise-updates/folsom',
1897- 'precise-updates/folsom': 'precise-updates/folsom',
1898- 'folsom/proposed': 'precise-proposed/folsom',
1899- 'precise-folsom/proposed': 'precise-proposed/folsom',
1900- 'precise-proposed/folsom': 'precise-proposed/folsom',
1901- # Grizzly
1902- 'grizzly': 'precise-updates/grizzly',
1903- 'precise-grizzly': 'precise-updates/grizzly',
1904- 'precise-grizzly/updates': 'precise-updates/grizzly',
1905- 'precise-updates/grizzly': 'precise-updates/grizzly',
1906- 'grizzly/proposed': 'precise-proposed/grizzly',
1907- 'precise-grizzly/proposed': 'precise-proposed/grizzly',
1908- 'precise-proposed/grizzly': 'precise-proposed/grizzly',
1909- # Havana
1910- 'havana': 'precise-updates/havana',
1911- 'precise-havana': 'precise-updates/havana',
1912- 'precise-havana/updates': 'precise-updates/havana',
1913- 'precise-updates/havana': 'precise-updates/havana',
1914- 'havana/proposed': 'precise-proposed/havana',
1915- 'precise-havana/proposed': 'precise-proposed/havana',
1916- 'precise-proposed/havana': 'precise-proposed/havana',
1917- # Icehouse
1918- 'icehouse': 'precise-updates/icehouse',
1919- 'precise-icehouse': 'precise-updates/icehouse',
1920- 'precise-icehouse/updates': 'precise-updates/icehouse',
1921- 'precise-updates/icehouse': 'precise-updates/icehouse',
1922- 'icehouse/proposed': 'precise-proposed/icehouse',
1923- 'precise-icehouse/proposed': 'precise-proposed/icehouse',
1924- 'precise-proposed/icehouse': 'precise-proposed/icehouse',
1925- # Juno
1926- 'juno': 'trusty-updates/juno',
1927- 'trusty-juno': 'trusty-updates/juno',
1928- 'trusty-juno/updates': 'trusty-updates/juno',
1929- 'trusty-updates/juno': 'trusty-updates/juno',
1930- 'juno/proposed': 'trusty-proposed/juno',
1931- 'trusty-juno/proposed': 'trusty-proposed/juno',
1932- 'trusty-proposed/juno': 'trusty-proposed/juno',
1933- # Kilo
1934- 'kilo': 'trusty-updates/kilo',
1935- 'trusty-kilo': 'trusty-updates/kilo',
1936- 'trusty-kilo/updates': 'trusty-updates/kilo',
1937- 'trusty-updates/kilo': 'trusty-updates/kilo',
1938- 'kilo/proposed': 'trusty-proposed/kilo',
1939- 'trusty-kilo/proposed': 'trusty-proposed/kilo',
1940- 'trusty-proposed/kilo': 'trusty-proposed/kilo',
1941- # Liberty
1942- 'liberty': 'trusty-updates/liberty',
1943- 'trusty-liberty': 'trusty-updates/liberty',
1944- 'trusty-liberty/updates': 'trusty-updates/liberty',
1945- 'trusty-updates/liberty': 'trusty-updates/liberty',
1946- 'liberty/proposed': 'trusty-proposed/liberty',
1947- 'trusty-liberty/proposed': 'trusty-proposed/liberty',
1948- 'trusty-proposed/liberty': 'trusty-proposed/liberty',
1949-}
1950-
1951 # The order of this list is very important. Handlers should be listed in from
1952 # least- to most-specific URL matching.
1953 FETCH_HANDLERS = (
1954@@ -108,10 +35,6 @@ FETCH_HANDLERS = (
1955 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
1956 )
1957
1958-APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
1959-APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
1960-APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
1961-
1962
1963 class SourceConfigError(Exception):
1964 pass
1965@@ -149,180 +72,38 @@ class BaseFetchHandler(object):
1966 return urlunparse(parts)
1967
1968
1969-def filter_installed_packages(packages):
1970- """Returns a list of packages that require installation"""
1971- cache = apt_cache()
1972- _pkgs = []
1973- for package in packages:
1974- try:
1975- p = cache[package]
1976- p.current_ver or _pkgs.append(package)
1977- except KeyError:
1978- log('Package {} has no installation candidate.'.format(package),
1979- level='WARNING')
1980- _pkgs.append(package)
1981- return _pkgs
1982-
1983-
1984-def apt_cache(in_memory=True):
1985- """Build and return an apt cache"""
1986- from apt import apt_pkg
1987- apt_pkg.init()
1988- if in_memory:
1989- apt_pkg.config.set("Dir::Cache::pkgcache", "")
1990- apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
1991- return apt_pkg.Cache()
1992-
1993-
1994-def apt_install(packages, options=None, fatal=False):
1995- """Install one or more packages"""
1996- if options is None:
1997- options = ['--option=Dpkg::Options::=--force-confold']
1998-
1999- cmd = ['apt-get', '--assume-yes']
2000- cmd.extend(options)
2001- cmd.append('install')
2002- if isinstance(packages, six.string_types):
2003- cmd.append(packages)
2004- else:
2005- cmd.extend(packages)
2006- log("Installing {} with options: {}".format(packages,
2007- options))
2008- _run_apt_command(cmd, fatal)
2009-
2010-
2011-def apt_upgrade(options=None, fatal=False, dist=False):
2012- """Upgrade all packages"""
2013- if options is None:
2014- options = ['--option=Dpkg::Options::=--force-confold']
2015-
2016- cmd = ['apt-get', '--assume-yes']
2017- cmd.extend(options)
2018- if dist:
2019- cmd.append('dist-upgrade')
2020- else:
2021- cmd.append('upgrade')
2022- log("Upgrading with options: {}".format(options))
2023- _run_apt_command(cmd, fatal)
2024-
2025-
2026-def apt_update(fatal=False):
2027- """Update local apt cache"""
2028- cmd = ['apt-get', 'update']
2029- _run_apt_command(cmd, fatal)
2030-
2031-
2032-def apt_purge(packages, fatal=False):
2033- """Purge one or more packages"""
2034- cmd = ['apt-get', '--assume-yes', 'purge']
2035- if isinstance(packages, six.string_types):
2036- cmd.append(packages)
2037- else:
2038- cmd.extend(packages)
2039- log("Purging {}".format(packages))
2040- _run_apt_command(cmd, fatal)
2041-
2042-
2043-def apt_mark(packages, mark, fatal=False):
2044- """Flag one or more packages using apt-mark"""
2045- log("Marking {} as {}".format(packages, mark))
2046- cmd = ['apt-mark', mark]
2047- if isinstance(packages, six.string_types):
2048- cmd.append(packages)
2049- else:
2050- cmd.extend(packages)
2051-
2052- if fatal:
2053- subprocess.check_call(cmd, universal_newlines=True)
2054- else:
2055- subprocess.call(cmd, universal_newlines=True)
2056+__platform__ = get_platform()
2057+module = "charmhelpers.fetch.%s" % __platform__
2058+fetch = importlib.import_module(module)
2059
2060+filter_installed_packages = fetch.filter_installed_packages
2061+install = fetch.install
2062+upgrade = fetch.upgrade
2063+update = fetch.update
2064+purge = fetch.purge
2065+add_source = fetch.add_source
2066
2067-def apt_hold(packages, fatal=False):
2068- return apt_mark(packages, 'hold', fatal=fatal)
2069-
2070-
2071-def apt_unhold(packages, fatal=False):
2072- return apt_mark(packages, 'unhold', fatal=fatal)
2073-
2074-
2075-def add_source(source, key=None):
2076- """Add a package source to this system.
2077-
2078- @param source: a URL or sources.list entry, as supported by
2079- add-apt-repository(1). Examples::
2080-
2081- ppa:charmers/example
2082- deb https://stub:key@private.example.com/ubuntu trusty main
2083-
2084- In addition:
2085- 'proposed:' may be used to enable the standard 'proposed'
2086- pocket for the release.
2087- 'cloud:' may be used to activate official cloud archive pockets,
2088- such as 'cloud:icehouse'
2089- 'distro' may be used as a noop
2090-
2091- @param key: A key to be added to the system's APT keyring and used
2092- to verify the signatures on packages. Ideally, this should be an
2093- ASCII format GPG public key including the block headers. A GPG key
2094- id may also be used, but be aware that only insecure protocols are
2095- available to retrieve the actual public key from a public keyserver
2096- placing your Juju environment at risk. ppa and cloud archive keys
2097- are securely added automtically, so sould not be provided.
2098- """
2099- if source is None:
2100- log('Source is not present. Skipping')
2101- return
2102-
2103- if (source.startswith('ppa:') or
2104- source.startswith('http') or
2105- source.startswith('deb ') or
2106- source.startswith('cloud-archive:')):
2107- subprocess.check_call(['add-apt-repository', '--yes', source])
2108- elif source.startswith('cloud:'):
2109- apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
2110- fatal=True)
2111- pocket = source.split(':')[-1]
2112- if pocket not in CLOUD_ARCHIVE_POCKETS:
2113- raise SourceConfigError(
2114- 'Unsupported cloud: source option %s' %
2115- pocket)
2116- actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
2117- with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
2118- apt.write(CLOUD_ARCHIVE.format(actual_pocket))
2119- elif source == 'proposed':
2120- release = lsb_release()['DISTRIB_CODENAME']
2121- with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
2122- apt.write(PROPOSED_POCKET.format(release))
2123- elif source == 'distro':
2124- pass
2125- else:
2126- log("Unknown source: {!r}".format(source))
2127-
2128- if key:
2129- if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
2130- with NamedTemporaryFile('w+') as key_file:
2131- key_file.write(key)
2132- key_file.flush()
2133- key_file.seek(0)
2134- subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
2135- else:
2136- # Note that hkp: is in no way a secure protocol. Using a
2137- # GPG key id is pointless from a security POV unless you
2138- # absolutely trust your network and DNS.
2139- subprocess.check_call(['apt-key', 'adv', '--keyserver',
2140- 'hkp://keyserver.ubuntu.com:80', '--recv',
2141- key])
2142+if __platform__ == "ubuntu":
2143+ apt_cache = fetch.apt_cache
2144+ apt_install = fetch.install
2145+ apt_update = fetch.update
2146+ apt_upgrade = fetch.upgrade
2147+ apt_purge = fetch.purge
2148+ apt_mark = fetch.apt_mark
2149+ apt_hold = fetch.apt_hold
2150+ apt_unhold = fetch.apt_unhold
2151+ get_upstream_version = fetch.get_upstream_version
2152+elif __platform__ == "centos":
2153+ yum_search = fetch.yum_search
2154
2155
2156 def configure_sources(update=False,
2157 sources_var='install_sources',
2158 keys_var='install_keys'):
2159- """
2160- Configure multiple sources from charm configuration.
2161+ """Configure multiple sources from charm configuration.
2162
2163 The lists are encoded as yaml fragments in the configuration.
2164- The frament needs to be included as a string. Sources and their
2165+ The fragment needs to be included as a string. Sources and their
2166 corresponding keys are of the types supported by add_source().
2167
2168 Example config:
2169@@ -354,12 +135,11 @@ def configure_sources(update=False,
2170 for source, key in zip(sources, keys):
2171 add_source(source, key)
2172 if update:
2173- apt_update(fatal=True)
2174+ fetch.update(fatal=True)
2175
2176
2177 def install_remote(source, *args, **kwargs):
2178- """
2179- Install a file tree from a remote source
2180+ """Install a file tree from a remote source.
2181
2182 The specified source should be a url of the form:
2183 scheme://[host]/path[#[option=value][&...]]
2184@@ -382,19 +162,17 @@ def install_remote(source, *args, **kwargs):
2185 # We ONLY check for True here because can_handle may return a string
2186 # explaining why it can't handle a given source.
2187 handlers = [h for h in plugins() if h.can_handle(source) is True]
2188- installed_to = None
2189 for handler in handlers:
2190 try:
2191- installed_to = handler.install(source, *args, **kwargs)
2192+ return handler.install(source, *args, **kwargs)
2193 except UnhandledSource as e:
2194 log('Install source attempt unsuccessful: {}'.format(e),
2195 level='WARNING')
2196- if not installed_to:
2197- raise UnhandledSource("No handler found for source {}".format(source))
2198- return installed_to
2199+ raise UnhandledSource("No handler found for source {}".format(source))
2200
2201
2202 def install_from_config(config_var_name):
2203+ """Install a file from config."""
2204 charm_config = config()
2205 source = charm_config[config_var_name]
2206 return install_remote(source)
2207@@ -411,46 +189,9 @@ def plugins(fetch_handlers=None):
2208 importlib.import_module(package),
2209 classname)
2210 plugin_list.append(handler_class())
2211- except (ImportError, AttributeError):
2212+ except NotImplementedError:
2213 # Skip missing plugins so that they can be ommitted from
2214 # installation if desired
2215 log("FetchHandler {} not found, skipping plugin".format(
2216 handler_name))
2217 return plugin_list
2218-
2219-
2220-def _run_apt_command(cmd, fatal=False):
2221- """
2222- Run an APT command, checking output and retrying if the fatal flag is set
2223- to True.
2224-
2225- :param: cmd: str: The apt command to run.
2226- :param: fatal: bool: Whether the command's output should be checked and
2227- retried.
2228- """
2229- env = os.environ.copy()
2230-
2231- if 'DEBIAN_FRONTEND' not in env:
2232- env['DEBIAN_FRONTEND'] = 'noninteractive'
2233-
2234- if fatal:
2235- retry_count = 0
2236- result = None
2237-
2238- # If the command is considered "fatal", we need to retry if the apt
2239- # lock was not acquired.
2240-
2241- while result is None or result == APT_NO_LOCK:
2242- try:
2243- result = subprocess.check_call(cmd, env=env)
2244- except subprocess.CalledProcessError as e:
2245- retry_count = retry_count + 1
2246- if retry_count > APT_NO_LOCK_RETRY_COUNT:
2247- raise
2248- result = e.returncode
2249- log("Couldn't acquire DPKG lock. Will retry in {} seconds."
2250- "".format(APT_NO_LOCK_RETRY_DELAY))
2251- time.sleep(APT_NO_LOCK_RETRY_DELAY)
2252-
2253- else:
2254- subprocess.call(cmd, env=env)
2255diff --git a/hooks/charmhelpers/fetch/archiveurl.py b/hooks/charmhelpers/fetch/archiveurl.py
2256index efd7f9f..dd24f9e 100644
2257--- a/hooks/charmhelpers/fetch/archiveurl.py
2258+++ b/hooks/charmhelpers/fetch/archiveurl.py
2259@@ -1,18 +1,16 @@
2260 # Copyright 2014-2015 Canonical Limited.
2261 #
2262-# This file is part of charm-helpers.
2263+# Licensed under the Apache License, Version 2.0 (the "License");
2264+# you may not use this file except in compliance with the License.
2265+# You may obtain a copy of the License at
2266 #
2267-# charm-helpers is free software: you can redistribute it and/or modify
2268-# it under the terms of the GNU Lesser General Public License version 3 as
2269-# published by the Free Software Foundation.
2270+# http://www.apache.org/licenses/LICENSE-2.0
2271 #
2272-# charm-helpers is distributed in the hope that it will be useful,
2273-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2274-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2275-# GNU Lesser General Public License for more details.
2276-#
2277-# You should have received a copy of the GNU Lesser General Public License
2278-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2279+# Unless required by applicable law or agreed to in writing, software
2280+# distributed under the License is distributed on an "AS IS" BASIS,
2281+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2282+# See the License for the specific language governing permissions and
2283+# limitations under the License.
2284
2285 import os
2286 import hashlib
2287@@ -108,7 +106,7 @@ class ArchiveUrlFetchHandler(BaseFetchHandler):
2288 install_opener(opener)
2289 response = urlopen(source)
2290 try:
2291- with open(dest, 'w') as dest_file:
2292+ with open(dest, 'wb') as dest_file:
2293 dest_file.write(response.read())
2294 except Exception as e:
2295 if os.path.isfile(dest):
2296diff --git a/hooks/charmhelpers/fetch/bzrurl.py b/hooks/charmhelpers/fetch/bzrurl.py
2297index 3531315..07cd029 100644
2298--- a/hooks/charmhelpers/fetch/bzrurl.py
2299+++ b/hooks/charmhelpers/fetch/bzrurl.py
2300@@ -1,78 +1,76 @@
2301 # Copyright 2014-2015 Canonical Limited.
2302 #
2303-# This file is part of charm-helpers.
2304+# Licensed under the Apache License, Version 2.0 (the "License");
2305+# you may not use this file except in compliance with the License.
2306+# You may obtain a copy of the License at
2307 #
2308-# charm-helpers is free software: you can redistribute it and/or modify
2309-# it under the terms of the GNU Lesser General Public License version 3 as
2310-# published by the Free Software Foundation.
2311+# http://www.apache.org/licenses/LICENSE-2.0
2312 #
2313-# charm-helpers is distributed in the hope that it will be useful,
2314-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2315-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2316-# GNU Lesser General Public License for more details.
2317-#
2318-# You should have received a copy of the GNU Lesser General Public License
2319-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2320+# Unless required by applicable law or agreed to in writing, software
2321+# distributed under the License is distributed on an "AS IS" BASIS,
2322+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2323+# See the License for the specific language governing permissions and
2324+# limitations under the License.
2325
2326 import os
2327+from subprocess import check_call
2328 from charmhelpers.fetch import (
2329 BaseFetchHandler,
2330- UnhandledSource
2331+ UnhandledSource,
2332+ filter_installed_packages,
2333+ install,
2334 )
2335 from charmhelpers.core.host import mkdir
2336
2337-import six
2338-if six.PY3:
2339- raise ImportError('bzrlib does not support Python3')
2340
2341-try:
2342- from bzrlib.branch import Branch
2343- from bzrlib import bzrdir, workingtree, errors
2344-except ImportError:
2345- from charmhelpers.fetch import apt_install
2346- apt_install("python-bzrlib")
2347- from bzrlib.branch import Branch
2348- from bzrlib import bzrdir, workingtree, errors
2349+if filter_installed_packages(['bzr']) != []:
2350+ install(['bzr'])
2351+ if filter_installed_packages(['bzr']) != []:
2352+ raise NotImplementedError('Unable to install bzr')
2353
2354
2355 class BzrUrlFetchHandler(BaseFetchHandler):
2356- """Handler for bazaar branches via generic and lp URLs"""
2357+ """Handler for bazaar branches via generic and lp URLs."""
2358+
2359 def can_handle(self, source):
2360 url_parts = self.parse_url(source)
2361- if url_parts.scheme not in ('bzr+ssh', 'lp'):
2362+ if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
2363 return False
2364+ elif not url_parts.scheme:
2365+ return os.path.exists(os.path.join(source, '.bzr'))
2366 else:
2367 return True
2368
2369- def branch(self, source, dest):
2370- url_parts = self.parse_url(source)
2371- # If we use lp:branchname scheme we need to load plugins
2372+ def branch(self, source, dest, revno=None):
2373 if not self.can_handle(source):
2374 raise UnhandledSource("Cannot handle {}".format(source))
2375- if url_parts.scheme == "lp":
2376- from bzrlib.plugin import load_plugins
2377- load_plugins()
2378- try:
2379- local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
2380- except errors.AlreadyControlDirError:
2381- local_branch = Branch.open(dest)
2382- try:
2383- remote_branch = Branch.open(source)
2384- remote_branch.push(local_branch)
2385- tree = workingtree.WorkingTree.open(dest)
2386- tree.update()
2387- except Exception as e:
2388- raise e
2389+ cmd_opts = []
2390+ if revno:
2391+ cmd_opts += ['-r', str(revno)]
2392+ if os.path.exists(dest):
2393+ cmd = ['bzr', 'pull']
2394+ cmd += cmd_opts
2395+ cmd += ['--overwrite', '-d', dest, source]
2396+ else:
2397+ cmd = ['bzr', 'branch']
2398+ cmd += cmd_opts
2399+ cmd += [source, dest]
2400+ check_call(cmd)
2401
2402- def install(self, source):
2403+ def install(self, source, dest=None, revno=None):
2404 url_parts = self.parse_url(source)
2405 branch_name = url_parts.path.strip("/").split("/")[-1]
2406- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2407- branch_name)
2408- if not os.path.exists(dest_dir):
2409- mkdir(dest_dir, perms=0o755)
2410+ if dest:
2411+ dest_dir = os.path.join(dest, branch_name)
2412+ else:
2413+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2414+ branch_name)
2415+
2416+ if dest and not os.path.exists(dest):
2417+ mkdir(dest, perms=0o755)
2418+
2419 try:
2420- self.branch(source, dest_dir)
2421+ self.branch(source, dest_dir, revno)
2422 except OSError as e:
2423 raise UnhandledSource(e.strerror)
2424 return dest_dir
2425diff --git a/hooks/charmhelpers/fetch/centos.py b/hooks/charmhelpers/fetch/centos.py
2426new file mode 100644
2427index 0000000..604bbfb
2428--- /dev/null
2429+++ b/hooks/charmhelpers/fetch/centos.py
2430@@ -0,0 +1,171 @@
2431+# Copyright 2014-2015 Canonical Limited.
2432+#
2433+# Licensed under the Apache License, Version 2.0 (the "License");
2434+# you may not use this file except in compliance with the License.
2435+# You may obtain a copy of the License at
2436+#
2437+# http://www.apache.org/licenses/LICENSE-2.0
2438+#
2439+# Unless required by applicable law or agreed to in writing, software
2440+# distributed under the License is distributed on an "AS IS" BASIS,
2441+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2442+# See the License for the specific language governing permissions and
2443+# limitations under the License.
2444+
2445+import subprocess
2446+import os
2447+import time
2448+import six
2449+import yum
2450+
2451+from tempfile import NamedTemporaryFile
2452+from charmhelpers.core.hookenv import log
2453+
2454+YUM_NO_LOCK = 1 # The return code for "couldn't acquire lock" in YUM.
2455+YUM_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
2456+YUM_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
2457+
2458+
2459+def filter_installed_packages(packages):
2460+ """Return a list of packages that require installation."""
2461+ yb = yum.YumBase()
2462+ package_list = yb.doPackageLists()
2463+ temp_cache = {p.base_package_name: 1 for p in package_list['installed']}
2464+
2465+ _pkgs = [p for p in packages if not temp_cache.get(p, False)]
2466+ return _pkgs
2467+
2468+
2469+def install(packages, options=None, fatal=False):
2470+ """Install one or more packages."""
2471+ cmd = ['yum', '--assumeyes']
2472+ if options is not None:
2473+ cmd.extend(options)
2474+ cmd.append('install')
2475+ if isinstance(packages, six.string_types):
2476+ cmd.append(packages)
2477+ else:
2478+ cmd.extend(packages)
2479+ log("Installing {} with options: {}".format(packages,
2480+ options))
2481+ _run_yum_command(cmd, fatal)
2482+
2483+
2484+def upgrade(options=None, fatal=False, dist=False):
2485+ """Upgrade all packages."""
2486+ cmd = ['yum', '--assumeyes']
2487+ if options is not None:
2488+ cmd.extend(options)
2489+ cmd.append('upgrade')
2490+ log("Upgrading with options: {}".format(options))
2491+ _run_yum_command(cmd, fatal)
2492+
2493+
2494+def update(fatal=False):
2495+ """Update local yum cache."""
2496+ cmd = ['yum', '--assumeyes', 'update']
2497+ log("Update with fatal: {}".format(fatal))
2498+ _run_yum_command(cmd, fatal)
2499+
2500+
2501+def purge(packages, fatal=False):
2502+ """Purge one or more packages."""
2503+ cmd = ['yum', '--assumeyes', 'remove']
2504+ if isinstance(packages, six.string_types):
2505+ cmd.append(packages)
2506+ else:
2507+ cmd.extend(packages)
2508+ log("Purging {}".format(packages))
2509+ _run_yum_command(cmd, fatal)
2510+
2511+
2512+def yum_search(packages):
2513+ """Search for a package."""
2514+ output = {}
2515+ cmd = ['yum', 'search']
2516+ if isinstance(packages, six.string_types):
2517+ cmd.append(packages)
2518+ else:
2519+ cmd.extend(packages)
2520+ log("Searching for {}".format(packages))
2521+ result = subprocess.check_output(cmd)
2522+ for package in list(packages):
2523+ output[package] = package in result
2524+ return output
2525+
2526+
2527+def add_source(source, key=None):
2528+ """Add a package source to this system.
2529+
2530+ @param source: a URL with a rpm package
2531+
2532+ @param key: A key to be added to the system's keyring and used
2533+ to verify the signatures on packages. Ideally, this should be an
2534+ ASCII format GPG public key including the block headers. A GPG key
2535+ id may also be used, but be aware that only insecure protocols are
2536+ available to retrieve the actual public key from a public keyserver
2537+ placing your Juju environment at risk.
2538+ """
2539+ if source is None:
2540+ log('Source is not present. Skipping')
2541+ return
2542+
2543+ if source.startswith('http'):
2544+ directory = '/etc/yum.repos.d/'
2545+ for filename in os.listdir(directory):
2546+ with open(directory + filename, 'r') as rpm_file:
2547+ if source in rpm_file.read():
2548+ break
2549+ else:
2550+ log("Add source: {!r}".format(source))
2551+ # write in the charms.repo
2552+ with open(directory + 'Charms.repo', 'a') as rpm_file:
2553+ rpm_file.write('[%s]\n' % source[7:].replace('/', '_'))
2554+ rpm_file.write('name=%s\n' % source[7:])
2555+ rpm_file.write('baseurl=%s\n\n' % source)
2556+ else:
2557+ log("Unknown source: {!r}".format(source))
2558+
2559+ if key:
2560+ if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
2561+ with NamedTemporaryFile('w+') as key_file:
2562+ key_file.write(key)
2563+ key_file.flush()
2564+ key_file.seek(0)
2565+ subprocess.check_call(['rpm', '--import', key_file])
2566+ else:
2567+ subprocess.check_call(['rpm', '--import', key])
2568+
2569+
2570+def _run_yum_command(cmd, fatal=False):
2571+ """Run an YUM command.
2572+
2573+ Checks the output and retry if the fatal flag is set to True.
2574+
2575+ :param: cmd: str: The yum command to run.
2576+ :param: fatal: bool: Whether the command's output should be checked and
2577+ retried.
2578+ """
2579+ env = os.environ.copy()
2580+
2581+ if fatal:
2582+ retry_count = 0
2583+ result = None
2584+
2585+ # If the command is considered "fatal", we need to retry if the yum
2586+ # lock was not acquired.
2587+
2588+ while result is None or result == YUM_NO_LOCK:
2589+ try:
2590+ result = subprocess.check_call(cmd, env=env)
2591+ except subprocess.CalledProcessError as e:
2592+ retry_count = retry_count + 1
2593+ if retry_count > YUM_NO_LOCK_RETRY_COUNT:
2594+ raise
2595+ result = e.returncode
2596+ log("Couldn't acquire YUM lock. Will retry in {} seconds."
2597+ "".format(YUM_NO_LOCK_RETRY_DELAY))
2598+ time.sleep(YUM_NO_LOCK_RETRY_DELAY)
2599+
2600+ else:
2601+ subprocess.call(cmd, env=env)
2602diff --git a/hooks/charmhelpers/fetch/giturl.py b/hooks/charmhelpers/fetch/giturl.py
2603index f023b26..4cf21bc 100644
2604--- a/hooks/charmhelpers/fetch/giturl.py
2605+++ b/hooks/charmhelpers/fetch/giturl.py
2606@@ -1,58 +1,56 @@
2607 # Copyright 2014-2015 Canonical Limited.
2608 #
2609-# This file is part of charm-helpers.
2610+# Licensed under the Apache License, Version 2.0 (the "License");
2611+# you may not use this file except in compliance with the License.
2612+# You may obtain a copy of the License at
2613 #
2614-# charm-helpers is free software: you can redistribute it and/or modify
2615-# it under the terms of the GNU Lesser General Public License version 3 as
2616-# published by the Free Software Foundation.
2617+# http://www.apache.org/licenses/LICENSE-2.0
2618 #
2619-# charm-helpers is distributed in the hope that it will be useful,
2620-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2621-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2622-# GNU Lesser General Public License for more details.
2623-#
2624-# You should have received a copy of the GNU Lesser General Public License
2625-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2626+# Unless required by applicable law or agreed to in writing, software
2627+# distributed under the License is distributed on an "AS IS" BASIS,
2628+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2629+# See the License for the specific language governing permissions and
2630+# limitations under the License.
2631
2632 import os
2633+from subprocess import check_call, CalledProcessError
2634 from charmhelpers.fetch import (
2635 BaseFetchHandler,
2636- UnhandledSource
2637+ UnhandledSource,
2638+ filter_installed_packages,
2639+ install,
2640 )
2641-from charmhelpers.core.host import mkdir
2642-
2643-import six
2644-if six.PY3:
2645- raise ImportError('GitPython does not support Python 3')
2646
2647-try:
2648- from git import Repo
2649-except ImportError:
2650- from charmhelpers.fetch import apt_install
2651- apt_install("python-git")
2652- from git import Repo
2653-
2654-from git.exc import GitCommandError # noqa E402
2655+if filter_installed_packages(['git']) != []:
2656+ install(['git'])
2657+ if filter_installed_packages(['git']) != []:
2658+ raise NotImplementedError('Unable to install git')
2659
2660
2661 class GitUrlFetchHandler(BaseFetchHandler):
2662- """Handler for git branches via generic and github URLs"""
2663+ """Handler for git branches via generic and github URLs."""
2664+
2665 def can_handle(self, source):
2666 url_parts = self.parse_url(source)
2667 # TODO (mattyw) no support for ssh git@ yet
2668- if url_parts.scheme not in ('http', 'https', 'git'):
2669+ if url_parts.scheme not in ('http', 'https', 'git', ''):
2670 return False
2671+ elif not url_parts.scheme:
2672+ return os.path.exists(os.path.join(source, '.git'))
2673 else:
2674 return True
2675
2676- def clone(self, source, dest, branch, depth=None):
2677+ def clone(self, source, dest, branch="master", depth=None):
2678 if not self.can_handle(source):
2679 raise UnhandledSource("Cannot handle {}".format(source))
2680
2681- if depth:
2682- Repo.clone_from(source, dest, branch=branch, depth=depth)
2683+ if os.path.exists(dest):
2684+ cmd = ['git', '-C', dest, 'pull', source, branch]
2685 else:
2686- Repo.clone_from(source, dest, branch=branch)
2687+ cmd = ['git', 'clone', source, dest, '--branch', branch]
2688+ if depth:
2689+ cmd.extend(['--depth', depth])
2690+ check_call(cmd)
2691
2692 def install(self, source, branch="master", dest=None, depth=None):
2693 url_parts = self.parse_url(source)
2694@@ -62,11 +60,9 @@ class GitUrlFetchHandler(BaseFetchHandler):
2695 else:
2696 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2697 branch_name)
2698- if not os.path.exists(dest_dir):
2699- mkdir(dest_dir, perms=0o755)
2700 try:
2701 self.clone(source, dest_dir, branch, depth)
2702- except GitCommandError as e:
2703+ except CalledProcessError as e:
2704 raise UnhandledSource(e)
2705 except OSError as e:
2706 raise UnhandledSource(e.strerror)
2707diff --git a/hooks/charmhelpers/fetch/ubuntu.py b/hooks/charmhelpers/fetch/ubuntu.py
2708new file mode 100644
2709index 0000000..fce496b
2710--- /dev/null
2711+++ b/hooks/charmhelpers/fetch/ubuntu.py
2712@@ -0,0 +1,336 @@
2713+# Copyright 2014-2015 Canonical Limited.
2714+#
2715+# Licensed under the Apache License, Version 2.0 (the "License");
2716+# you may not use this file except in compliance with the License.
2717+# You may obtain a copy of the License at
2718+#
2719+# http://www.apache.org/licenses/LICENSE-2.0
2720+#
2721+# Unless required by applicable law or agreed to in writing, software
2722+# distributed under the License is distributed on an "AS IS" BASIS,
2723+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2724+# See the License for the specific language governing permissions and
2725+# limitations under the License.
2726+
2727+import os
2728+import six
2729+import time
2730+import subprocess
2731+
2732+from tempfile import NamedTemporaryFile
2733+from charmhelpers.core.host import (
2734+ lsb_release
2735+)
2736+from charmhelpers.core.hookenv import log
2737+from charmhelpers.fetch import SourceConfigError
2738+
2739+CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
2740+deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
2741+"""
2742+
2743+PROPOSED_POCKET = """# Proposed
2744+deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
2745+"""
2746+
2747+CLOUD_ARCHIVE_POCKETS = {
2748+ # Folsom
2749+ 'folsom': 'precise-updates/folsom',
2750+ 'precise-folsom': 'precise-updates/folsom',
2751+ 'precise-folsom/updates': 'precise-updates/folsom',
2752+ 'precise-updates/folsom': 'precise-updates/folsom',
2753+ 'folsom/proposed': 'precise-proposed/folsom',
2754+ 'precise-folsom/proposed': 'precise-proposed/folsom',
2755+ 'precise-proposed/folsom': 'precise-proposed/folsom',
2756+ # Grizzly
2757+ 'grizzly': 'precise-updates/grizzly',
2758+ 'precise-grizzly': 'precise-updates/grizzly',
2759+ 'precise-grizzly/updates': 'precise-updates/grizzly',
2760+ 'precise-updates/grizzly': 'precise-updates/grizzly',
2761+ 'grizzly/proposed': 'precise-proposed/grizzly',
2762+ 'precise-grizzly/proposed': 'precise-proposed/grizzly',
2763+ 'precise-proposed/grizzly': 'precise-proposed/grizzly',
2764+ # Havana
2765+ 'havana': 'precise-updates/havana',
2766+ 'precise-havana': 'precise-updates/havana',
2767+ 'precise-havana/updates': 'precise-updates/havana',
2768+ 'precise-updates/havana': 'precise-updates/havana',
2769+ 'havana/proposed': 'precise-proposed/havana',
2770+ 'precise-havana/proposed': 'precise-proposed/havana',
2771+ 'precise-proposed/havana': 'precise-proposed/havana',
2772+ # Icehouse
2773+ 'icehouse': 'precise-updates/icehouse',
2774+ 'precise-icehouse': 'precise-updates/icehouse',
2775+ 'precise-icehouse/updates': 'precise-updates/icehouse',
2776+ 'precise-updates/icehouse': 'precise-updates/icehouse',
2777+ 'icehouse/proposed': 'precise-proposed/icehouse',
2778+ 'precise-icehouse/proposed': 'precise-proposed/icehouse',
2779+ 'precise-proposed/icehouse': 'precise-proposed/icehouse',
2780+ # Juno
2781+ 'juno': 'trusty-updates/juno',
2782+ 'trusty-juno': 'trusty-updates/juno',
2783+ 'trusty-juno/updates': 'trusty-updates/juno',
2784+ 'trusty-updates/juno': 'trusty-updates/juno',
2785+ 'juno/proposed': 'trusty-proposed/juno',
2786+ 'trusty-juno/proposed': 'trusty-proposed/juno',
2787+ 'trusty-proposed/juno': 'trusty-proposed/juno',
2788+ # Kilo
2789+ 'kilo': 'trusty-updates/kilo',
2790+ 'trusty-kilo': 'trusty-updates/kilo',
2791+ 'trusty-kilo/updates': 'trusty-updates/kilo',
2792+ 'trusty-updates/kilo': 'trusty-updates/kilo',
2793+ 'kilo/proposed': 'trusty-proposed/kilo',
2794+ 'trusty-kilo/proposed': 'trusty-proposed/kilo',
2795+ 'trusty-proposed/kilo': 'trusty-proposed/kilo',
2796+ # Liberty
2797+ 'liberty': 'trusty-updates/liberty',
2798+ 'trusty-liberty': 'trusty-updates/liberty',
2799+ 'trusty-liberty/updates': 'trusty-updates/liberty',
2800+ 'trusty-updates/liberty': 'trusty-updates/liberty',
2801+ 'liberty/proposed': 'trusty-proposed/liberty',
2802+ 'trusty-liberty/proposed': 'trusty-proposed/liberty',
2803+ 'trusty-proposed/liberty': 'trusty-proposed/liberty',
2804+ # Mitaka
2805+ 'mitaka': 'trusty-updates/mitaka',
2806+ 'trusty-mitaka': 'trusty-updates/mitaka',
2807+ 'trusty-mitaka/updates': 'trusty-updates/mitaka',
2808+ 'trusty-updates/mitaka': 'trusty-updates/mitaka',
2809+ 'mitaka/proposed': 'trusty-proposed/mitaka',
2810+ 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
2811+ 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
2812+ # Newton
2813+ 'newton': 'xenial-updates/newton',
2814+ 'xenial-newton': 'xenial-updates/newton',
2815+ 'xenial-newton/updates': 'xenial-updates/newton',
2816+ 'xenial-updates/newton': 'xenial-updates/newton',
2817+ 'newton/proposed': 'xenial-proposed/newton',
2818+ 'xenial-newton/proposed': 'xenial-proposed/newton',
2819+ 'xenial-proposed/newton': 'xenial-proposed/newton',
2820+}
2821+
2822+APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
2823+APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
2824+APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
2825+
2826+
2827+def filter_installed_packages(packages):
2828+ """Return a list of packages that require installation."""
2829+ cache = apt_cache()
2830+ _pkgs = []
2831+ for package in packages:
2832+ try:
2833+ p = cache[package]
2834+ p.current_ver or _pkgs.append(package)
2835+ except KeyError:
2836+ log('Package {} has no installation candidate.'.format(package),
2837+ level='WARNING')
2838+ _pkgs.append(package)
2839+ return _pkgs
2840+
2841+
2842+def apt_cache(in_memory=True, progress=None):
2843+ """Build and return an apt cache."""
2844+ from apt import apt_pkg
2845+ apt_pkg.init()
2846+ if in_memory:
2847+ apt_pkg.config.set("Dir::Cache::pkgcache", "")
2848+ apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
2849+ return apt_pkg.Cache(progress)
2850+
2851+
2852+def install(packages, options=None, fatal=False):
2853+ """Install one or more packages."""
2854+ if options is None:
2855+ options = ['--option=Dpkg::Options::=--force-confold']
2856+
2857+ cmd = ['apt-get', '--assume-yes']
2858+ cmd.extend(options)
2859+ cmd.append('install')
2860+ if isinstance(packages, six.string_types):
2861+ cmd.append(packages)
2862+ else:
2863+ cmd.extend(packages)
2864+ log("Installing {} with options: {}".format(packages,
2865+ options))
2866+ _run_apt_command(cmd, fatal)
2867+
2868+
2869+def upgrade(options=None, fatal=False, dist=False):
2870+ """Upgrade all packages."""
2871+ if options is None:
2872+ options = ['--option=Dpkg::Options::=--force-confold']
2873+
2874+ cmd = ['apt-get', '--assume-yes']
2875+ cmd.extend(options)
2876+ if dist:
2877+ cmd.append('dist-upgrade')
2878+ else:
2879+ cmd.append('upgrade')
2880+ log("Upgrading with options: {}".format(options))
2881+ _run_apt_command(cmd, fatal)
2882+
2883+
2884+def update(fatal=False):
2885+ """Update local apt cache."""
2886+ cmd = ['apt-get', 'update']
2887+ _run_apt_command(cmd, fatal)
2888+
2889+
2890+def purge(packages, fatal=False):
2891+ """Purge one or more packages."""
2892+ cmd = ['apt-get', '--assume-yes', 'purge']
2893+ if isinstance(packages, six.string_types):
2894+ cmd.append(packages)
2895+ else:
2896+ cmd.extend(packages)
2897+ log("Purging {}".format(packages))
2898+ _run_apt_command(cmd, fatal)
2899+
2900+
2901+def apt_mark(packages, mark, fatal=False):
2902+ """Flag one or more packages using apt-mark."""
2903+ log("Marking {} as {}".format(packages, mark))
2904+ cmd = ['apt-mark', mark]
2905+ if isinstance(packages, six.string_types):
2906+ cmd.append(packages)
2907+ else:
2908+ cmd.extend(packages)
2909+
2910+ if fatal:
2911+ subprocess.check_call(cmd, universal_newlines=True)
2912+ else:
2913+ subprocess.call(cmd, universal_newlines=True)
2914+
2915+
2916+def apt_hold(packages, fatal=False):
2917+ return apt_mark(packages, 'hold', fatal=fatal)
2918+
2919+
2920+def apt_unhold(packages, fatal=False):
2921+ return apt_mark(packages, 'unhold', fatal=fatal)
2922+
2923+
2924+def add_source(source, key=None):
2925+ """Add a package source to this system.
2926+
2927+ @param source: a URL or sources.list entry, as supported by
2928+ add-apt-repository(1). Examples::
2929+
2930+ ppa:charmers/example
2931+ deb https://stub:key@private.example.com/ubuntu trusty main
2932+
2933+ In addition:
2934+ 'proposed:' may be used to enable the standard 'proposed'
2935+ pocket for the release.
2936+ 'cloud:' may be used to activate official cloud archive pockets,
2937+ such as 'cloud:icehouse'
2938+ 'distro' may be used as a noop
2939+
2940+ @param key: A key to be added to the system's APT keyring and used
2941+ to verify the signatures on packages. Ideally, this should be an
2942+ ASCII format GPG public key including the block headers. A GPG key
2943+ id may also be used, but be aware that only insecure protocols are
2944+ available to retrieve the actual public key from a public keyserver
2945+ placing your Juju environment at risk. ppa and cloud archive keys
2946+ are securely added automtically, so sould not be provided.
2947+ """
2948+ if source is None:
2949+ log('Source is not present. Skipping')
2950+ return
2951+
2952+ if (source.startswith('ppa:') or
2953+ source.startswith('http') or
2954+ source.startswith('deb ') or
2955+ source.startswith('cloud-archive:')):
2956+ subprocess.check_call(['add-apt-repository', '--yes', source])
2957+ elif source.startswith('cloud:'):
2958+ install(filter_installed_packages(['ubuntu-cloud-keyring']),
2959+ fatal=True)
2960+ pocket = source.split(':')[-1]
2961+ if pocket not in CLOUD_ARCHIVE_POCKETS:
2962+ raise SourceConfigError(
2963+ 'Unsupported cloud: source option %s' %
2964+ pocket)
2965+ actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
2966+ with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
2967+ apt.write(CLOUD_ARCHIVE.format(actual_pocket))
2968+ elif source == 'proposed':
2969+ release = lsb_release()['DISTRIB_CODENAME']
2970+ with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
2971+ apt.write(PROPOSED_POCKET.format(release))
2972+ elif source == 'distro':
2973+ pass
2974+ else:
2975+ log("Unknown source: {!r}".format(source))
2976+
2977+ if key:
2978+ if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
2979+ with NamedTemporaryFile('w+') as key_file:
2980+ key_file.write(key)
2981+ key_file.flush()
2982+ key_file.seek(0)
2983+ subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
2984+ else:
2985+ # Note that hkp: is in no way a secure protocol. Using a
2986+ # GPG key id is pointless from a security POV unless you
2987+ # absolutely trust your network and DNS.
2988+ subprocess.check_call(['apt-key', 'adv', '--keyserver',
2989+ 'hkp://keyserver.ubuntu.com:80', '--recv',
2990+ key])
2991+
2992+
2993+def _run_apt_command(cmd, fatal=False):
2994+ """Run an APT command.
2995+
2996+ Checks the output and retries if the fatal flag is set
2997+ to True.
2998+
2999+ :param: cmd: str: The apt command to run.
3000+ :param: fatal: bool: Whether the command's output should be checked and
3001+ retried.
3002+ """
3003+ env = os.environ.copy()
3004+
3005+ if 'DEBIAN_FRONTEND' not in env:
3006+ env['DEBIAN_FRONTEND'] = 'noninteractive'
3007+
3008+ if fatal:
3009+ retry_count = 0
3010+ result = None
3011+
3012+ # If the command is considered "fatal", we need to retry if the apt
3013+ # lock was not acquired.
3014+
3015+ while result is None or result == APT_NO_LOCK:
3016+ try:
3017+ result = subprocess.check_call(cmd, env=env)
3018+ except subprocess.CalledProcessError as e:
3019+ retry_count = retry_count + 1
3020+ if retry_count > APT_NO_LOCK_RETRY_COUNT:
3021+ raise
3022+ result = e.returncode
3023+ log("Couldn't acquire DPKG lock. Will retry in {} seconds."
3024+ "".format(APT_NO_LOCK_RETRY_DELAY))
3025+ time.sleep(APT_NO_LOCK_RETRY_DELAY)
3026+
3027+ else:
3028+ subprocess.call(cmd, env=env)
3029+
3030+
3031+def get_upstream_version(package):
3032+ """Determine upstream version based on installed package
3033+
3034+ @returns None (if not installed) or the upstream version
3035+ """
3036+ import apt_pkg
3037+ cache = apt_cache()
3038+ try:
3039+ pkg = cache[package]
3040+ except:
3041+ # the package is unknown to the current apt cache.
3042+ return None
3043+
3044+ if not pkg.current_ver:
3045+ # package is known, but no version is currently installed.
3046+ return None
3047+
3048+ return apt_pkg.upstream_version(pkg.current_ver.ver_str)
3049diff --git a/hooks/charmhelpers/osplatform.py b/hooks/charmhelpers/osplatform.py
3050new file mode 100644
3051index 0000000..ea490bb
3052--- /dev/null
3053+++ b/hooks/charmhelpers/osplatform.py
3054@@ -0,0 +1,19 @@
3055+import platform
3056+
3057+
3058+def get_platform():
3059+ """Return the current OS platform.
3060+
3061+ For example: if current os platform is Ubuntu then a string "ubuntu"
3062+ will be returned (which is the name of the module).
3063+ This string is used to decide which platform module should be imported.
3064+ """
3065+ tuple_platform = platform.linux_distribution()
3066+ current_platform = tuple_platform[0]
3067+ if "Ubuntu" in current_platform:
3068+ return "ubuntu"
3069+ elif "CentOS" in current_platform:
3070+ return "centos"
3071+ else:
3072+ raise RuntimeError("This module is not supported on {}."
3073+ .format(current_platform))
3074diff --git a/hooks/ntp_hooks.py b/hooks/ntp_hooks.py
3075index 1faa26d..a385c31 100755
3076--- a/hooks/ntp_hooks.py
3077+++ b/hooks/ntp_hooks.py
3078@@ -91,8 +91,6 @@ def write_config():
3079 if hookenv.relation_ids('nrpe-external-master'):
3080 update_nrpe_config()
3081
3082- hookenv.status_set('active', 'ready')
3083-
3084
3085 @hooks.hook('nrpe-external-master-relation-joined',
3086 'nrpe-external-master-relation-changed')
3087@@ -136,8 +134,19 @@ def update_nrpe_config():
3088 nrpe_setup.write()
3089
3090
3091+def assess_status():
3092+ hookenv.application_version_set(
3093+ fetch.get_upstream_version('ntp')
3094+ )
3095+ if host.service_running('ntp'):
3096+ hookenv.status_set('active', 'Unit is ready')
3097+ else:
3098+ hookenv.status_set('blocked', 'ntp not running')
3099+
3100+
3101 if __name__ == '__main__':
3102 try:
3103 hooks.execute(sys.argv)
3104 except UnregisteredHookError as e:
3105 hookenv.log('Unknown hook {} - skipping.'.format(e))
3106+ assess_status()
3107diff --git a/hooks/update-status b/hooks/update-status
3108new file mode 120000
3109index 0000000..ebfa934
3110--- /dev/null
3111+++ b/hooks/update-status
3112@@ -0,0 +1 @@
3113+ntp_hooks.py
3114\ No newline at end of file

Subscribers

People subscribed via source and target branches