Merge lp:~1chb1n/charms/trusty/mysql/newton into lp:charms/trusty/mysql

Proposed by Ryan Beisner on 2016-09-22
Status: Merged
Merged at revision: 162
Proposed branch: lp:~1chb1n/charms/trusty/mysql/newton
Merge into: lp:charms/trusty/mysql
Diff against target: 5281 lines (+3759/-753)
44 files modified
.bzrignore (+1/-0)
charm-helpers.yaml (+1/-0)
hooks/charmhelpers/__init__.py (+11/-13)
hooks/charmhelpers/contrib/__init__.py (+11/-13)
hooks/charmhelpers/contrib/database/__init__.py (+11/-0)
hooks/charmhelpers/contrib/database/mysql.py (+12/-0)
hooks/charmhelpers/contrib/network/__init__.py (+11/-13)
hooks/charmhelpers/contrib/network/ip.py (+59/-20)
hooks/charmhelpers/contrib/peerstorage/__init__.py (+11/-13)
hooks/charmhelpers/core/__init__.py (+11/-13)
hooks/charmhelpers/core/decorators.py (+11/-13)
hooks/charmhelpers/core/files.py (+11/-13)
hooks/charmhelpers/core/fstab.py (+11/-13)
hooks/charmhelpers/core/hookenv.py (+56/-13)
hooks/charmhelpers/core/host.py (+192/-116)
hooks/charmhelpers/core/host_factory/centos.py (+56/-0)
hooks/charmhelpers/core/host_factory/ubuntu.py (+56/-0)
hooks/charmhelpers/core/hugepage.py (+11/-13)
hooks/charmhelpers/core/kernel.py (+34/-30)
hooks/charmhelpers/core/kernel_factory/centos.py (+17/-0)
hooks/charmhelpers/core/kernel_factory/ubuntu.py (+13/-0)
hooks/charmhelpers/core/services/__init__.py (+11/-13)
hooks/charmhelpers/core/services/base.py (+11/-13)
hooks/charmhelpers/core/services/helpers.py (+11/-13)
hooks/charmhelpers/core/strutils.py (+11/-13)
hooks/charmhelpers/core/sysctl.py (+11/-13)
hooks/charmhelpers/core/templating.py (+19/-16)
hooks/charmhelpers/core/unitdata.py (+11/-14)
hooks/charmhelpers/fetch/__init__.py (+42/-309)
hooks/charmhelpers/fetch/archiveurl.py (+11/-13)
hooks/charmhelpers/fetch/bzrurl.py (+31/-23)
hooks/charmhelpers/fetch/centos.py (+171/-0)
hooks/charmhelpers/fetch/giturl.py (+18/-17)
hooks/charmhelpers/fetch/ubuntu.py (+336/-0)
hooks/charmhelpers/osplatform.py (+19/-0)
tests/charmhelpers/__init__.py (+36/-0)
tests/charmhelpers/contrib/__init__.py (+13/-0)
tests/charmhelpers/contrib/amulet/__init__.py (+13/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+97/-0)
tests/charmhelpers/contrib/amulet/utils.py (+827/-0)
tests/charmhelpers/contrib/openstack/__init__.py (+13/-0)
tests/charmhelpers/contrib/openstack/amulet/__init__.py (+13/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+300/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+1127/-0)
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/mysql/newton
Reviewer Review Type Date Requested Status
David Ames 2016-09-22 Approve on 2016-09-23
Review via email: mp+306554@code.launchpad.net

Description of the Change

Sync charm-helpers for Newton awareness

To post a comment you must log in.
162. By Ryan Beisner on 2016-09-22

Re-sync charm-helpers for Newton awareness

charm_lint_check #192 mysql for 1chb1n mp306554
    LINT OK: passed

Build: http://10.245.162.208:8080/job/charm_lint_check/192/

charm_unit_test #109 mysql for 1chb1n mp306554
    UNIT OK: passed

Build: http://10.245.162.208:8080/job/charm_unit_test/109/

David Ames (thedac) wrote :

Ran tests, such as they are, manually as well as a quick deploy with several consumers of the shared-db relation. No regressions found.

Merging.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2014-12-27 04:24:01 +0000
3+++ .bzrignore 2016-09-22 21:57:09 +0000
4@@ -1,2 +1,3 @@
5 bin/
6 .venv
7+.coverage
8
9=== modified file 'charm-helpers.yaml'
10--- charm-helpers.yaml 2015-02-10 11:18:49 +0000
11+++ charm-helpers.yaml 2016-09-22 21:57:09 +0000
12@@ -4,6 +4,7 @@
13 - __init__
14 - core
15 - fetch
16+ - osplatform
17 - contrib.network.ip
18 - contrib.database
19 - contrib.peerstorage
20
21=== modified file 'hooks/charmhelpers/__init__.py'
22--- hooks/charmhelpers/__init__.py 2015-02-04 19:09:09 +0000
23+++ hooks/charmhelpers/__init__.py 2016-09-22 21:57:09 +0000
24@@ -1,18 +1,16 @@
25 # Copyright 2014-2015 Canonical Limited.
26 #
27-# This file is part of charm-helpers.
28-#
29-# charm-helpers is free software: you can redistribute it and/or modify
30-# it under the terms of the GNU Lesser General Public License version 3 as
31-# published by the Free Software Foundation.
32-#
33-# charm-helpers is distributed in the hope that it will be useful,
34-# but WITHOUT ANY WARRANTY; without even the implied warranty of
35-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36-# GNU Lesser General Public License for more details.
37-#
38-# You should have received a copy of the GNU Lesser General Public License
39-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
40+# Licensed under the Apache License, Version 2.0 (the "License");
41+# you may not use this file except in compliance with the License.
42+# You may obtain a copy of the License at
43+#
44+# http://www.apache.org/licenses/LICENSE-2.0
45+#
46+# Unless required by applicable law or agreed to in writing, software
47+# distributed under the License is distributed on an "AS IS" BASIS,
48+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
49+# See the License for the specific language governing permissions and
50+# limitations under the License.
51
52 # Bootstrap charm-helpers, installing its dependencies if necessary using
53 # only standard libraries.
54
55=== modified file 'hooks/charmhelpers/contrib/__init__.py'
56--- hooks/charmhelpers/contrib/__init__.py 2015-02-04 19:09:09 +0000
57+++ hooks/charmhelpers/contrib/__init__.py 2016-09-22 21:57:09 +0000
58@@ -1,15 +1,13 @@
59 # Copyright 2014-2015 Canonical Limited.
60 #
61-# This file is part of charm-helpers.
62-#
63-# charm-helpers is free software: you can redistribute it and/or modify
64-# it under the terms of the GNU Lesser General Public License version 3 as
65-# published by the Free Software Foundation.
66-#
67-# charm-helpers is distributed in the hope that it will be useful,
68-# but WITHOUT ANY WARRANTY; without even the implied warranty of
69-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
70-# GNU Lesser General Public License for more details.
71-#
72-# You should have received a copy of the GNU Lesser General Public License
73-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
74+# Licensed under the Apache License, Version 2.0 (the "License");
75+# you may not use this file except in compliance with the License.
76+# You may obtain a copy of the License at
77+#
78+# http://www.apache.org/licenses/LICENSE-2.0
79+#
80+# Unless required by applicable law or agreed to in writing, software
81+# distributed under the License is distributed on an "AS IS" BASIS,
82+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
83+# See the License for the specific language governing permissions and
84+# limitations under the License.
85
86=== modified file 'hooks/charmhelpers/contrib/database/__init__.py'
87--- hooks/charmhelpers/contrib/database/__init__.py 2015-02-04 19:09:09 +0000
88+++ hooks/charmhelpers/contrib/database/__init__.py 2016-09-22 21:57:09 +0000
89@@ -0,0 +1,11 @@
90+# Licensed under the Apache License, Version 2.0 (the "License");
91+# you may not use this file except in compliance with the License.
92+# You may obtain a copy of the License at
93+#
94+# http://www.apache.org/licenses/LICENSE-2.0
95+#
96+# Unless required by applicable law or agreed to in writing, software
97+# distributed under the License is distributed on an "AS IS" BASIS,
98+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
99+# See the License for the specific language governing permissions and
100+# limitations under the License.
101
102=== modified file 'hooks/charmhelpers/contrib/database/mysql.py'
103--- hooks/charmhelpers/contrib/database/mysql.py 2015-08-19 14:07:56 +0000
104+++ hooks/charmhelpers/contrib/database/mysql.py 2016-09-22 21:57:09 +0000
105@@ -1,3 +1,15 @@
106+# Licensed under the Apache License, Version 2.0 (the "License");
107+# you may not use this file except in compliance with the License.
108+# You may obtain a copy of the License at
109+#
110+# http://www.apache.org/licenses/LICENSE-2.0
111+#
112+# Unless required by applicable law or agreed to in writing, software
113+# distributed under the License is distributed on an "AS IS" BASIS,
114+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
115+# See the License for the specific language governing permissions and
116+# limitations under the License.
117+
118 """Helper for working with a MySQL database"""
119 import json
120 import re
121
122=== modified file 'hooks/charmhelpers/contrib/network/__init__.py'
123--- hooks/charmhelpers/contrib/network/__init__.py 2015-02-04 19:09:09 +0000
124+++ hooks/charmhelpers/contrib/network/__init__.py 2016-09-22 21:57:09 +0000
125@@ -1,15 +1,13 @@
126 # Copyright 2014-2015 Canonical Limited.
127 #
128-# This file is part of charm-helpers.
129-#
130-# charm-helpers is free software: you can redistribute it and/or modify
131-# it under the terms of the GNU Lesser General Public License version 3 as
132-# published by the Free Software Foundation.
133-#
134-# charm-helpers is distributed in the hope that it will be useful,
135-# but WITHOUT ANY WARRANTY; without even the implied warranty of
136-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
137-# GNU Lesser General Public License for more details.
138-#
139-# You should have received a copy of the GNU Lesser General Public License
140-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
141+# Licensed under the Apache License, Version 2.0 (the "License");
142+# you may not use this file except in compliance with the License.
143+# You may obtain a copy of the License at
144+#
145+# http://www.apache.org/licenses/LICENSE-2.0
146+#
147+# Unless required by applicable law or agreed to in writing, software
148+# distributed under the License is distributed on an "AS IS" BASIS,
149+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
150+# See the License for the specific language governing permissions and
151+# limitations under the License.
152
153=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
154--- hooks/charmhelpers/contrib/network/ip.py 2016-01-11 18:11:26 +0000
155+++ hooks/charmhelpers/contrib/network/ip.py 2016-09-22 21:57:09 +0000
156@@ -1,18 +1,16 @@
157 # Copyright 2014-2015 Canonical Limited.
158 #
159-# This file is part of charm-helpers.
160-#
161-# charm-helpers is free software: you can redistribute it and/or modify
162-# it under the terms of the GNU Lesser General Public License version 3 as
163-# published by the Free Software Foundation.
164-#
165-# charm-helpers is distributed in the hope that it will be useful,
166-# but WITHOUT ANY WARRANTY; without even the implied warranty of
167-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
168-# GNU Lesser General Public License for more details.
169-#
170-# You should have received a copy of the GNU Lesser General Public License
171-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
172+# Licensed under the Apache License, Version 2.0 (the "License");
173+# you may not use this file except in compliance with the License.
174+# You may obtain a copy of the License at
175+#
176+# http://www.apache.org/licenses/LICENSE-2.0
177+#
178+# Unless required by applicable law or agreed to in writing, software
179+# distributed under the License is distributed on an "AS IS" BASIS,
180+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
181+# See the License for the specific language governing permissions and
182+# limitations under the License.
183
184 import glob
185 import re
186@@ -191,6 +189,15 @@
187 get_netmask_for_address = partial(_get_for_address, key='netmask')
188
189
190+def resolve_network_cidr(ip_address):
191+ '''
192+ Resolves the full address cidr of an ip_address based on
193+ configured network interfaces
194+ '''
195+ netmask = get_netmask_for_address(ip_address)
196+ return str(netaddr.IPNetwork("%s/%s" % (ip_address, netmask)).cidr)
197+
198+
199 def format_ipv6_addr(address):
200 """If address is IPv6, wrap it in '[]' otherwise return None.
201
202@@ -205,7 +212,16 @@
203
204 def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
205 fatal=True, exc_list=None):
206- """Return the assigned IP address for a given interface, if any."""
207+ """Return the assigned IP address for a given interface, if any.
208+
209+ :param iface: network interface on which address(es) are expected to
210+ be found.
211+ :param inet_type: inet address family
212+ :param inc_aliases: include alias interfaces in search
213+ :param fatal: if True, raise exception if address not found
214+ :param exc_list: list of addresses to ignore
215+ :return: list of ip addresses
216+ """
217 # Extract nic if passed /dev/ethX
218 if '/' in iface:
219 iface = iface.split('/')[-1]
220@@ -306,6 +322,14 @@
221 We currently only support scope global IPv6 addresses i.e. non-temporary
222 addresses. If no global IPv6 address is found, return the first one found
223 in the ipv6 address list.
224+
225+ :param iface: network interface on which ipv6 address(es) are expected to
226+ be found.
227+ :param inc_aliases: include alias interfaces in search
228+ :param fatal: if True, raise exception if address not found
229+ :param exc_list: list of addresses to ignore
230+ :param dynamic_only: only recognise dynamic addresses
231+ :return: list of ipv6 addresses
232 """
233 addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
234 inc_aliases=inc_aliases, fatal=fatal,
235@@ -327,7 +351,7 @@
236 cmd = ['ip', 'addr', 'show', iface]
237 out = subprocess.check_output(cmd).decode('UTF-8')
238 if dynamic_only:
239- key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*")
240+ key = re.compile("inet6 (.+)/[0-9]+ scope global.* dynamic.*")
241 else:
242 key = re.compile("inet6 (.+)/[0-9]+ scope global.*")
243
244@@ -379,10 +403,10 @@
245 Returns True if address is a valid IP address.
246 """
247 try:
248- # Test to see if already an IPv4 address
249- socket.inet_aton(address)
250+ # Test to see if already an IPv4/IPv6 address
251+ address = netaddr.IPAddress(address)
252 return True
253- except socket.error:
254+ except (netaddr.AddrFormatError, ValueError):
255 return False
256
257
258@@ -390,7 +414,7 @@
259 try:
260 import dns.resolver
261 except ImportError:
262- apt_install('python-dnspython')
263+ apt_install('python-dnspython', fatal=True)
264 import dns.resolver
265
266 if isinstance(address, dns.name.Name):
267@@ -434,7 +458,7 @@
268 try:
269 import dns.reversename
270 except ImportError:
271- apt_install("python-dnspython")
272+ apt_install("python-dnspython", fatal=True)
273 import dns.reversename
274
275 rev = dns.reversename.from_address(address)
276@@ -456,3 +480,18 @@
277 return result
278 else:
279 return result.split('.')[0]
280+
281+
282+def port_has_listener(address, port):
283+ """
284+ Returns True if the address:port is open and being listened to,
285+ else False.
286+
287+ @param address: an IP address or hostname
288+ @param port: integer port
289+
290+ Note calls 'zc' via a subprocess shell
291+ """
292+ cmd = ['nc', '-z', address, str(port)]
293+ result = subprocess.call(cmd)
294+ return not(bool(result))
295
296=== modified file 'hooks/charmhelpers/contrib/peerstorage/__init__.py'
297--- hooks/charmhelpers/contrib/peerstorage/__init__.py 2015-08-19 14:07:56 +0000
298+++ hooks/charmhelpers/contrib/peerstorage/__init__.py 2016-09-22 21:57:09 +0000
299@@ -1,18 +1,16 @@
300 # Copyright 2014-2015 Canonical Limited.
301 #
302-# This file is part of charm-helpers.
303-#
304-# charm-helpers is free software: you can redistribute it and/or modify
305-# it under the terms of the GNU Lesser General Public License version 3 as
306-# published by the Free Software Foundation.
307-#
308-# charm-helpers is distributed in the hope that it will be useful,
309-# but WITHOUT ANY WARRANTY; without even the implied warranty of
310-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
311-# GNU Lesser General Public License for more details.
312-#
313-# You should have received a copy of the GNU Lesser General Public License
314-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
315+# Licensed under the Apache License, Version 2.0 (the "License");
316+# you may not use this file except in compliance with the License.
317+# You may obtain a copy of the License at
318+#
319+# http://www.apache.org/licenses/LICENSE-2.0
320+#
321+# Unless required by applicable law or agreed to in writing, software
322+# distributed under the License is distributed on an "AS IS" BASIS,
323+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
324+# See the License for the specific language governing permissions and
325+# limitations under the License.
326
327 import json
328 import six
329
330=== modified file 'hooks/charmhelpers/core/__init__.py'
331--- hooks/charmhelpers/core/__init__.py 2015-02-04 19:09:09 +0000
332+++ hooks/charmhelpers/core/__init__.py 2016-09-22 21:57:09 +0000
333@@ -1,15 +1,13 @@
334 # Copyright 2014-2015 Canonical Limited.
335 #
336-# This file is part of charm-helpers.
337-#
338-# charm-helpers is free software: you can redistribute it and/or modify
339-# it under the terms of the GNU Lesser General Public License version 3 as
340-# published by the Free Software Foundation.
341-#
342-# charm-helpers is distributed in the hope that it will be useful,
343-# but WITHOUT ANY WARRANTY; without even the implied warranty of
344-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
345-# GNU Lesser General Public License for more details.
346-#
347-# You should have received a copy of the GNU Lesser General Public License
348-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
349+# Licensed under the Apache License, Version 2.0 (the "License");
350+# you may not use this file except in compliance with the License.
351+# You may obtain a copy of the License at
352+#
353+# http://www.apache.org/licenses/LICENSE-2.0
354+#
355+# Unless required by applicable law or agreed to in writing, software
356+# distributed under the License is distributed on an "AS IS" BASIS,
357+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
358+# See the License for the specific language governing permissions and
359+# limitations under the License.
360
361=== modified file 'hooks/charmhelpers/core/decorators.py'
362--- hooks/charmhelpers/core/decorators.py 2015-08-19 14:07:56 +0000
363+++ hooks/charmhelpers/core/decorators.py 2016-09-22 21:57:09 +0000
364@@ -1,18 +1,16 @@
365 # Copyright 2014-2015 Canonical Limited.
366 #
367-# This file is part of charm-helpers.
368-#
369-# charm-helpers is free software: you can redistribute it and/or modify
370-# it under the terms of the GNU Lesser General Public License version 3 as
371-# published by the Free Software Foundation.
372-#
373-# charm-helpers is distributed in the hope that it will be useful,
374-# but WITHOUT ANY WARRANTY; without even the implied warranty of
375-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
376-# GNU Lesser General Public License for more details.
377-#
378-# You should have received a copy of the GNU Lesser General Public License
379-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
380+# Licensed under the Apache License, Version 2.0 (the "License");
381+# you may not use this file except in compliance with the License.
382+# You may obtain a copy of the License at
383+#
384+# http://www.apache.org/licenses/LICENSE-2.0
385+#
386+# Unless required by applicable law or agreed to in writing, software
387+# distributed under the License is distributed on an "AS IS" BASIS,
388+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
389+# See the License for the specific language governing permissions and
390+# limitations under the License.
391
392 #
393 # Copyright 2014 Canonical Ltd.
394
395=== modified file 'hooks/charmhelpers/core/files.py'
396--- hooks/charmhelpers/core/files.py 2015-08-19 14:07:56 +0000
397+++ hooks/charmhelpers/core/files.py 2016-09-22 21:57:09 +0000
398@@ -3,19 +3,17 @@
399
400 # Copyright 2014-2015 Canonical Limited.
401 #
402-# This file is part of charm-helpers.
403-#
404-# charm-helpers is free software: you can redistribute it and/or modify
405-# it under the terms of the GNU Lesser General Public License version 3 as
406-# published by the Free Software Foundation.
407-#
408-# charm-helpers is distributed in the hope that it will be useful,
409-# but WITHOUT ANY WARRANTY; without even the implied warranty of
410-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
411-# GNU Lesser General Public License for more details.
412-#
413-# You should have received a copy of the GNU Lesser General Public License
414-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
415+# Licensed under the Apache License, Version 2.0 (the "License");
416+# you may not use this file except in compliance with the License.
417+# You may obtain a copy of the License at
418+#
419+# http://www.apache.org/licenses/LICENSE-2.0
420+#
421+# Unless required by applicable law or agreed to in writing, software
422+# distributed under the License is distributed on an "AS IS" BASIS,
423+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
424+# See the License for the specific language governing permissions and
425+# limitations under the License.
426
427 __author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
428
429
430=== modified file 'hooks/charmhelpers/core/fstab.py'
431--- hooks/charmhelpers/core/fstab.py 2015-02-25 17:10:21 +0000
432+++ hooks/charmhelpers/core/fstab.py 2016-09-22 21:57:09 +0000
433@@ -3,19 +3,17 @@
434
435 # Copyright 2014-2015 Canonical Limited.
436 #
437-# This file is part of charm-helpers.
438-#
439-# charm-helpers is free software: you can redistribute it and/or modify
440-# it under the terms of the GNU Lesser General Public License version 3 as
441-# published by the Free Software Foundation.
442-#
443-# charm-helpers is distributed in the hope that it will be useful,
444-# but WITHOUT ANY WARRANTY; without even the implied warranty of
445-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
446-# GNU Lesser General Public License for more details.
447-#
448-# You should have received a copy of the GNU Lesser General Public License
449-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
450+# Licensed under the Apache License, Version 2.0 (the "License");
451+# you may not use this file except in compliance with the License.
452+# You may obtain a copy of the License at
453+#
454+# http://www.apache.org/licenses/LICENSE-2.0
455+#
456+# Unless required by applicable law or agreed to in writing, software
457+# distributed under the License is distributed on an "AS IS" BASIS,
458+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
459+# See the License for the specific language governing permissions and
460+# limitations under the License.
461
462 import io
463 import os
464
465=== modified file 'hooks/charmhelpers/core/hookenv.py'
466--- hooks/charmhelpers/core/hookenv.py 2016-01-11 18:11:26 +0000
467+++ hooks/charmhelpers/core/hookenv.py 2016-09-22 21:57:09 +0000
468@@ -1,18 +1,16 @@
469 # Copyright 2014-2015 Canonical Limited.
470 #
471-# This file is part of charm-helpers.
472-#
473-# charm-helpers is free software: you can redistribute it and/or modify
474-# it under the terms of the GNU Lesser General Public License version 3 as
475-# published by the Free Software Foundation.
476-#
477-# charm-helpers is distributed in the hope that it will be useful,
478-# but WITHOUT ANY WARRANTY; without even the implied warranty of
479-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
480-# GNU Lesser General Public License for more details.
481-#
482-# You should have received a copy of the GNU Lesser General Public License
483-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
484+# Licensed under the Apache License, Version 2.0 (the "License");
485+# you may not use this file except in compliance with the License.
486+# You may obtain a copy of the License at
487+#
488+# http://www.apache.org/licenses/LICENSE-2.0
489+#
490+# Unless required by applicable law or agreed to in writing, software
491+# distributed under the License is distributed on an "AS IS" BASIS,
492+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
493+# See the License for the specific language governing permissions and
494+# limitations under the License.
495
496 "Interactions with the Juju environment"
497 # Copyright 2013 Canonical Ltd.
498@@ -845,6 +843,20 @@
499 return inner_translate_exc1
500
501
502+def application_version_set(version):
503+ """Charm authors may trigger this command from any hook to output what
504+ version of the application is running. This could be a package version,
505+ for instance postgres version 9.5. It could also be a build number or
506+ version control revision identifier, for instance git sha 6fb7ba68. """
507+
508+ cmd = ['application-version-set']
509+ cmd.append(version)
510+ try:
511+ subprocess.check_call(cmd)
512+ except OSError:
513+ log("Application Version: {}".format(version))
514+
515+
516 @translate_exc(from_exc=OSError, to_exc=NotImplementedError)
517 def is_leader():
518 """Does the current unit hold the juju leadership
519@@ -912,6 +924,24 @@
520 subprocess.check_call(cmd)
521
522
523+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
524+def resource_get(name):
525+ """used to fetch the resource path of the given name.
526+
527+ <name> must match a name of defined resource in metadata.yaml
528+
529+ returns either a path or False if resource not available
530+ """
531+ if not name:
532+ return False
533+
534+ cmd = ['resource-get', name]
535+ try:
536+ return subprocess.check_output(cmd).decode('UTF-8')
537+ except subprocess.CalledProcessError:
538+ return False
539+
540+
541 @cached
542 def juju_version():
543 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
544@@ -976,3 +1006,16 @@
545 for callback, args, kwargs in reversed(_atexit):
546 callback(*args, **kwargs)
547 del _atexit[:]
548+
549+
550+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
551+def network_get_primary_address(binding):
552+ '''
553+ Retrieve the primary network address for a named binding
554+
555+ :param binding: string. The name of a relation of extra-binding
556+ :return: string. The primary IP address for the named binding
557+ :raise: NotImplementedError if run on Juju < 2.0
558+ '''
559+ cmd = ['network-get', '--primary-address', binding]
560+ return subprocess.check_output(cmd).decode('UTF-8').strip()
561
562=== modified file 'hooks/charmhelpers/core/host.py'
563--- hooks/charmhelpers/core/host.py 2016-01-11 18:11:26 +0000
564+++ hooks/charmhelpers/core/host.py 2016-09-22 21:57:09 +0000
565@@ -1,18 +1,16 @@
566 # Copyright 2014-2015 Canonical Limited.
567 #
568-# This file is part of charm-helpers.
569-#
570-# charm-helpers is free software: you can redistribute it and/or modify
571-# it under the terms of the GNU Lesser General Public License version 3 as
572-# published by the Free Software Foundation.
573-#
574-# charm-helpers is distributed in the hope that it will be useful,
575-# but WITHOUT ANY WARRANTY; without even the implied warranty of
576-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
577-# GNU Lesser General Public License for more details.
578-#
579-# You should have received a copy of the GNU Lesser General Public License
580-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
581+# Licensed under the Apache License, Version 2.0 (the "License");
582+# you may not use this file except in compliance with the License.
583+# You may obtain a copy of the License at
584+#
585+# http://www.apache.org/licenses/LICENSE-2.0
586+#
587+# Unless required by applicable law or agreed to in writing, software
588+# distributed under the License is distributed on an "AS IS" BASIS,
589+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
590+# See the License for the specific language governing permissions and
591+# limitations under the License.
592
593 """Tools for working with the host system"""
594 # Copyright 2012 Canonical Ltd.
595@@ -30,13 +28,31 @@
596 import string
597 import subprocess
598 import hashlib
599+import functools
600+import itertools
601+import six
602+
603 from contextlib import contextmanager
604 from collections import OrderedDict
605-
606-import six
607-
608 from .hookenv import log
609 from .fstab import Fstab
610+from charmhelpers.osplatform import get_platform
611+
612+__platform__ = get_platform()
613+if __platform__ == "ubuntu":
614+ from charmhelpers.core.host_factory.ubuntu import (
615+ service_available,
616+ add_new_group,
617+ lsb_release,
618+ cmp_pkgrevno,
619+ ) # flake8: noqa -- ignore F401 for this import
620+elif __platform__ == "centos":
621+ from charmhelpers.core.host_factory.centos import (
622+ service_available,
623+ add_new_group,
624+ lsb_release,
625+ cmp_pkgrevno,
626+ ) # flake8: noqa -- ignore F401 for this import
627
628
629 def service_start(service_name):
630@@ -126,47 +142,48 @@
631 return subprocess.call(cmd) == 0
632
633
634+_UPSTART_CONF = "/etc/init/{}.conf"
635+_INIT_D_CONF = "/etc/init.d/{}"
636+
637+
638 def service_running(service_name):
639 """Determine whether a system service is running"""
640 if init_is_systemd():
641 return service('is-active', service_name)
642 else:
643- try:
644- output = subprocess.check_output(
645- ['service', service_name, 'status'],
646- stderr=subprocess.STDOUT).decode('UTF-8')
647- except subprocess.CalledProcessError:
648- return False
649- else:
650- if ("start/running" in output or "is running" in output):
651- return True
652- else:
653+ if os.path.exists(_UPSTART_CONF.format(service_name)):
654+ try:
655+ output = subprocess.check_output(
656+ ['status', service_name],
657+ stderr=subprocess.STDOUT).decode('UTF-8')
658+ except subprocess.CalledProcessError:
659 return False
660-
661-
662-def service_available(service_name):
663- """Determine whether a system service is available"""
664- try:
665- subprocess.check_output(
666- ['service', service_name, 'status'],
667- stderr=subprocess.STDOUT).decode('UTF-8')
668- except subprocess.CalledProcessError as e:
669- return b'unrecognized service' not in e.output
670- else:
671- return True
672+ else:
673+ # This works for upstart scripts where the 'service' command
674+ # returns a consistent string to represent running
675+ # 'start/running'
676+ if ("start/running" in output or
677+ "is running" in output or
678+ "up and running" in output):
679+ return True
680+ elif os.path.exists(_INIT_D_CONF.format(service_name)):
681+ # Check System V scripts init script return codes
682+ return service('status', service_name)
683+ return False
684
685
686 SYSTEMD_SYSTEM = '/run/systemd/system'
687
688
689 def init_is_systemd():
690+ """Return True if the host system uses systemd, False otherwise."""
691 return os.path.isdir(SYSTEMD_SYSTEM)
692
693
694-def adduser(username, password=None, shell='/bin/bash', system_user=False,
695- primary_group=None, secondary_groups=None):
696- """
697- Add a user to the system.
698+def adduser(username, password=None, shell='/bin/bash',
699+ system_user=False, primary_group=None,
700+ secondary_groups=None, uid=None, home_dir=None):
701+ """Add a user to the system.
702
703 Will log but otherwise succeed if the user already exists.
704
705@@ -174,17 +191,26 @@
706 :param str password: Password for user; if ``None``, create a system user
707 :param str shell: The default shell for the user
708 :param bool system_user: Whether to create a login or system user
709- :param str primary_group: Primary group for user; defaults to their username
710+ :param str primary_group: Primary group for user; defaults to username
711 :param list secondary_groups: Optional list of additional groups
712+ :param int uid: UID for user being created
713+ :param str home_dir: Home directory for user
714
715 :returns: The password database entry struct, as returned by `pwd.getpwnam`
716 """
717 try:
718 user_info = pwd.getpwnam(username)
719 log('user {0} already exists!'.format(username))
720+ if uid:
721+ user_info = pwd.getpwuid(int(uid))
722+ log('user with uid {0} already exists!'.format(uid))
723 except KeyError:
724 log('creating user {0}'.format(username))
725 cmd = ['useradd']
726+ if uid:
727+ cmd.extend(['--uid', str(uid)])
728+ if home_dir:
729+ cmd.extend(['--home', str(home_dir)])
730 if system_user or password is None:
731 cmd.append('--system')
732 else:
733@@ -219,22 +245,56 @@
734 return user_exists
735
736
737-def add_group(group_name, system_group=False):
738- """Add a group to the system"""
739+def uid_exists(uid):
740+ """Check if a uid exists"""
741+ try:
742+ pwd.getpwuid(uid)
743+ uid_exists = True
744+ except KeyError:
745+ uid_exists = False
746+ return uid_exists
747+
748+
749+def group_exists(groupname):
750+ """Check if a group exists"""
751+ try:
752+ grp.getgrnam(groupname)
753+ group_exists = True
754+ except KeyError:
755+ group_exists = False
756+ return group_exists
757+
758+
759+def gid_exists(gid):
760+ """Check if a gid exists"""
761+ try:
762+ grp.getgrgid(gid)
763+ gid_exists = True
764+ except KeyError:
765+ gid_exists = False
766+ return gid_exists
767+
768+
769+def add_group(group_name, system_group=False, gid=None):
770+ """Add a group to the system
771+
772+ Will log but otherwise succeed if the group already exists.
773+
774+ :param str group_name: group to create
775+ :param bool system_group: Create system group
776+ :param int gid: GID for user being created
777+
778+ :returns: The password database entry struct, as returned by `grp.getgrnam`
779+ """
780 try:
781 group_info = grp.getgrnam(group_name)
782 log('group {0} already exists!'.format(group_name))
783+ if gid:
784+ group_info = grp.getgrgid(gid)
785+ log('group with gid {0} already exists!'.format(gid))
786 except KeyError:
787 log('creating group {0}'.format(group_name))
788- cmd = ['addgroup']
789- if system_group:
790- cmd.append('--system')
791- else:
792- cmd.extend([
793- '--group',
794- ])
795- cmd.append(group_name)
796- subprocess.check_call(cmd)
797+ add_new_group(group_name, system_group, gid)
798 group_info = grp.getgrnam(group_name)
799 return group_info
800
801@@ -300,14 +360,12 @@
802
803
804 def fstab_remove(mp):
805- """Remove the given mountpoint entry from /etc/fstab
806- """
807+ """Remove the given mountpoint entry from /etc/fstab"""
808 return Fstab.remove_by_mountpoint(mp)
809
810
811 def fstab_add(dev, mp, fs, options=None):
812- """Adds the given device entry to the /etc/fstab file
813- """
814+ """Adds the given device entry to the /etc/fstab file"""
815 return Fstab.add(dev, mp, fs, options=options)
816
817
818@@ -363,8 +421,7 @@
819
820
821 def file_hash(path, hash_type='md5'):
822- """
823- Generate a hash checksum of the contents of 'path' or None if not found.
824+ """Generate a hash checksum of the contents of 'path' or None if not found.
825
826 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
827 such as md5, sha1, sha256, sha512, etc.
828@@ -379,10 +436,9 @@
829
830
831 def path_hash(path):
832- """
833- Generate a hash checksum of all files matching 'path'. Standard wildcards
834- like '*' and '?' are supported, see documentation for the 'glob' module for
835- more information.
836+ """Generate a hash checksum of all files matching 'path'. Standard
837+ wildcards like '*' and '?' are supported, see documentation for the 'glob'
838+ module for more information.
839
840 :return: dict: A { filename: hash } dictionary for all matched files.
841 Empty if none found.
842@@ -394,8 +450,7 @@
843
844
845 def check_hash(path, checksum, hash_type='md5'):
846- """
847- Validate a file using a cryptographic checksum.
848+ """Validate a file using a cryptographic checksum.
849
850 :param str checksum: Value of the checksum used to validate the file.
851 :param str hash_type: Hash algorithm used to generate `checksum`.
852@@ -410,10 +465,11 @@
853
854
855 class ChecksumError(ValueError):
856+ """A class derived from Value error to indicate the checksum failed."""
857 pass
858
859
860-def restart_on_change(restart_map, stopstart=False):
861+def restart_on_change(restart_map, stopstart=False, restart_functions=None):
862 """Restart services based on configuration files changing
863
864 This function is used a decorator, for example::
865@@ -431,35 +487,56 @@
866 restarted if any file matching the pattern got changed, created
867 or removed. Standard wildcards are supported, see documentation
868 for the 'glob' module for more information.
869+
870+ @param restart_map: {path_file_name: [service_name, ...]
871+ @param stopstart: DEFAULT false; whether to stop, start OR restart
872+ @param restart_functions: nonstandard functions to use to restart services
873+ {svc: func, ...}
874+ @returns result from decorated function
875 """
876 def wrap(f):
877+ @functools.wraps(f)
878 def wrapped_f(*args, **kwargs):
879- checksums = {path: path_hash(path) for path in restart_map}
880- f(*args, **kwargs)
881- restarts = []
882- for path in restart_map:
883- if path_hash(path) != checksums[path]:
884- restarts += restart_map[path]
885- services_list = list(OrderedDict.fromkeys(restarts))
886- if not stopstart:
887- for service_name in services_list:
888- service('restart', service_name)
889- else:
890- for action in ['stop', 'start']:
891- for service_name in services_list:
892- service(action, service_name)
893+ return restart_on_change_helper(
894+ (lambda: f(*args, **kwargs)), restart_map, stopstart,
895+ restart_functions)
896 return wrapped_f
897 return wrap
898
899
900-def lsb_release():
901- """Return /etc/lsb-release in a dict"""
902- d = {}
903- with open('/etc/lsb-release', 'r') as lsb:
904- for l in lsb:
905- k, v = l.split('=')
906- d[k.strip()] = v.strip()
907- return d
908+def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
909+ restart_functions=None):
910+ """Helper function to perform the restart_on_change function.
911+
912+ This is provided for decorators to restart services if files described
913+ in the restart_map have changed after an invocation of lambda_f().
914+
915+ @param lambda_f: function to call.
916+ @param restart_map: {file: [service, ...]}
917+ @param stopstart: whether to stop, start or restart a service
918+ @param restart_functions: nonstandard functions to use to restart services
919+ {svc: func, ...}
920+ @returns result of lambda_f()
921+ """
922+ if restart_functions is None:
923+ restart_functions = {}
924+ checksums = {path: path_hash(path) for path in restart_map}
925+ r = lambda_f()
926+ # create a list of lists of the services to restart
927+ restarts = [restart_map[path]
928+ for path in restart_map
929+ if path_hash(path) != checksums[path]]
930+ # create a flat list of ordered services without duplicates from lists
931+ services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
932+ if services_list:
933+ actions = ('stop', 'start') if stopstart else ('restart',)
934+ for service_name in services_list:
935+ if service_name in restart_functions:
936+ restart_functions[service_name](service_name)
937+ else:
938+ for action in actions:
939+ service(action, service_name)
940+ return r
941
942
943 def pwgen(length=None):
944@@ -515,7 +592,7 @@
945
946
947 def list_nics(nic_type=None):
948- '''Return a list of nics of given type(s)'''
949+ """Return a list of nics of given type(s)"""
950 if isinstance(nic_type, six.string_types):
951 int_types = [nic_type]
952 else:
953@@ -557,12 +634,13 @@
954
955
956 def set_nic_mtu(nic, mtu):
957- '''Set MTU on a network interface'''
958+ """Set the Maximum Transmission Unit (MTU) on a network interface."""
959 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
960 subprocess.check_call(cmd)
961
962
963 def get_nic_mtu(nic):
964+ """Return the Maximum Transmission Unit (MTU) for a network interface."""
965 cmd = ['ip', 'addr', 'show', nic]
966 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
967 mtu = ""
968@@ -574,6 +652,7 @@
969
970
971 def get_nic_hwaddr(nic):
972+ """Return the Media Access Control (MAC) for a network interface."""
973 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
974 ip_output = subprocess.check_output(cmd).decode('UTF-8')
975 hwaddr = ""
976@@ -583,39 +662,28 @@
977 return hwaddr
978
979
980-def cmp_pkgrevno(package, revno, pkgcache=None):
981- '''Compare supplied revno with the revno of the installed package
982-
983- * 1 => Installed revno is greater than supplied arg
984- * 0 => Installed revno is the same as supplied arg
985- * -1 => Installed revno is less than supplied arg
986-
987- This function imports apt_cache function from charmhelpers.fetch if
988- the pkgcache argument is None. Be sure to add charmhelpers.fetch if
989- you call this function, or pass an apt_pkg.Cache() instance.
990- '''
991- import apt_pkg
992- if not pkgcache:
993- from charmhelpers.fetch import apt_cache
994- pkgcache = apt_cache()
995- pkg = pkgcache[package]
996- return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
997-
998-
999 @contextmanager
1000-def chdir(d):
1001+def chdir(directory):
1002+ """Change the current working directory to a different directory for a code
1003+ block and return the previous directory after the block exits. Useful to
1004+ run commands from a specificed directory.
1005+
1006+ :param str directory: The directory path to change to for this context.
1007+ """
1008 cur = os.getcwd()
1009 try:
1010- yield os.chdir(d)
1011+ yield os.chdir(directory)
1012 finally:
1013 os.chdir(cur)
1014
1015
1016 def chownr(path, owner, group, follow_links=True, chowntopdir=False):
1017- """
1018- Recursively change user and group ownership of files and directories
1019+ """Recursively change user and group ownership of files and directories
1020 in given path. Doesn't chown path itself by default, only its children.
1021
1022+ :param str path: The string path to start changing ownership.
1023+ :param str owner: The owner string to use when looking up the uid.
1024+ :param str group: The group string to use when looking up the gid.
1025 :param bool follow_links: Also Chown links if True
1026 :param bool chowntopdir: Also chown path itself if True
1027 """
1028@@ -639,15 +707,23 @@
1029
1030
1031 def lchownr(path, owner, group):
1032+ """Recursively change user and group ownership of files and directories
1033+ in a given path, not following symbolic links. See the documentation for
1034+ 'os.lchown' for more information.
1035+
1036+ :param str path: The string path to start changing ownership.
1037+ :param str owner: The owner string to use when looking up the uid.
1038+ :param str group: The group string to use when looking up the gid.
1039+ """
1040 chownr(path, owner, group, follow_links=False)
1041
1042
1043 def get_total_ram():
1044- '''The total amount of system RAM in bytes.
1045+ """The total amount of system RAM in bytes.
1046
1047 This is what is reported by the OS, and may be overcommitted when
1048 there are multiple containers hosted on the same machine.
1049- '''
1050+ """
1051 with open('/proc/meminfo', 'r') as f:
1052 for line in f.readlines():
1053 if line:
1054
1055=== added directory 'hooks/charmhelpers/core/host_factory'
1056=== added file 'hooks/charmhelpers/core/host_factory/__init__.py'
1057=== added file 'hooks/charmhelpers/core/host_factory/centos.py'
1058--- hooks/charmhelpers/core/host_factory/centos.py 1970-01-01 00:00:00 +0000
1059+++ hooks/charmhelpers/core/host_factory/centos.py 2016-09-22 21:57:09 +0000
1060@@ -0,0 +1,56 @@
1061+import subprocess
1062+import yum
1063+import os
1064+
1065+
1066+def service_available(service_name):
1067+ # """Determine whether a system service is available."""
1068+ if os.path.isdir('/run/systemd/system'):
1069+ cmd = ['systemctl', 'is-enabled', service_name]
1070+ else:
1071+ cmd = ['service', service_name, 'is-enabled']
1072+ return subprocess.call(cmd) == 0
1073+
1074+
1075+def add_new_group(group_name, system_group=False, gid=None):
1076+ cmd = ['groupadd']
1077+ if gid:
1078+ cmd.extend(['--gid', str(gid)])
1079+ if system_group:
1080+ cmd.append('-r')
1081+ cmd.append(group_name)
1082+ subprocess.check_call(cmd)
1083+
1084+
1085+def lsb_release():
1086+ """Return /etc/os-release in a dict."""
1087+ d = {}
1088+ with open('/etc/os-release', 'r') as lsb:
1089+ for l in lsb:
1090+ s = l.split('=')
1091+ if len(s) != 2:
1092+ continue
1093+ d[s[0].strip()] = s[1].strip()
1094+ return d
1095+
1096+
1097+def cmp_pkgrevno(package, revno, pkgcache=None):
1098+ """Compare supplied revno with the revno of the installed package.
1099+
1100+ * 1 => Installed revno is greater than supplied arg
1101+ * 0 => Installed revno is the same as supplied arg
1102+ * -1 => Installed revno is less than supplied arg
1103+
1104+ This function imports YumBase function if the pkgcache argument
1105+ is None.
1106+ """
1107+ if not pkgcache:
1108+ y = yum.YumBase()
1109+ packages = y.doPackageLists()
1110+ pkgcache = {i.Name: i.version for i in packages['installed']}
1111+ pkg = pkgcache[package]
1112+ if pkg > revno:
1113+ return 1
1114+ if pkg < revno:
1115+ return -1
1116+ return 0
1117
1118=== added file 'hooks/charmhelpers/core/host_factory/ubuntu.py'
1119--- hooks/charmhelpers/core/host_factory/ubuntu.py 1970-01-01 00:00:00 +0000
1120+++ hooks/charmhelpers/core/host_factory/ubuntu.py 2016-09-22 21:57:09 +0000
1121@@ -0,0 +1,56 @@
1122+import subprocess
1123+
1124+
1125+def service_available(service_name):
1126+ """Determine whether a system service is available"""
1127+ try:
1128+ subprocess.check_output(
1129+ ['service', service_name, 'status'],
1130+ stderr=subprocess.STDOUT).decode('UTF-8')
1131+ except subprocess.CalledProcessError as e:
1132+ return b'unrecognized service' not in e.output
1133+ else:
1134+ return True
1135+
1136+
1137+def add_new_group(group_name, system_group=False, gid=None):
1138+ cmd = ['addgroup']
1139+ if gid:
1140+ cmd.extend(['--gid', str(gid)])
1141+ if system_group:
1142+ cmd.append('--system')
1143+ else:
1144+ cmd.extend([
1145+ '--group',
1146+ ])
1147+ cmd.append(group_name)
1148+ subprocess.check_call(cmd)
1149+
1150+
1151+def lsb_release():
1152+ """Return /etc/lsb-release in a dict"""
1153+ d = {}
1154+ with open('/etc/lsb-release', 'r') as lsb:
1155+ for l in lsb:
1156+ k, v = l.split('=')
1157+ d[k.strip()] = v.strip()
1158+ return d
1159+
1160+
1161+def cmp_pkgrevno(package, revno, pkgcache=None):
1162+ """Compare supplied revno with the revno of the installed package.
1163+
1164+ * 1 => Installed revno is greater than supplied arg
1165+ * 0 => Installed revno is the same as supplied arg
1166+ * -1 => Installed revno is less than supplied arg
1167+
1168+ This function imports apt_cache function from charmhelpers.fetch if
1169+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1170+ you call this function, or pass an apt_pkg.Cache() instance.
1171+ """
1172+ import apt_pkg
1173+ if not pkgcache:
1174+ from charmhelpers.fetch import apt_cache
1175+ pkgcache = apt_cache()
1176+ pkg = pkgcache[package]
1177+ return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
1178
1179=== modified file 'hooks/charmhelpers/core/hugepage.py'
1180--- hooks/charmhelpers/core/hugepage.py 2016-01-11 18:11:26 +0000
1181+++ hooks/charmhelpers/core/hugepage.py 2016-09-22 21:57:09 +0000
1182@@ -2,19 +2,17 @@
1183
1184 # Copyright 2014-2015 Canonical Limited.
1185 #
1186-# This file is part of charm-helpers.
1187-#
1188-# charm-helpers is free software: you can redistribute it and/or modify
1189-# it under the terms of the GNU Lesser General Public License version 3 as
1190-# published by the Free Software Foundation.
1191-#
1192-# charm-helpers is distributed in the hope that it will be useful,
1193-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1194-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1195-# GNU Lesser General Public License for more details.
1196-#
1197-# You should have received a copy of the GNU Lesser General Public License
1198-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1199+# Licensed under the Apache License, Version 2.0 (the "License");
1200+# you may not use this file except in compliance with the License.
1201+# You may obtain a copy of the License at
1202+#
1203+# http://www.apache.org/licenses/LICENSE-2.0
1204+#
1205+# Unless required by applicable law or agreed to in writing, software
1206+# distributed under the License is distributed on an "AS IS" BASIS,
1207+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1208+# See the License for the specific language governing permissions and
1209+# limitations under the License.
1210
1211 import yaml
1212 from charmhelpers.core import fstab
1213
1214=== modified file 'hooks/charmhelpers/core/kernel.py'
1215--- hooks/charmhelpers/core/kernel.py 2016-01-11 18:11:26 +0000
1216+++ hooks/charmhelpers/core/kernel.py 2016-09-22 21:57:09 +0000
1217@@ -3,29 +3,40 @@
1218
1219 # Copyright 2014-2015 Canonical Limited.
1220 #
1221-# This file is part of charm-helpers.
1222-#
1223-# charm-helpers is free software: you can redistribute it and/or modify
1224-# it under the terms of the GNU Lesser General Public License version 3 as
1225-# published by the Free Software Foundation.
1226-#
1227-# charm-helpers is distributed in the hope that it will be useful,
1228-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1229-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1230-# GNU Lesser General Public License for more details.
1231-#
1232-# You should have received a copy of the GNU Lesser General Public License
1233-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1234-
1235-__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1236-
1237+# Licensed under the Apache License, Version 2.0 (the "License");
1238+# you may not use this file except in compliance with the License.
1239+# You may obtain a copy of the License at
1240+#
1241+# http://www.apache.org/licenses/LICENSE-2.0
1242+#
1243+# Unless required by applicable law or agreed to in writing, software
1244+# distributed under the License is distributed on an "AS IS" BASIS,
1245+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1246+# See the License for the specific language governing permissions and
1247+# limitations under the License.
1248+
1249+import re
1250+import subprocess
1251+
1252+from charmhelpers.osplatform import get_platform
1253 from charmhelpers.core.hookenv import (
1254 log,
1255 INFO
1256 )
1257
1258-from subprocess import check_call, check_output
1259-import re
1260+__platform__ = get_platform()
1261+if __platform__ == "ubuntu":
1262+ from charmhelpers.core.kernel_factory.ubuntu import (
1263+ persistent_modprobe,
1264+ update_initramfs,
1265+ ) # flake8: noqa -- ignore F401 for this import
1266+elif __platform__ == "centos":
1267+ from charmhelpers.core.kernel_factory.centos import (
1268+ persistent_modprobe,
1269+ update_initramfs,
1270+ ) # flake8: noqa -- ignore F401 for this import
1271+
1272+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1273
1274
1275 def modprobe(module, persist=True):
1276@@ -34,11 +45,9 @@
1277
1278 log('Loading kernel module %s' % module, level=INFO)
1279
1280- check_call(cmd)
1281+ subprocess.check_call(cmd)
1282 if persist:
1283- with open('/etc/modules', 'r+') as modules:
1284- if module not in modules.read():
1285- modules.write(module)
1286+ persistent_modprobe(module)
1287
1288
1289 def rmmod(module, force=False):
1290@@ -48,21 +57,16 @@
1291 cmd.append('-f')
1292 cmd.append(module)
1293 log('Removing kernel module %s' % module, level=INFO)
1294- return check_call(cmd)
1295+ return subprocess.check_call(cmd)
1296
1297
1298 def lsmod():
1299 """Shows what kernel modules are currently loaded"""
1300- return check_output(['lsmod'],
1301- universal_newlines=True)
1302+ return subprocess.check_output(['lsmod'],
1303+ universal_newlines=True)
1304
1305
1306 def is_module_loaded(module):
1307 """Checks if a kernel module is already loaded"""
1308 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
1309 return len(matches) > 0
1310-
1311-
1312-def update_initramfs(version='all'):
1313- """Updates an initramfs image"""
1314- return check_call(["update-initramfs", "-k", version, "-u"])
1315
1316=== added directory 'hooks/charmhelpers/core/kernel_factory'
1317=== added file 'hooks/charmhelpers/core/kernel_factory/__init__.py'
1318=== added file 'hooks/charmhelpers/core/kernel_factory/centos.py'
1319--- hooks/charmhelpers/core/kernel_factory/centos.py 1970-01-01 00:00:00 +0000
1320+++ hooks/charmhelpers/core/kernel_factory/centos.py 2016-09-22 21:57:09 +0000
1321@@ -0,0 +1,17 @@
1322+import subprocess
1323+import os
1324+
1325+
1326+def persistent_modprobe(module):
1327+ """Load a kernel module and configure for auto-load on reboot."""
1328+ if not os.path.exists('/etc/rc.modules'):
1329+ open('/etc/rc.modules', 'a')
1330+ os.chmod('/etc/rc.modules', 111)
1331+ with open('/etc/rc.modules', 'r+') as modules:
1332+ if module not in modules.read():
1333+ modules.write('modprobe %s\n' % module)
1334+
1335+
1336+def update_initramfs(version='all'):
1337+ """Updates an initramfs image."""
1338+ return subprocess.check_call(["dracut", "-f", version])
1339
1340=== added file 'hooks/charmhelpers/core/kernel_factory/ubuntu.py'
1341--- hooks/charmhelpers/core/kernel_factory/ubuntu.py 1970-01-01 00:00:00 +0000
1342+++ hooks/charmhelpers/core/kernel_factory/ubuntu.py 2016-09-22 21:57:09 +0000
1343@@ -0,0 +1,13 @@
1344+import subprocess
1345+
1346+
1347+def persistent_modprobe(module):
1348+ """Load a kernel module and configure for auto-load on reboot."""
1349+ with open('/etc/modules', 'r+') as modules:
1350+ if module not in modules.read():
1351+ modules.write(module)
1352+
1353+
1354+def update_initramfs(version='all'):
1355+ """Updates an initramfs image."""
1356+ return subprocess.check_call(["update-initramfs", "-k", version, "-u"])
1357
1358=== modified file 'hooks/charmhelpers/core/services/__init__.py'
1359--- hooks/charmhelpers/core/services/__init__.py 2015-02-04 19:09:09 +0000
1360+++ hooks/charmhelpers/core/services/__init__.py 2016-09-22 21:57:09 +0000
1361@@ -1,18 +1,16 @@
1362 # Copyright 2014-2015 Canonical Limited.
1363 #
1364-# This file is part of charm-helpers.
1365-#
1366-# charm-helpers is free software: you can redistribute it and/or modify
1367-# it under the terms of the GNU Lesser General Public License version 3 as
1368-# published by the Free Software Foundation.
1369-#
1370-# charm-helpers is distributed in the hope that it will be useful,
1371-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1372-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1373-# GNU Lesser General Public License for more details.
1374-#
1375-# You should have received a copy of the GNU Lesser General Public License
1376-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1377+# Licensed under the Apache License, Version 2.0 (the "License");
1378+# you may not use this file except in compliance with the License.
1379+# You may obtain a copy of the License at
1380+#
1381+# http://www.apache.org/licenses/LICENSE-2.0
1382+#
1383+# Unless required by applicable law or agreed to in writing, software
1384+# distributed under the License is distributed on an "AS IS" BASIS,
1385+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1386+# See the License for the specific language governing permissions and
1387+# limitations under the License.
1388
1389 from .base import * # NOQA
1390 from .helpers import * # NOQA
1391
1392=== modified file 'hooks/charmhelpers/core/services/base.py'
1393--- hooks/charmhelpers/core/services/base.py 2015-08-19 14:07:56 +0000
1394+++ hooks/charmhelpers/core/services/base.py 2016-09-22 21:57:09 +0000
1395@@ -1,18 +1,16 @@
1396 # Copyright 2014-2015 Canonical Limited.
1397 #
1398-# This file is part of charm-helpers.
1399-#
1400-# charm-helpers is free software: you can redistribute it and/or modify
1401-# it under the terms of the GNU Lesser General Public License version 3 as
1402-# published by the Free Software Foundation.
1403-#
1404-# charm-helpers is distributed in the hope that it will be useful,
1405-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1406-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1407-# GNU Lesser General Public License for more details.
1408-#
1409-# You should have received a copy of the GNU Lesser General Public License
1410-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1411+# Licensed under the Apache License, Version 2.0 (the "License");
1412+# you may not use this file except in compliance with the License.
1413+# You may obtain a copy of the License at
1414+#
1415+# http://www.apache.org/licenses/LICENSE-2.0
1416+#
1417+# Unless required by applicable law or agreed to in writing, software
1418+# distributed under the License is distributed on an "AS IS" BASIS,
1419+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1420+# See the License for the specific language governing permissions and
1421+# limitations under the License.
1422
1423 import os
1424 import json
1425
1426=== modified file 'hooks/charmhelpers/core/services/helpers.py'
1427--- hooks/charmhelpers/core/services/helpers.py 2016-01-11 18:11:26 +0000
1428+++ hooks/charmhelpers/core/services/helpers.py 2016-09-22 21:57:09 +0000
1429@@ -1,18 +1,16 @@
1430 # Copyright 2014-2015 Canonical Limited.
1431 #
1432-# This file is part of charm-helpers.
1433-#
1434-# charm-helpers is free software: you can redistribute it and/or modify
1435-# it under the terms of the GNU Lesser General Public License version 3 as
1436-# published by the Free Software Foundation.
1437-#
1438-# charm-helpers is distributed in the hope that it will be useful,
1439-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1440-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1441-# GNU Lesser General Public License for more details.
1442-#
1443-# You should have received a copy of the GNU Lesser General Public License
1444-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1445+# Licensed under the Apache License, Version 2.0 (the "License");
1446+# you may not use this file except in compliance with the License.
1447+# You may obtain a copy of the License at
1448+#
1449+# http://www.apache.org/licenses/LICENSE-2.0
1450+#
1451+# Unless required by applicable law or agreed to in writing, software
1452+# distributed under the License is distributed on an "AS IS" BASIS,
1453+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1454+# See the License for the specific language governing permissions and
1455+# limitations under the License.
1456
1457 import os
1458 import yaml
1459
1460=== modified file 'hooks/charmhelpers/core/strutils.py'
1461--- hooks/charmhelpers/core/strutils.py 2016-01-11 18:11:26 +0000
1462+++ hooks/charmhelpers/core/strutils.py 2016-09-22 21:57:09 +0000
1463@@ -3,19 +3,17 @@
1464
1465 # Copyright 2014-2015 Canonical Limited.
1466 #
1467-# This file is part of charm-helpers.
1468-#
1469-# charm-helpers is free software: you can redistribute it and/or modify
1470-# it under the terms of the GNU Lesser General Public License version 3 as
1471-# published by the Free Software Foundation.
1472-#
1473-# charm-helpers is distributed in the hope that it will be useful,
1474-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1475-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1476-# GNU Lesser General Public License for more details.
1477-#
1478-# You should have received a copy of the GNU Lesser General Public License
1479-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1480+# Licensed under the Apache License, Version 2.0 (the "License");
1481+# you may not use this file except in compliance with the License.
1482+# You may obtain a copy of the License at
1483+#
1484+# http://www.apache.org/licenses/LICENSE-2.0
1485+#
1486+# Unless required by applicable law or agreed to in writing, software
1487+# distributed under the License is distributed on an "AS IS" BASIS,
1488+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1489+# See the License for the specific language governing permissions and
1490+# limitations under the License.
1491
1492 import six
1493 import re
1494
1495=== modified file 'hooks/charmhelpers/core/sysctl.py'
1496--- hooks/charmhelpers/core/sysctl.py 2015-02-25 17:10:21 +0000
1497+++ hooks/charmhelpers/core/sysctl.py 2016-09-22 21:57:09 +0000
1498@@ -3,19 +3,17 @@
1499
1500 # Copyright 2014-2015 Canonical Limited.
1501 #
1502-# This file is part of charm-helpers.
1503-#
1504-# charm-helpers is free software: you can redistribute it and/or modify
1505-# it under the terms of the GNU Lesser General Public License version 3 as
1506-# published by the Free Software Foundation.
1507-#
1508-# charm-helpers is distributed in the hope that it will be useful,
1509-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1510-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1511-# GNU Lesser General Public License for more details.
1512-#
1513-# You should have received a copy of the GNU Lesser General Public License
1514-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1515+# Licensed under the Apache License, Version 2.0 (the "License");
1516+# you may not use this file except in compliance with the License.
1517+# You may obtain a copy of the License at
1518+#
1519+# http://www.apache.org/licenses/LICENSE-2.0
1520+#
1521+# Unless required by applicable law or agreed to in writing, software
1522+# distributed under the License is distributed on an "AS IS" BASIS,
1523+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1524+# See the License for the specific language governing permissions and
1525+# limitations under the License.
1526
1527 import yaml
1528
1529
1530=== modified file 'hooks/charmhelpers/core/templating.py'
1531--- hooks/charmhelpers/core/templating.py 2016-01-11 18:11:26 +0000
1532+++ hooks/charmhelpers/core/templating.py 2016-09-22 21:57:09 +0000
1533@@ -1,20 +1,19 @@
1534 # Copyright 2014-2015 Canonical Limited.
1535 #
1536-# This file is part of charm-helpers.
1537-#
1538-# charm-helpers is free software: you can redistribute it and/or modify
1539-# it under the terms of the GNU Lesser General Public License version 3 as
1540-# published by the Free Software Foundation.
1541-#
1542-# charm-helpers is distributed in the hope that it will be useful,
1543-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1544-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1545-# GNU Lesser General Public License for more details.
1546-#
1547-# You should have received a copy of the GNU Lesser General Public License
1548-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1549+# Licensed under the Apache License, Version 2.0 (the "License");
1550+# you may not use this file except in compliance with the License.
1551+# You may obtain a copy of the License at
1552+#
1553+# http://www.apache.org/licenses/LICENSE-2.0
1554+#
1555+# Unless required by applicable law or agreed to in writing, software
1556+# distributed under the License is distributed on an "AS IS" BASIS,
1557+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1558+# See the License for the specific language governing permissions and
1559+# limitations under the License.
1560
1561 import os
1562+import sys
1563
1564 from charmhelpers.core import host
1565 from charmhelpers.core import hookenv
1566@@ -40,8 +39,9 @@
1567 The rendered template will be written to the file as well as being returned
1568 as a string.
1569
1570- Note: Using this requires python-jinja2; if it is not installed, calling
1571- this will attempt to use charmhelpers.fetch.apt_install to install it.
1572+ Note: Using this requires python-jinja2 or python3-jinja2; if it is not
1573+ installed, calling this will attempt to use charmhelpers.fetch.apt_install
1574+ to install it.
1575 """
1576 try:
1577 from jinja2 import FileSystemLoader, Environment, exceptions
1578@@ -53,7 +53,10 @@
1579 'charmhelpers.fetch to install it',
1580 level=hookenv.ERROR)
1581 raise
1582- apt_install('python-jinja2', fatal=True)
1583+ if sys.version_info.major == 2:
1584+ apt_install('python-jinja2', fatal=True)
1585+ else:
1586+ apt_install('python3-jinja2', fatal=True)
1587 from jinja2 import FileSystemLoader, Environment, exceptions
1588
1589 if template_loader:
1590
1591=== modified file 'hooks/charmhelpers/core/unitdata.py'
1592--- hooks/charmhelpers/core/unitdata.py 2015-08-19 14:07:56 +0000
1593+++ hooks/charmhelpers/core/unitdata.py 2016-09-22 21:57:09 +0000
1594@@ -3,20 +3,17 @@
1595 #
1596 # Copyright 2014-2015 Canonical Limited.
1597 #
1598-# This file is part of charm-helpers.
1599-#
1600-# charm-helpers is free software: you can redistribute it and/or modify
1601-# it under the terms of the GNU Lesser General Public License version 3 as
1602-# published by the Free Software Foundation.
1603-#
1604-# charm-helpers is distributed in the hope that it will be useful,
1605-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1606-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1607-# GNU Lesser General Public License for more details.
1608-#
1609-# You should have received a copy of the GNU Lesser General Public License
1610-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1611-#
1612+# Licensed under the Apache License, Version 2.0 (the "License");
1613+# you may not use this file except in compliance with the License.
1614+# You may obtain a copy of the License at
1615+#
1616+# http://www.apache.org/licenses/LICENSE-2.0
1617+#
1618+# Unless required by applicable law or agreed to in writing, software
1619+# distributed under the License is distributed on an "AS IS" BASIS,
1620+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1621+# See the License for the specific language governing permissions and
1622+# limitations under the License.
1623 #
1624 # Authors:
1625 # Kapil Thangavelu <kapil.foss@gmail.com>
1626
1627=== modified file 'hooks/charmhelpers/fetch/__init__.py'
1628--- hooks/charmhelpers/fetch/__init__.py 2016-01-11 18:11:26 +0000
1629+++ hooks/charmhelpers/fetch/__init__.py 2016-09-22 21:57:09 +0000
1630@@ -1,32 +1,24 @@
1631 # Copyright 2014-2015 Canonical Limited.
1632 #
1633-# This file is part of charm-helpers.
1634-#
1635-# charm-helpers is free software: you can redistribute it and/or modify
1636-# it under the terms of the GNU Lesser General Public License version 3 as
1637-# published by the Free Software Foundation.
1638-#
1639-# charm-helpers is distributed in the hope that it will be useful,
1640-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1641-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1642-# GNU Lesser General Public License for more details.
1643-#
1644-# You should have received a copy of the GNU Lesser General Public License
1645-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1646+# Licensed under the Apache License, Version 2.0 (the "License");
1647+# you may not use this file except in compliance with the License.
1648+# You may obtain a copy of the License at
1649+#
1650+# http://www.apache.org/licenses/LICENSE-2.0
1651+#
1652+# Unless required by applicable law or agreed to in writing, software
1653+# distributed under the License is distributed on an "AS IS" BASIS,
1654+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1655+# See the License for the specific language governing permissions and
1656+# limitations under the License.
1657
1658 import importlib
1659-from tempfile import NamedTemporaryFile
1660-import time
1661+from charmhelpers.osplatform import get_platform
1662 from yaml import safe_load
1663-from charmhelpers.core.host import (
1664- lsb_release
1665-)
1666-import subprocess
1667 from charmhelpers.core.hookenv import (
1668 config,
1669 log,
1670 )
1671-import os
1672
1673 import six
1674 if six.PY3:
1675@@ -35,79 +27,6 @@
1676 from urlparse import urlparse, urlunparse
1677
1678
1679-CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
1680-deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
1681-"""
1682-PROPOSED_POCKET = """# Proposed
1683-deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
1684-"""
1685-CLOUD_ARCHIVE_POCKETS = {
1686- # Folsom
1687- 'folsom': 'precise-updates/folsom',
1688- 'precise-folsom': 'precise-updates/folsom',
1689- 'precise-folsom/updates': 'precise-updates/folsom',
1690- 'precise-updates/folsom': 'precise-updates/folsom',
1691- 'folsom/proposed': 'precise-proposed/folsom',
1692- 'precise-folsom/proposed': 'precise-proposed/folsom',
1693- 'precise-proposed/folsom': 'precise-proposed/folsom',
1694- # Grizzly
1695- 'grizzly': 'precise-updates/grizzly',
1696- 'precise-grizzly': 'precise-updates/grizzly',
1697- 'precise-grizzly/updates': 'precise-updates/grizzly',
1698- 'precise-updates/grizzly': 'precise-updates/grizzly',
1699- 'grizzly/proposed': 'precise-proposed/grizzly',
1700- 'precise-grizzly/proposed': 'precise-proposed/grizzly',
1701- 'precise-proposed/grizzly': 'precise-proposed/grizzly',
1702- # Havana
1703- 'havana': 'precise-updates/havana',
1704- 'precise-havana': 'precise-updates/havana',
1705- 'precise-havana/updates': 'precise-updates/havana',
1706- 'precise-updates/havana': 'precise-updates/havana',
1707- 'havana/proposed': 'precise-proposed/havana',
1708- 'precise-havana/proposed': 'precise-proposed/havana',
1709- 'precise-proposed/havana': 'precise-proposed/havana',
1710- # Icehouse
1711- 'icehouse': 'precise-updates/icehouse',
1712- 'precise-icehouse': 'precise-updates/icehouse',
1713- 'precise-icehouse/updates': 'precise-updates/icehouse',
1714- 'precise-updates/icehouse': 'precise-updates/icehouse',
1715- 'icehouse/proposed': 'precise-proposed/icehouse',
1716- 'precise-icehouse/proposed': 'precise-proposed/icehouse',
1717- 'precise-proposed/icehouse': 'precise-proposed/icehouse',
1718- # Juno
1719- 'juno': 'trusty-updates/juno',
1720- 'trusty-juno': 'trusty-updates/juno',
1721- 'trusty-juno/updates': 'trusty-updates/juno',
1722- 'trusty-updates/juno': 'trusty-updates/juno',
1723- 'juno/proposed': 'trusty-proposed/juno',
1724- 'trusty-juno/proposed': 'trusty-proposed/juno',
1725- 'trusty-proposed/juno': 'trusty-proposed/juno',
1726- # Kilo
1727- 'kilo': 'trusty-updates/kilo',
1728- 'trusty-kilo': 'trusty-updates/kilo',
1729- 'trusty-kilo/updates': 'trusty-updates/kilo',
1730- 'trusty-updates/kilo': 'trusty-updates/kilo',
1731- 'kilo/proposed': 'trusty-proposed/kilo',
1732- 'trusty-kilo/proposed': 'trusty-proposed/kilo',
1733- 'trusty-proposed/kilo': 'trusty-proposed/kilo',
1734- # Liberty
1735- 'liberty': 'trusty-updates/liberty',
1736- 'trusty-liberty': 'trusty-updates/liberty',
1737- 'trusty-liberty/updates': 'trusty-updates/liberty',
1738- 'trusty-updates/liberty': 'trusty-updates/liberty',
1739- 'liberty/proposed': 'trusty-proposed/liberty',
1740- 'trusty-liberty/proposed': 'trusty-proposed/liberty',
1741- 'trusty-proposed/liberty': 'trusty-proposed/liberty',
1742- # Mitaka
1743- 'mitaka': 'trusty-updates/mitaka',
1744- 'trusty-mitaka': 'trusty-updates/mitaka',
1745- 'trusty-mitaka/updates': 'trusty-updates/mitaka',
1746- 'trusty-updates/mitaka': 'trusty-updates/mitaka',
1747- 'mitaka/proposed': 'trusty-proposed/mitaka',
1748- 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
1749- 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
1750-}
1751-
1752 # The order of this list is very important. Handlers should be listed in from
1753 # least- to most-specific URL matching.
1754 FETCH_HANDLERS = (
1755@@ -116,10 +35,6 @@
1756 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
1757 )
1758
1759-APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
1760-APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
1761-APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
1762-
1763
1764 class SourceConfigError(Exception):
1765 pass
1766@@ -157,180 +72,38 @@
1767 return urlunparse(parts)
1768
1769
1770-def filter_installed_packages(packages):
1771- """Returns a list of packages that require installation"""
1772- cache = apt_cache()
1773- _pkgs = []
1774- for package in packages:
1775- try:
1776- p = cache[package]
1777- p.current_ver or _pkgs.append(package)
1778- except KeyError:
1779- log('Package {} has no installation candidate.'.format(package),
1780- level='WARNING')
1781- _pkgs.append(package)
1782- return _pkgs
1783-
1784-
1785-def apt_cache(in_memory=True):
1786- """Build and return an apt cache"""
1787- from apt import apt_pkg
1788- apt_pkg.init()
1789- if in_memory:
1790- apt_pkg.config.set("Dir::Cache::pkgcache", "")
1791- apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
1792- return apt_pkg.Cache()
1793-
1794-
1795-def apt_install(packages, options=None, fatal=False):
1796- """Install one or more packages"""
1797- if options is None:
1798- options = ['--option=Dpkg::Options::=--force-confold']
1799-
1800- cmd = ['apt-get', '--assume-yes']
1801- cmd.extend(options)
1802- cmd.append('install')
1803- if isinstance(packages, six.string_types):
1804- cmd.append(packages)
1805- else:
1806- cmd.extend(packages)
1807- log("Installing {} with options: {}".format(packages,
1808- options))
1809- _run_apt_command(cmd, fatal)
1810-
1811-
1812-def apt_upgrade(options=None, fatal=False, dist=False):
1813- """Upgrade all packages"""
1814- if options is None:
1815- options = ['--option=Dpkg::Options::=--force-confold']
1816-
1817- cmd = ['apt-get', '--assume-yes']
1818- cmd.extend(options)
1819- if dist:
1820- cmd.append('dist-upgrade')
1821- else:
1822- cmd.append('upgrade')
1823- log("Upgrading with options: {}".format(options))
1824- _run_apt_command(cmd, fatal)
1825-
1826-
1827-def apt_update(fatal=False):
1828- """Update local apt cache"""
1829- cmd = ['apt-get', 'update']
1830- _run_apt_command(cmd, fatal)
1831-
1832-
1833-def apt_purge(packages, fatal=False):
1834- """Purge one or more packages"""
1835- cmd = ['apt-get', '--assume-yes', 'purge']
1836- if isinstance(packages, six.string_types):
1837- cmd.append(packages)
1838- else:
1839- cmd.extend(packages)
1840- log("Purging {}".format(packages))
1841- _run_apt_command(cmd, fatal)
1842-
1843-
1844-def apt_mark(packages, mark, fatal=False):
1845- """Flag one or more packages using apt-mark"""
1846- log("Marking {} as {}".format(packages, mark))
1847- cmd = ['apt-mark', mark]
1848- if isinstance(packages, six.string_types):
1849- cmd.append(packages)
1850- else:
1851- cmd.extend(packages)
1852-
1853- if fatal:
1854- subprocess.check_call(cmd, universal_newlines=True)
1855- else:
1856- subprocess.call(cmd, universal_newlines=True)
1857-
1858-
1859-def apt_hold(packages, fatal=False):
1860- return apt_mark(packages, 'hold', fatal=fatal)
1861-
1862-
1863-def apt_unhold(packages, fatal=False):
1864- return apt_mark(packages, 'unhold', fatal=fatal)
1865-
1866-
1867-def add_source(source, key=None):
1868- """Add a package source to this system.
1869-
1870- @param source: a URL or sources.list entry, as supported by
1871- add-apt-repository(1). Examples::
1872-
1873- ppa:charmers/example
1874- deb https://stub:key@private.example.com/ubuntu trusty main
1875-
1876- In addition:
1877- 'proposed:' may be used to enable the standard 'proposed'
1878- pocket for the release.
1879- 'cloud:' may be used to activate official cloud archive pockets,
1880- such as 'cloud:icehouse'
1881- 'distro' may be used as a noop
1882-
1883- @param key: A key to be added to the system's APT keyring and used
1884- to verify the signatures on packages. Ideally, this should be an
1885- ASCII format GPG public key including the block headers. A GPG key
1886- id may also be used, but be aware that only insecure protocols are
1887- available to retrieve the actual public key from a public keyserver
1888- placing your Juju environment at risk. ppa and cloud archive keys
1889- are securely added automtically, so sould not be provided.
1890- """
1891- if source is None:
1892- log('Source is not present. Skipping')
1893- return
1894-
1895- if (source.startswith('ppa:') or
1896- source.startswith('http') or
1897- source.startswith('deb ') or
1898- source.startswith('cloud-archive:')):
1899- subprocess.check_call(['add-apt-repository', '--yes', source])
1900- elif source.startswith('cloud:'):
1901- apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
1902- fatal=True)
1903- pocket = source.split(':')[-1]
1904- if pocket not in CLOUD_ARCHIVE_POCKETS:
1905- raise SourceConfigError(
1906- 'Unsupported cloud: source option %s' %
1907- pocket)
1908- actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
1909- with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
1910- apt.write(CLOUD_ARCHIVE.format(actual_pocket))
1911- elif source == 'proposed':
1912- release = lsb_release()['DISTRIB_CODENAME']
1913- with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
1914- apt.write(PROPOSED_POCKET.format(release))
1915- elif source == 'distro':
1916- pass
1917- else:
1918- log("Unknown source: {!r}".format(source))
1919-
1920- if key:
1921- if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
1922- with NamedTemporaryFile('w+') as key_file:
1923- key_file.write(key)
1924- key_file.flush()
1925- key_file.seek(0)
1926- subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
1927- else:
1928- # Note that hkp: is in no way a secure protocol. Using a
1929- # GPG key id is pointless from a security POV unless you
1930- # absolutely trust your network and DNS.
1931- subprocess.check_call(['apt-key', 'adv', '--keyserver',
1932- 'hkp://keyserver.ubuntu.com:80', '--recv',
1933- key])
1934+__platform__ = get_platform()
1935+module = "charmhelpers.fetch.%s" % __platform__
1936+fetch = importlib.import_module(module)
1937+
1938+filter_installed_packages = fetch.filter_installed_packages
1939+install = fetch.install
1940+upgrade = fetch.upgrade
1941+update = fetch.update
1942+purge = fetch.purge
1943+add_source = fetch.add_source
1944+
1945+if __platform__ == "ubuntu":
1946+ apt_cache = fetch.apt_cache
1947+ apt_install = fetch.install
1948+ apt_update = fetch.update
1949+ apt_upgrade = fetch.upgrade
1950+ apt_purge = fetch.purge
1951+ apt_mark = fetch.apt_mark
1952+ apt_hold = fetch.apt_hold
1953+ apt_unhold = fetch.apt_unhold
1954+ get_upstream_version = fetch.get_upstream_version
1955+elif __platform__ == "centos":
1956+ yum_search = fetch.yum_search
1957
1958
1959 def configure_sources(update=False,
1960 sources_var='install_sources',
1961 keys_var='install_keys'):
1962- """
1963- Configure multiple sources from charm configuration.
1964+ """Configure multiple sources from charm configuration.
1965
1966 The lists are encoded as yaml fragments in the configuration.
1967- The frament needs to be included as a string. Sources and their
1968+ The fragment needs to be included as a string. Sources and their
1969 corresponding keys are of the types supported by add_source().
1970
1971 Example config:
1972@@ -362,12 +135,11 @@
1973 for source, key in zip(sources, keys):
1974 add_source(source, key)
1975 if update:
1976- apt_update(fatal=True)
1977+ fetch.update(fatal=True)
1978
1979
1980 def install_remote(source, *args, **kwargs):
1981- """
1982- Install a file tree from a remote source
1983+ """Install a file tree from a remote source.
1984
1985 The specified source should be a url of the form:
1986 scheme://[host]/path[#[option=value][&...]]
1987@@ -390,19 +162,17 @@
1988 # We ONLY check for True here because can_handle may return a string
1989 # explaining why it can't handle a given source.
1990 handlers = [h for h in plugins() if h.can_handle(source) is True]
1991- installed_to = None
1992 for handler in handlers:
1993 try:
1994- installed_to = handler.install(source, *args, **kwargs)
1995+ return handler.install(source, *args, **kwargs)
1996 except UnhandledSource as e:
1997 log('Install source attempt unsuccessful: {}'.format(e),
1998 level='WARNING')
1999- if not installed_to:
2000- raise UnhandledSource("No handler found for source {}".format(source))
2001- return installed_to
2002+ raise UnhandledSource("No handler found for source {}".format(source))
2003
2004
2005 def install_from_config(config_var_name):
2006+ """Install a file from config."""
2007 charm_config = config()
2008 source = charm_config[config_var_name]
2009 return install_remote(source)
2010@@ -425,40 +195,3 @@
2011 log("FetchHandler {} not found, skipping plugin".format(
2012 handler_name))
2013 return plugin_list
2014-
2015-
2016-def _run_apt_command(cmd, fatal=False):
2017- """
2018- Run an APT command, checking output and retrying if the fatal flag is set
2019- to True.
2020-
2021- :param: cmd: str: The apt command to run.
2022- :param: fatal: bool: Whether the command's output should be checked and
2023- retried.
2024- """
2025- env = os.environ.copy()
2026-
2027- if 'DEBIAN_FRONTEND' not in env:
2028- env['DEBIAN_FRONTEND'] = 'noninteractive'
2029-
2030- if fatal:
2031- retry_count = 0
2032- result = None
2033-
2034- # If the command is considered "fatal", we need to retry if the apt
2035- # lock was not acquired.
2036-
2037- while result is None or result == APT_NO_LOCK:
2038- try:
2039- result = subprocess.check_call(cmd, env=env)
2040- except subprocess.CalledProcessError as e:
2041- retry_count = retry_count + 1
2042- if retry_count > APT_NO_LOCK_RETRY_COUNT:
2043- raise
2044- result = e.returncode
2045- log("Couldn't acquire DPKG lock. Will retry in {} seconds."
2046- "".format(APT_NO_LOCK_RETRY_DELAY))
2047- time.sleep(APT_NO_LOCK_RETRY_DELAY)
2048-
2049- else:
2050- subprocess.call(cmd, env=env)
2051
2052=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
2053--- hooks/charmhelpers/fetch/archiveurl.py 2016-01-11 18:11:26 +0000
2054+++ hooks/charmhelpers/fetch/archiveurl.py 2016-09-22 21:57:09 +0000
2055@@ -1,18 +1,16 @@
2056 # Copyright 2014-2015 Canonical Limited.
2057 #
2058-# This file is part of charm-helpers.
2059-#
2060-# charm-helpers is free software: you can redistribute it and/or modify
2061-# it under the terms of the GNU Lesser General Public License version 3 as
2062-# published by the Free Software Foundation.
2063-#
2064-# charm-helpers is distributed in the hope that it will be useful,
2065-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2066-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2067-# GNU Lesser General Public License for more details.
2068-#
2069-# You should have received a copy of the GNU Lesser General Public License
2070-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2071+# Licensed under the Apache License, Version 2.0 (the "License");
2072+# you may not use this file except in compliance with the License.
2073+# You may obtain a copy of the License at
2074+#
2075+# http://www.apache.org/licenses/LICENSE-2.0
2076+#
2077+# Unless required by applicable law or agreed to in writing, software
2078+# distributed under the License is distributed on an "AS IS" BASIS,
2079+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2080+# See the License for the specific language governing permissions and
2081+# limitations under the License.
2082
2083 import os
2084 import hashlib
2085
2086=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
2087--- hooks/charmhelpers/fetch/bzrurl.py 2016-01-11 18:11:26 +0000
2088+++ hooks/charmhelpers/fetch/bzrurl.py 2016-09-22 21:57:09 +0000
2089@@ -1,18 +1,16 @@
2090 # Copyright 2014-2015 Canonical Limited.
2091 #
2092-# This file is part of charm-helpers.
2093-#
2094-# charm-helpers is free software: you can redistribute it and/or modify
2095-# it under the terms of the GNU Lesser General Public License version 3 as
2096-# published by the Free Software Foundation.
2097-#
2098-# charm-helpers is distributed in the hope that it will be useful,
2099-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2100-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2101-# GNU Lesser General Public License for more details.
2102-#
2103-# You should have received a copy of the GNU Lesser General Public License
2104-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2105+# Licensed under the Apache License, Version 2.0 (the "License");
2106+# you may not use this file except in compliance with the License.
2107+# You may obtain a copy of the License at
2108+#
2109+# http://www.apache.org/licenses/LICENSE-2.0
2110+#
2111+# Unless required by applicable law or agreed to in writing, software
2112+# distributed under the License is distributed on an "AS IS" BASIS,
2113+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2114+# See the License for the specific language governing permissions and
2115+# limitations under the License.
2116
2117 import os
2118 from subprocess import check_call
2119@@ -20,19 +18,20 @@
2120 BaseFetchHandler,
2121 UnhandledSource,
2122 filter_installed_packages,
2123- apt_install,
2124+ install,
2125 )
2126 from charmhelpers.core.host import mkdir
2127
2128
2129 if filter_installed_packages(['bzr']) != []:
2130- apt_install(['bzr'])
2131+ install(['bzr'])
2132 if filter_installed_packages(['bzr']) != []:
2133 raise NotImplementedError('Unable to install bzr')
2134
2135
2136 class BzrUrlFetchHandler(BaseFetchHandler):
2137- """Handler for bazaar branches via generic and lp URLs"""
2138+ """Handler for bazaar branches via generic and lp URLs."""
2139+
2140 def can_handle(self, source):
2141 url_parts = self.parse_url(source)
2142 if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
2143@@ -42,15 +41,23 @@
2144 else:
2145 return True
2146
2147- def branch(self, source, dest):
2148+ def branch(self, source, dest, revno=None):
2149 if not self.can_handle(source):
2150 raise UnhandledSource("Cannot handle {}".format(source))
2151+ cmd_opts = []
2152+ if revno:
2153+ cmd_opts += ['-r', str(revno)]
2154 if os.path.exists(dest):
2155- check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
2156+ cmd = ['bzr', 'pull']
2157+ cmd += cmd_opts
2158+ cmd += ['--overwrite', '-d', dest, source]
2159 else:
2160- check_call(['bzr', 'branch', source, dest])
2161+ cmd = ['bzr', 'branch']
2162+ cmd += cmd_opts
2163+ cmd += [source, dest]
2164+ check_call(cmd)
2165
2166- def install(self, source, dest=None):
2167+ def install(self, source, dest=None, revno=None):
2168 url_parts = self.parse_url(source)
2169 branch_name = url_parts.path.strip("/").split("/")[-1]
2170 if dest:
2171@@ -59,10 +66,11 @@
2172 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2173 branch_name)
2174
2175- if not os.path.exists(dest_dir):
2176- mkdir(dest_dir, perms=0o755)
2177+ if dest and not os.path.exists(dest):
2178+ mkdir(dest, perms=0o755)
2179+
2180 try:
2181- self.branch(source, dest_dir)
2182+ self.branch(source, dest_dir, revno)
2183 except OSError as e:
2184 raise UnhandledSource(e.strerror)
2185 return dest_dir
2186
2187=== added file 'hooks/charmhelpers/fetch/centos.py'
2188--- hooks/charmhelpers/fetch/centos.py 1970-01-01 00:00:00 +0000
2189+++ hooks/charmhelpers/fetch/centos.py 2016-09-22 21:57:09 +0000
2190@@ -0,0 +1,171 @@
2191+# Copyright 2014-2015 Canonical Limited.
2192+#
2193+# Licensed under the Apache License, Version 2.0 (the "License");
2194+# you may not use this file except in compliance with the License.
2195+# You may obtain a copy of the License at
2196+#
2197+# http://www.apache.org/licenses/LICENSE-2.0
2198+#
2199+# Unless required by applicable law or agreed to in writing, software
2200+# distributed under the License is distributed on an "AS IS" BASIS,
2201+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2202+# See the License for the specific language governing permissions and
2203+# limitations under the License.
2204+
2205+import subprocess
2206+import os
2207+import time
2208+import six
2209+import yum
2210+
2211+from tempfile import NamedTemporaryFile
2212+from charmhelpers.core.hookenv import log
2213+
2214+YUM_NO_LOCK = 1 # The return code for "couldn't acquire lock" in YUM.
2215+YUM_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
2216+YUM_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
2217+
2218+
2219+def filter_installed_packages(packages):
2220+ """Return a list of packages that require installation."""
2221+ yb = yum.YumBase()
2222+ package_list = yb.doPackageLists()
2223+ temp_cache = {p.base_package_name: 1 for p in package_list['installed']}
2224+
2225+ _pkgs = [p for p in packages if not temp_cache.get(p, False)]
2226+ return _pkgs
2227+
2228+
2229+def install(packages, options=None, fatal=False):
2230+ """Install one or more packages."""
2231+ cmd = ['yum', '--assumeyes']
2232+ if options is not None:
2233+ cmd.extend(options)
2234+ cmd.append('install')
2235+ if isinstance(packages, six.string_types):
2236+ cmd.append(packages)
2237+ else:
2238+ cmd.extend(packages)
2239+ log("Installing {} with options: {}".format(packages,
2240+ options))
2241+ _run_yum_command(cmd, fatal)
2242+
2243+
2244+def upgrade(options=None, fatal=False, dist=False):
2245+ """Upgrade all packages."""
2246+ cmd = ['yum', '--assumeyes']
2247+ if options is not None:
2248+ cmd.extend(options)
2249+ cmd.append('upgrade')
2250+ log("Upgrading with options: {}".format(options))
2251+ _run_yum_command(cmd, fatal)
2252+
2253+
2254+def update(fatal=False):
2255+ """Update local yum cache."""
2256+ cmd = ['yum', '--assumeyes', 'update']
2257+ log("Update with fatal: {}".format(fatal))
2258+ _run_yum_command(cmd, fatal)
2259+
2260+
2261+def purge(packages, fatal=False):
2262+ """Purge one or more packages."""
2263+ cmd = ['yum', '--assumeyes', 'remove']
2264+ if isinstance(packages, six.string_types):
2265+ cmd.append(packages)
2266+ else:
2267+ cmd.extend(packages)
2268+ log("Purging {}".format(packages))
2269+ _run_yum_command(cmd, fatal)
2270+
2271+
2272+def yum_search(packages):
2273+ """Search for a package."""
2274+ output = {}
2275+ cmd = ['yum', 'search']
2276+ if isinstance(packages, six.string_types):
2277+ cmd.append(packages)
2278+ else:
2279+ cmd.extend(packages)
2280+ log("Searching for {}".format(packages))
2281+ result = subprocess.check_output(cmd)
2282+ for package in list(packages):
2283+ output[package] = package in result
2284+ return output
2285+
2286+
2287+def add_source(source, key=None):
2288+ """Add a package source to this system.
2289+
2290+ @param source: a URL with a rpm package
2291+
2292+ @param key: A key to be added to the system's keyring and used
2293+ to verify the signatures on packages. Ideally, this should be an
2294+ ASCII format GPG public key including the block headers. A GPG key
2295+ id may also be used, but be aware that only insecure protocols are
2296+ available to retrieve the actual public key from a public keyserver
2297+ placing your Juju environment at risk.
2298+ """
2299+ if source is None:
2300+ log('Source is not present. Skipping')
2301+ return
2302+
2303+ if source.startswith('http'):
2304+ directory = '/etc/yum.repos.d/'
2305+ for filename in os.listdir(directory):
2306+ with open(directory + filename, 'r') as rpm_file:
2307+ if source in rpm_file.read():
2308+ break
2309+ else:
2310+ log("Add source: {!r}".format(source))
2311+ # write in the charms.repo
2312+ with open(directory + 'Charms.repo', 'a') as rpm_file:
2313+ rpm_file.write('[%s]\n' % source[7:].replace('/', '_'))
2314+ rpm_file.write('name=%s\n' % source[7:])
2315+ rpm_file.write('baseurl=%s\n\n' % source)
2316+ else:
2317+ log("Unknown source: {!r}".format(source))
2318+
2319+ if key:
2320+ if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
2321+ with NamedTemporaryFile('w+') as key_file:
2322+ key_file.write(key)
2323+ key_file.flush()
2324+ key_file.seek(0)
2325+ subprocess.check_call(['rpm', '--import', key_file])
2326+ else:
2327+ subprocess.check_call(['rpm', '--import', key])
2328+
2329+
2330+def _run_yum_command(cmd, fatal=False):
2331+ """Run an YUM command.
2332+
2333+ Checks the output and retry if the fatal flag is set to True.
2334+
2335+ :param: cmd: str: The yum command to run.
2336+ :param: fatal: bool: Whether the command's output should be checked and
2337+ retried.
2338+ """
2339+ env = os.environ.copy()
2340+
2341+ if fatal:
2342+ retry_count = 0
2343+ result = None
2344+
2345+ # If the command is considered "fatal", we need to retry if the yum
2346+ # lock was not acquired.
2347+
2348+ while result is None or result == YUM_NO_LOCK:
2349+ try:
2350+ result = subprocess.check_call(cmd, env=env)
2351+ except subprocess.CalledProcessError as e:
2352+ retry_count = retry_count + 1
2353+ if retry_count > YUM_NO_LOCK_RETRY_COUNT:
2354+ raise
2355+ result = e.returncode
2356+ log("Couldn't acquire YUM lock. Will retry in {} seconds."
2357+ "".format(YUM_NO_LOCK_RETRY_DELAY))
2358+ time.sleep(YUM_NO_LOCK_RETRY_DELAY)
2359+
2360+ else:
2361+ subprocess.call(cmd, env=env)
2362
2363=== modified file 'hooks/charmhelpers/fetch/giturl.py'
2364--- hooks/charmhelpers/fetch/giturl.py 2016-01-11 18:11:26 +0000
2365+++ hooks/charmhelpers/fetch/giturl.py 2016-09-22 21:57:09 +0000
2366@@ -1,36 +1,35 @@
2367 # Copyright 2014-2015 Canonical Limited.
2368 #
2369-# This file is part of charm-helpers.
2370-#
2371-# charm-helpers is free software: you can redistribute it and/or modify
2372-# it under the terms of the GNU Lesser General Public License version 3 as
2373-# published by the Free Software Foundation.
2374-#
2375-# charm-helpers is distributed in the hope that it will be useful,
2376-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2377-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2378-# GNU Lesser General Public License for more details.
2379-#
2380-# You should have received a copy of the GNU Lesser General Public License
2381-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2382+# Licensed under the Apache License, Version 2.0 (the "License");
2383+# you may not use this file except in compliance with the License.
2384+# You may obtain a copy of the License at
2385+#
2386+# http://www.apache.org/licenses/LICENSE-2.0
2387+#
2388+# Unless required by applicable law or agreed to in writing, software
2389+# distributed under the License is distributed on an "AS IS" BASIS,
2390+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2391+# See the License for the specific language governing permissions and
2392+# limitations under the License.
2393
2394 import os
2395-from subprocess import check_call
2396+from subprocess import check_call, CalledProcessError
2397 from charmhelpers.fetch import (
2398 BaseFetchHandler,
2399 UnhandledSource,
2400 filter_installed_packages,
2401- apt_install,
2402+ install,
2403 )
2404
2405 if filter_installed_packages(['git']) != []:
2406- apt_install(['git'])
2407+ install(['git'])
2408 if filter_installed_packages(['git']) != []:
2409 raise NotImplementedError('Unable to install git')
2410
2411
2412 class GitUrlFetchHandler(BaseFetchHandler):
2413- """Handler for git branches via generic and github URLs"""
2414+ """Handler for git branches via generic and github URLs."""
2415+
2416 def can_handle(self, source):
2417 url_parts = self.parse_url(source)
2418 # TODO (mattyw) no support for ssh git@ yet
2419@@ -63,6 +62,8 @@
2420 branch_name)
2421 try:
2422 self.clone(source, dest_dir, branch, depth)
2423+ except CalledProcessError as e:
2424+ raise UnhandledSource(e)
2425 except OSError as e:
2426 raise UnhandledSource(e.strerror)
2427 return dest_dir
2428
2429=== added file 'hooks/charmhelpers/fetch/ubuntu.py'
2430--- hooks/charmhelpers/fetch/ubuntu.py 1970-01-01 00:00:00 +0000
2431+++ hooks/charmhelpers/fetch/ubuntu.py 2016-09-22 21:57:09 +0000
2432@@ -0,0 +1,336 @@
2433+# Copyright 2014-2015 Canonical Limited.
2434+#
2435+# Licensed under the Apache License, Version 2.0 (the "License");
2436+# you may not use this file except in compliance with the License.
2437+# You may obtain a copy of the License at
2438+#
2439+# http://www.apache.org/licenses/LICENSE-2.0
2440+#
2441+# Unless required by applicable law or agreed to in writing, software
2442+# distributed under the License is distributed on an "AS IS" BASIS,
2443+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2444+# See the License for the specific language governing permissions and
2445+# limitations under the License.
2446+
2447+import os
2448+import six
2449+import time
2450+import subprocess
2451+
2452+from tempfile import NamedTemporaryFile
2453+from charmhelpers.core.host import (
2454+ lsb_release
2455+)
2456+from charmhelpers.core.hookenv import log
2457+from charmhelpers.fetch import SourceConfigError
2458+
2459+CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
2460+deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
2461+"""
2462+
2463+PROPOSED_POCKET = """# Proposed
2464+deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
2465+"""
2466+
2467+CLOUD_ARCHIVE_POCKETS = {
2468+ # Folsom
2469+ 'folsom': 'precise-updates/folsom',
2470+ 'precise-folsom': 'precise-updates/folsom',
2471+ 'precise-folsom/updates': 'precise-updates/folsom',
2472+ 'precise-updates/folsom': 'precise-updates/folsom',
2473+ 'folsom/proposed': 'precise-proposed/folsom',
2474+ 'precise-folsom/proposed': 'precise-proposed/folsom',
2475+ 'precise-proposed/folsom': 'precise-proposed/folsom',
2476+ # Grizzly
2477+ 'grizzly': 'precise-updates/grizzly',
2478+ 'precise-grizzly': 'precise-updates/grizzly',
2479+ 'precise-grizzly/updates': 'precise-updates/grizzly',
2480+ 'precise-updates/grizzly': 'precise-updates/grizzly',
2481+ 'grizzly/proposed': 'precise-proposed/grizzly',
2482+ 'precise-grizzly/proposed': 'precise-proposed/grizzly',
2483+ 'precise-proposed/grizzly': 'precise-proposed/grizzly',
2484+ # Havana
2485+ 'havana': 'precise-updates/havana',
2486+ 'precise-havana': 'precise-updates/havana',
2487+ 'precise-havana/updates': 'precise-updates/havana',
2488+ 'precise-updates/havana': 'precise-updates/havana',
2489+ 'havana/proposed': 'precise-proposed/havana',
2490+ 'precise-havana/proposed': 'precise-proposed/havana',
2491+ 'precise-proposed/havana': 'precise-proposed/havana',
2492+ # Icehouse
2493+ 'icehouse': 'precise-updates/icehouse',
2494+ 'precise-icehouse': 'precise-updates/icehouse',
2495+ 'precise-icehouse/updates': 'precise-updates/icehouse',
2496+ 'precise-updates/icehouse': 'precise-updates/icehouse',
2497+ 'icehouse/proposed': 'precise-proposed/icehouse',
2498+ 'precise-icehouse/proposed': 'precise-proposed/icehouse',
2499+ 'precise-proposed/icehouse': 'precise-proposed/icehouse',
2500+ # Juno
2501+ 'juno': 'trusty-updates/juno',
2502+ 'trusty-juno': 'trusty-updates/juno',
2503+ 'trusty-juno/updates': 'trusty-updates/juno',
2504+ 'trusty-updates/juno': 'trusty-updates/juno',
2505+ 'juno/proposed': 'trusty-proposed/juno',
2506+ 'trusty-juno/proposed': 'trusty-proposed/juno',
2507+ 'trusty-proposed/juno': 'trusty-proposed/juno',
2508+ # Kilo
2509+ 'kilo': 'trusty-updates/kilo',
2510+ 'trusty-kilo': 'trusty-updates/kilo',
2511+ 'trusty-kilo/updates': 'trusty-updates/kilo',
2512+ 'trusty-updates/kilo': 'trusty-updates/kilo',
2513+ 'kilo/proposed': 'trusty-proposed/kilo',
2514+ 'trusty-kilo/proposed': 'trusty-proposed/kilo',
2515+ 'trusty-proposed/kilo': 'trusty-proposed/kilo',
2516+ # Liberty
2517+ 'liberty': 'trusty-updates/liberty',
2518+ 'trusty-liberty': 'trusty-updates/liberty',
2519+ 'trusty-liberty/updates': 'trusty-updates/liberty',
2520+ 'trusty-updates/liberty': 'trusty-updates/liberty',
2521+ 'liberty/proposed': 'trusty-proposed/liberty',
2522+ 'trusty-liberty/proposed': 'trusty-proposed/liberty',
2523+ 'trusty-proposed/liberty': 'trusty-proposed/liberty',
2524+ # Mitaka
2525+ 'mitaka': 'trusty-updates/mitaka',
2526+ 'trusty-mitaka': 'trusty-updates/mitaka',
2527+ 'trusty-mitaka/updates': 'trusty-updates/mitaka',
2528+ 'trusty-updates/mitaka': 'trusty-updates/mitaka',
2529+ 'mitaka/proposed': 'trusty-proposed/mitaka',
2530+ 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
2531+ 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
2532+ # Newton
2533+ 'newton': 'xenial-updates/newton',
2534+ 'xenial-newton': 'xenial-updates/newton',
2535+ 'xenial-newton/updates': 'xenial-updates/newton',
2536+ 'xenial-updates/newton': 'xenial-updates/newton',
2537+ 'newton/proposed': 'xenial-proposed/newton',
2538+ 'xenial-newton/proposed': 'xenial-proposed/newton',
2539+ 'xenial-proposed/newton': 'xenial-proposed/newton',
2540+}
2541+
2542+APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
2543+APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
2544+APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
2545+
2546+
2547+def filter_installed_packages(packages):
2548+ """Return a list of packages that require installation."""
2549+ cache = apt_cache()
2550+ _pkgs = []
2551+ for package in packages:
2552+ try:
2553+ p = cache[package]
2554+ p.current_ver or _pkgs.append(package)
2555+ except KeyError:
2556+ log('Package {} has no installation candidate.'.format(package),
2557+ level='WARNING')
2558+ _pkgs.append(package)
2559+ return _pkgs
2560+
2561+
2562+def apt_cache(in_memory=True, progress=None):
2563+ """Build and return an apt cache."""
2564+ from apt import apt_pkg
2565+ apt_pkg.init()
2566+ if in_memory:
2567+ apt_pkg.config.set("Dir::Cache::pkgcache", "")
2568+ apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
2569+ return apt_pkg.Cache(progress)
2570+
2571+
2572+def install(packages, options=None, fatal=False):
2573+ """Install one or more packages."""
2574+ if options is None:
2575+ options = ['--option=Dpkg::Options::=--force-confold']
2576+
2577+ cmd = ['apt-get', '--assume-yes']
2578+ cmd.extend(options)
2579+ cmd.append('install')
2580+ if isinstance(packages, six.string_types):
2581+ cmd.append(packages)
2582+ else:
2583+ cmd.extend(packages)
2584+ log("Installing {} with options: {}".format(packages,
2585+ options))
2586+ _run_apt_command(cmd, fatal)
2587+
2588+
2589+def upgrade(options=None, fatal=False, dist=False):
2590+ """Upgrade all packages."""
2591+ if options is None:
2592+ options = ['--option=Dpkg::Options::=--force-confold']
2593+
2594+ cmd = ['apt-get', '--assume-yes']
2595+ cmd.extend(options)
2596+ if dist:
2597+ cmd.append('dist-upgrade')
2598+ else:
2599+ cmd.append('upgrade')
2600+ log("Upgrading with options: {}".format(options))
2601+ _run_apt_command(cmd, fatal)
2602+
2603+
2604+def update(fatal=False):
2605+ """Update local apt cache."""
2606+ cmd = ['apt-get', 'update']
2607+ _run_apt_command(cmd, fatal)
2608+
2609+
2610+def purge(packages, fatal=False):
2611+ """Purge one or more packages."""
2612+ cmd = ['apt-get', '--assume-yes', 'purge']
2613+ if isinstance(packages, six.string_types):
2614+ cmd.append(packages)
2615+ else:
2616+ cmd.extend(packages)
2617+ log("Purging {}".format(packages))
2618+ _run_apt_command(cmd, fatal)
2619+
2620+
2621+def apt_mark(packages, mark, fatal=False):
2622+ """Flag one or more packages using apt-mark."""
2623+ log("Marking {} as {}".format(packages, mark))
2624+ cmd = ['apt-mark', mark]
2625+ if isinstance(packages, six.string_types):
2626+ cmd.append(packages)
2627+ else:
2628+ cmd.extend(packages)
2629+
2630+ if fatal:
2631+ subprocess.check_call(cmd, universal_newlines=True)
2632+ else:
2633+ subprocess.call(cmd, universal_newlines=True)
2634+
2635+
2636+def apt_hold(packages, fatal=False):
2637+ return apt_mark(packages, 'hold', fatal=fatal)
2638+
2639+
2640+def apt_unhold(packages, fatal=False):
2641+ return apt_mark(packages, 'unhold', fatal=fatal)
2642+
2643+
2644+def add_source(source, key=None):
2645+ """Add a package source to this system.
2646+
2647+ @param source: a URL or sources.list entry, as supported by
2648+ add-apt-repository(1). Examples::
2649+
2650+ ppa:charmers/example
2651+ deb https://stub:key@private.example.com/ubuntu trusty main
2652+
2653+ In addition:
2654+ 'proposed:' may be used to enable the standard 'proposed'
2655+ pocket for the release.
2656+ 'cloud:' may be used to activate official cloud archive pockets,
2657+ such as 'cloud:icehouse'
2658+ 'distro' may be used as a noop
2659+
2660+ @param key: A key to be added to the system's APT keyring and used
2661+ to verify the signatures on packages. Ideally, this should be an
2662+ ASCII format GPG public key including the block headers. A GPG key
2663+ id may also be used, but be aware that only insecure protocols are
2664+ available to retrieve the actual public key from a public keyserver
2665+ placing your Juju environment at risk. ppa and cloud archive keys
2666+ are securely added automtically, so sould not be provided.
2667+ """
2668+ if source is None:
2669+ log('Source is not present. Skipping')
2670+ return
2671+
2672+ if (source.startswith('ppa:') or
2673+ source.startswith('http') or
2674+ source.startswith('deb ') or
2675+ source.startswith('cloud-archive:')):
2676+ subprocess.check_call(['add-apt-repository', '--yes', source])
2677+ elif source.startswith('cloud:'):
2678+ install(filter_installed_packages(['ubuntu-cloud-keyring']),
2679+ fatal=True)
2680+ pocket = source.split(':')[-1]
2681+ if pocket not in CLOUD_ARCHIVE_POCKETS:
2682+ raise SourceConfigError(
2683+ 'Unsupported cloud: source option %s' %
2684+ pocket)
2685+ actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
2686+ with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
2687+ apt.write(CLOUD_ARCHIVE.format(actual_pocket))
2688+ elif source == 'proposed':
2689+ release = lsb_release()['DISTRIB_CODENAME']
2690+ with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
2691+ apt.write(PROPOSED_POCKET.format(release))
2692+ elif source == 'distro':
2693+ pass
2694+ else:
2695+ log("Unknown source: {!r}".format(source))
2696+
2697+ if key:
2698+ if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
2699+ with NamedTemporaryFile('w+') as key_file:
2700+ key_file.write(key)
2701+ key_file.flush()
2702+ key_file.seek(0)
2703+ subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
2704+ else:
2705+ # Note that hkp: is in no way a secure protocol. Using a
2706+ # GPG key id is pointless from a security POV unless you
2707+ # absolutely trust your network and DNS.
2708+ subprocess.check_call(['apt-key', 'adv', '--keyserver',
2709+ 'hkp://keyserver.ubuntu.com:80', '--recv',
2710+ key])
2711+
2712+
2713+def _run_apt_command(cmd, fatal=False):
2714+ """Run an APT command.
2715+
2716+ Checks the output and retries if the fatal flag is set
2717+ to True.
2718+
2719+ :param: cmd: str: The apt command to run.
2720+ :param: fatal: bool: Whether the command's output should be checked and
2721+ retried.
2722+ """
2723+ env = os.environ.copy()
2724+
2725+ if 'DEBIAN_FRONTEND' not in env:
2726+ env['DEBIAN_FRONTEND'] = 'noninteractive'
2727+
2728+ if fatal:
2729+ retry_count = 0
2730+ result = None
2731+
2732+ # If the command is considered "fatal", we need to retry if the apt
2733+ # lock was not acquired.
2734+
2735+ while result is None or result == APT_NO_LOCK:
2736+ try:
2737+ result = subprocess.check_call(cmd, env=env)
2738+ except subprocess.CalledProcessError as e:
2739+ retry_count = retry_count + 1
2740+ if retry_count > APT_NO_LOCK_RETRY_COUNT:
2741+ raise
2742+ result = e.returncode
2743+ log("Couldn't acquire DPKG lock. Will retry in {} seconds."
2744+ "".format(APT_NO_LOCK_RETRY_DELAY))
2745+ time.sleep(APT_NO_LOCK_RETRY_DELAY)
2746+
2747+ else:
2748+ subprocess.call(cmd, env=env)
2749+
2750+
2751+def get_upstream_version(package):
2752+ """Determine upstream version based on installed package
2753+
2754+ @returns None (if not installed) or the upstream version
2755+ """
2756+ import apt_pkg
2757+ cache = apt_cache()
2758+ try:
2759+ pkg = cache[package]
2760+ except:
2761+ # the package is unknown to the current apt cache.
2762+ return None
2763+
2764+ if not pkg.current_ver:
2765+ # package is known, but no version is currently installed.
2766+ return None
2767+
2768+ return apt_pkg.upstream_version(pkg.current_ver.ver_str)
2769
2770=== added file 'hooks/charmhelpers/osplatform.py'
2771--- hooks/charmhelpers/osplatform.py 1970-01-01 00:00:00 +0000
2772+++ hooks/charmhelpers/osplatform.py 2016-09-22 21:57:09 +0000
2773@@ -0,0 +1,19 @@
2774+import platform
2775+
2776+
2777+def get_platform():
2778+ """Return the current OS platform.
2779+
2780+ For example: if current os platform is Ubuntu then a string "ubuntu"
2781+ will be returned (which is the name of the module).
2782+ This string is used to decide which platform module should be imported.
2783+ """
2784+ tuple_platform = platform.linux_distribution()
2785+ current_platform = tuple_platform[0]
2786+ if "Ubuntu" in current_platform:
2787+ return "ubuntu"
2788+ elif "CentOS" in current_platform:
2789+ return "centos"
2790+ else:
2791+ raise RuntimeError("This module is not supported on {}."
2792+ .format(current_platform))
2793
2794=== added directory 'tests/charmhelpers'
2795=== added file 'tests/charmhelpers/__init__.py'
2796--- tests/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
2797+++ tests/charmhelpers/__init__.py 2016-09-22 21:57:09 +0000
2798@@ -0,0 +1,36 @@
2799+# Copyright 2014-2015 Canonical Limited.
2800+#
2801+# Licensed under the Apache License, Version 2.0 (the "License");
2802+# you may not use this file except in compliance with the License.
2803+# You may obtain a copy of the License at
2804+#
2805+# http://www.apache.org/licenses/LICENSE-2.0
2806+#
2807+# Unless required by applicable law or agreed to in writing, software
2808+# distributed under the License is distributed on an "AS IS" BASIS,
2809+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2810+# See the License for the specific language governing permissions and
2811+# limitations under the License.
2812+
2813+# Bootstrap charm-helpers, installing its dependencies if necessary using
2814+# only standard libraries.
2815+import subprocess
2816+import sys
2817+
2818+try:
2819+ import six # flake8: noqa
2820+except ImportError:
2821+ if sys.version_info.major == 2:
2822+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
2823+ else:
2824+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
2825+ import six # flake8: noqa
2826+
2827+try:
2828+ import yaml # flake8: noqa
2829+except ImportError:
2830+ if sys.version_info.major == 2:
2831+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
2832+ else:
2833+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
2834+ import yaml # flake8: noqa
2835
2836=== added directory 'tests/charmhelpers/contrib'
2837=== added file 'tests/charmhelpers/contrib/__init__.py'
2838--- tests/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
2839+++ tests/charmhelpers/contrib/__init__.py 2016-09-22 21:57:09 +0000
2840@@ -0,0 +1,13 @@
2841+# Copyright 2014-2015 Canonical Limited.
2842+#
2843+# Licensed under the Apache License, Version 2.0 (the "License");
2844+# you may not use this file except in compliance with the License.
2845+# You may obtain a copy of the License at
2846+#
2847+# http://www.apache.org/licenses/LICENSE-2.0
2848+#
2849+# Unless required by applicable law or agreed to in writing, software
2850+# distributed under the License is distributed on an "AS IS" BASIS,
2851+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2852+# See the License for the specific language governing permissions and
2853+# limitations under the License.
2854
2855=== added directory 'tests/charmhelpers/contrib/amulet'
2856=== added file 'tests/charmhelpers/contrib/amulet/__init__.py'
2857--- tests/charmhelpers/contrib/amulet/__init__.py 1970-01-01 00:00:00 +0000
2858+++ tests/charmhelpers/contrib/amulet/__init__.py 2016-09-22 21:57:09 +0000
2859@@ -0,0 +1,13 @@
2860+# Copyright 2014-2015 Canonical Limited.
2861+#
2862+# Licensed under the Apache License, Version 2.0 (the "License");
2863+# you may not use this file except in compliance with the License.
2864+# You may obtain a copy of the License at
2865+#
2866+# http://www.apache.org/licenses/LICENSE-2.0
2867+#
2868+# Unless required by applicable law or agreed to in writing, software
2869+# distributed under the License is distributed on an "AS IS" BASIS,
2870+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2871+# See the License for the specific language governing permissions and
2872+# limitations under the License.
2873
2874=== added file 'tests/charmhelpers/contrib/amulet/deployment.py'
2875--- tests/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
2876+++ tests/charmhelpers/contrib/amulet/deployment.py 2016-09-22 21:57:09 +0000
2877@@ -0,0 +1,97 @@
2878+# Copyright 2014-2015 Canonical Limited.
2879+#
2880+# Licensed under the Apache License, Version 2.0 (the "License");
2881+# you may not use this file except in compliance with the License.
2882+# You may obtain a copy of the License at
2883+#
2884+# http://www.apache.org/licenses/LICENSE-2.0
2885+#
2886+# Unless required by applicable law or agreed to in writing, software
2887+# distributed under the License is distributed on an "AS IS" BASIS,
2888+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2889+# See the License for the specific language governing permissions and
2890+# limitations under the License.
2891+
2892+import amulet
2893+import os
2894+import six
2895+
2896+
2897+class AmuletDeployment(object):
2898+ """Amulet deployment.
2899+
2900+ This class provides generic Amulet deployment and test runner
2901+ methods.
2902+ """
2903+
2904+ def __init__(self, series=None):
2905+ """Initialize the deployment environment."""
2906+ self.series = None
2907+
2908+ if series:
2909+ self.series = series
2910+ self.d = amulet.Deployment(series=self.series)
2911+ else:
2912+ self.d = amulet.Deployment()
2913+
2914+ def _add_services(self, this_service, other_services):
2915+ """Add services.
2916+
2917+ Add services to the deployment where this_service is the local charm
2918+ that we're testing and other_services are the other services that
2919+ are being used in the local amulet tests.
2920+ """
2921+ if this_service['name'] != os.path.basename(os.getcwd()):
2922+ s = this_service['name']
2923+ msg = "The charm's root directory name needs to be {}".format(s)
2924+ amulet.raise_status(amulet.FAIL, msg=msg)
2925+
2926+ if 'units' not in this_service:
2927+ this_service['units'] = 1
2928+
2929+ self.d.add(this_service['name'], units=this_service['units'],
2930+ constraints=this_service.get('constraints'))
2931+
2932+ for svc in other_services:
2933+ if 'location' in svc:
2934+ branch_location = svc['location']
2935+ elif self.series:
2936+ branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
2937+ else:
2938+ branch_location = None
2939+
2940+ if 'units' not in svc:
2941+ svc['units'] = 1
2942+
2943+ self.d.add(svc['name'], charm=branch_location, units=svc['units'],
2944+ constraints=svc.get('constraints'))
2945+
2946+ def _add_relations(self, relations):
2947+ """Add all of the relations for the services."""
2948+ for k, v in six.iteritems(relations):
2949+ self.d.relate(k, v)
2950+
2951+ def _configure_services(self, configs):
2952+ """Configure all of the services."""
2953+ for service, config in six.iteritems(configs):
2954+ self.d.configure(service, config)
2955+
2956+ def _deploy(self):
2957+ """Deploy environment and wait for all hooks to finish executing."""
2958+ timeout = int(os.environ.get('AMULET_SETUP_TIMEOUT', 900))
2959+ try:
2960+ self.d.setup(timeout=timeout)
2961+ self.d.sentry.wait(timeout=timeout)
2962+ except amulet.helpers.TimeoutError:
2963+ amulet.raise_status(
2964+ amulet.FAIL,
2965+ msg="Deployment timed out ({}s)".format(timeout)
2966+ )
2967+ except Exception:
2968+ raise
2969+
2970+ def run_tests(self):
2971+ """Run all of the methods that are prefixed with 'test_'."""
2972+ for test in dir(self):
2973+ if test.startswith('test_'):
2974+ getattr(self, test)()
2975
2976=== added file 'tests/charmhelpers/contrib/amulet/utils.py'
2977--- tests/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
2978+++ tests/charmhelpers/contrib/amulet/utils.py 2016-09-22 21:57:09 +0000
2979@@ -0,0 +1,827 @@
2980+# Copyright 2014-2015 Canonical Limited.
2981+#
2982+# Licensed under the Apache License, Version 2.0 (the "License");
2983+# you may not use this file except in compliance with the License.
2984+# You may obtain a copy of the License at
2985+#
2986+# http://www.apache.org/licenses/LICENSE-2.0
2987+#
2988+# Unless required by applicable law or agreed to in writing, software
2989+# distributed under the License is distributed on an "AS IS" BASIS,
2990+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2991+# See the License for the specific language governing permissions and
2992+# limitations under the License.
2993+
2994+import io
2995+import json
2996+import logging
2997+import os
2998+import re
2999+import socket
3000+import subprocess
3001+import sys
3002+import time
3003+import uuid
3004+
3005+import amulet
3006+import distro_info
3007+import six
3008+from six.moves import configparser
3009+if six.PY3:
3010+ from urllib import parse as urlparse
3011+else:
3012+ import urlparse
3013+
3014+
3015+class AmuletUtils(object):
3016+ """Amulet utilities.
3017+
3018+ This class provides common utility functions that are used by Amulet
3019+ tests.
3020+ """
3021+
3022+ def __init__(self, log_level=logging.ERROR):
3023+ self.log = self.get_logger(level=log_level)
3024+ self.ubuntu_releases = self.get_ubuntu_releases()
3025+
3026+ def get_logger(self, name="amulet-logger", level=logging.DEBUG):
3027+ """Get a logger object that will log to stdout."""
3028+ log = logging
3029+ logger = log.getLogger(name)
3030+ fmt = log.Formatter("%(asctime)s %(funcName)s "
3031+ "%(levelname)s: %(message)s")
3032+
3033+ handler = log.StreamHandler(stream=sys.stdout)
3034+ handler.setLevel(level)
3035+ handler.setFormatter(fmt)
3036+
3037+ logger.addHandler(handler)
3038+ logger.setLevel(level)
3039+
3040+ return logger
3041+
3042+ def valid_ip(self, ip):
3043+ if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
3044+ return True
3045+ else:
3046+ return False
3047+
3048+ def valid_url(self, url):
3049+ p = re.compile(
3050+ r'^(?:http|ftp)s?://'
3051+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
3052+ r'localhost|'
3053+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
3054+ r'(?::\d+)?'
3055+ r'(?:/?|[/?]\S+)$',
3056+ re.IGNORECASE)
3057+ if p.match(url):
3058+ return True
3059+ else:
3060+ return False
3061+
3062+ def get_ubuntu_release_from_sentry(self, sentry_unit):
3063+ """Get Ubuntu release codename from sentry unit.
3064+
3065+ :param sentry_unit: amulet sentry/service unit pointer
3066+ :returns: list of strings - release codename, failure message
3067+ """
3068+ msg = None
3069+ cmd = 'lsb_release -cs'
3070+ release, code = sentry_unit.run(cmd)
3071+ if code == 0:
3072+ self.log.debug('{} lsb_release: {}'.format(
3073+ sentry_unit.info['unit_name'], release))
3074+ else:
3075+ msg = ('{} `{}` returned {} '
3076+ '{}'.format(sentry_unit.info['unit_name'],
3077+ cmd, release, code))
3078+ if release not in self.ubuntu_releases:
3079+ msg = ("Release ({}) not found in Ubuntu releases "
3080+ "({})".format(release, self.ubuntu_releases))
3081+ return release, msg
3082+
3083+ def validate_services(self, commands):
3084+ """Validate that lists of commands succeed on service units. Can be
3085+ used to verify system services are running on the corresponding
3086+ service units.
3087+
3088+ :param commands: dict with sentry keys and arbitrary command list vals
3089+ :returns: None if successful, Failure string message otherwise
3090+ """
3091+ self.log.debug('Checking status of system services...')
3092+
3093+ # /!\ DEPRECATION WARNING (beisner):
3094+ # New and existing tests should be rewritten to use
3095+ # validate_services_by_name() as it is aware of init systems.
3096+ self.log.warn('DEPRECATION WARNING: use '
3097+ 'validate_services_by_name instead of validate_services '
3098+ 'due to init system differences.')
3099+
3100+ for k, v in six.iteritems(commands):
3101+ for cmd in v:
3102+ output, code = k.run(cmd)
3103+ self.log.debug('{} `{}` returned '
3104+ '{}'.format(k.info['unit_name'],
3105+ cmd, code))
3106+ if code != 0:
3107+ return "command `{}` returned {}".format(cmd, str(code))
3108+ return None
3109+
3110+ def validate_services_by_name(self, sentry_services):
3111+ """Validate system service status by service name, automatically
3112+ detecting init system based on Ubuntu release codename.
3113+
3114+ :param sentry_services: dict with sentry keys and svc list values
3115+ :returns: None if successful, Failure string message otherwise
3116+ """
3117+ self.log.debug('Checking status of system services...')
3118+
3119+ # Point at which systemd became a thing
3120+ systemd_switch = self.ubuntu_releases.index('vivid')
3121+
3122+ for sentry_unit, services_list in six.iteritems(sentry_services):
3123+ # Get lsb_release codename from unit
3124+ release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
3125+ if ret:
3126+ return ret
3127+
3128+ for service_name in services_list:
3129+ if (self.ubuntu_releases.index(release) >= systemd_switch or
3130+ service_name in ['rabbitmq-server', 'apache2']):
3131+ # init is systemd (or regular sysv)
3132+ cmd = 'sudo service {} status'.format(service_name)
3133+ output, code = sentry_unit.run(cmd)
3134+ service_running = code == 0
3135+ elif self.ubuntu_releases.index(release) < systemd_switch:
3136+ # init is upstart
3137+ cmd = 'sudo status {}'.format(service_name)
3138+ output, code = sentry_unit.run(cmd)
3139+ service_running = code == 0 and "start/running" in output
3140+
3141+ self.log.debug('{} `{}` returned '
3142+ '{}'.format(sentry_unit.info['unit_name'],
3143+ cmd, code))
3144+ if not service_running:
3145+ return u"command `{}` returned {} {}".format(
3146+ cmd, output, str(code))
3147+ return None
3148+
3149+ def _get_config(self, unit, filename):
3150+ """Get a ConfigParser object for parsing a unit's config file."""
3151+ file_contents = unit.file_contents(filename)
3152+
3153+ # NOTE(beisner): by default, ConfigParser does not handle options
3154+ # with no value, such as the flags used in the mysql my.cnf file.
3155+ # https://bugs.python.org/issue7005
3156+ config = configparser.ConfigParser(allow_no_value=True)
3157+ config.readfp(io.StringIO(file_contents))
3158+ return config
3159+
3160+ def validate_config_data(self, sentry_unit, config_file, section,
3161+ expected):
3162+ """Validate config file data.
3163+
3164+ Verify that the specified section of the config file contains
3165+ the expected option key:value pairs.
3166+
3167+ Compare expected dictionary data vs actual dictionary data.
3168+ The values in the 'expected' dictionary can be strings, bools, ints,
3169+ longs, or can be a function that evaluates a variable and returns a
3170+ bool.
3171+ """
3172+ self.log.debug('Validating config file data ({} in {} on {})'
3173+ '...'.format(section, config_file,
3174+ sentry_unit.info['unit_name']))
3175+ config = self._get_config(sentry_unit, config_file)
3176+
3177+ if section != 'DEFAULT' and not config.has_section(section):
3178+ return "section [{}] does not exist".format(section)
3179+
3180+ for k in expected.keys():
3181+ if not config.has_option(section, k):
3182+ return "section [{}] is missing option {}".format(section, k)
3183+
3184+ actual = config.get(section, k)
3185+ v = expected[k]
3186+ if (isinstance(v, six.string_types) or
3187+ isinstance(v, bool) or
3188+ isinstance(v, six.integer_types)):
3189+ # handle explicit values
3190+ if actual != v:
3191+ return "section [{}] {}:{} != expected {}:{}".format(
3192+ section, k, actual, k, expected[k])
3193+ # handle function pointers, such as not_null or valid_ip
3194+ elif not v(actual):
3195+ return "section [{}] {}:{} != expected {}:{}".format(
3196+ section, k, actual, k, expected[k])
3197+ return None
3198+
3199+ def _validate_dict_data(self, expected, actual):
3200+ """Validate dictionary data.
3201+
3202+ Compare expected dictionary data vs actual dictionary data.
3203+ The values in the 'expected' dictionary can be strings, bools, ints,
3204+ longs, or can be a function that evaluates a variable and returns a
3205+ bool.
3206+ """
3207+ self.log.debug('actual: {}'.format(repr(actual)))
3208+ self.log.debug('expected: {}'.format(repr(expected)))
3209+
3210+ for k, v in six.iteritems(expected):
3211+ if k in actual:
3212+ if (isinstance(v, six.string_types) or
3213+ isinstance(v, bool) or
3214+ isinstance(v, six.integer_types)):
3215+ # handle explicit values
3216+ if v != actual[k]:
3217+ return "{}:{}".format(k, actual[k])
3218+ # handle function pointers, such as not_null or valid_ip
3219+ elif not v(actual[k]):
3220+ return "{}:{}".format(k, actual[k])
3221+ else:
3222+ return "key '{}' does not exist".format(k)
3223+ return None
3224+
3225+ def validate_relation_data(self, sentry_unit, relation, expected):
3226+ """Validate actual relation data based on expected relation data."""
3227+ actual = sentry_unit.relation(relation[0], relation[1])
3228+ return self._validate_dict_data(expected, actual)
3229+
3230+ def _validate_list_data(self, expected, actual):
3231+ """Compare expected list vs actual list data."""
3232+ for e in expected:
3233+ if e not in actual:
3234+ return "expected item {} not found in actual list".format(e)
3235+ return None
3236+
3237+ def not_null(self, string):
3238+ if string is not None:
3239+ return True
3240+ else:
3241+ return False
3242+
3243+ def _get_file_mtime(self, sentry_unit, filename):
3244+ """Get last modification time of file."""
3245+ return sentry_unit.file_stat(filename)['mtime']
3246+
3247+ def _get_dir_mtime(self, sentry_unit, directory):
3248+ """Get last modification time of directory."""
3249+ return sentry_unit.directory_stat(directory)['mtime']
3250+
3251+ def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
3252+ """Get start time of a process based on the last modification time
3253+ of the /proc/pid directory.
3254+
3255+ :sentry_unit: The sentry unit to check for the service on
3256+ :service: service name to look for in process table
3257+ :pgrep_full: [Deprecated] Use full command line search mode with pgrep
3258+ :returns: epoch time of service process start
3259+ :param commands: list of bash commands
3260+ :param sentry_units: list of sentry unit pointers
3261+ :returns: None if successful; Failure message otherwise
3262+ """
3263+ if pgrep_full is not None:
3264+ # /!\ DEPRECATION WARNING (beisner):
3265+ # No longer implemented, as pidof is now used instead of pgrep.
3266+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
3267+ self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
3268+ 'longer implemented re: lp 1474030.')
3269+
3270+ pid_list = self.get_process_id_list(sentry_unit, service)
3271+ pid = pid_list[0]
3272+ proc_dir = '/proc/{}'.format(pid)
3273+ self.log.debug('Pid for {} on {}: {}'.format(
3274+ service, sentry_unit.info['unit_name'], pid))
3275+
3276+ return self._get_dir_mtime(sentry_unit, proc_dir)
3277+
3278+ def service_restarted(self, sentry_unit, service, filename,
3279+ pgrep_full=None, sleep_time=20):
3280+ """Check if service was restarted.
3281+
3282+ Compare a service's start time vs a file's last modification time
3283+ (such as a config file for that service) to determine if the service
3284+ has been restarted.
3285+ """
3286+ # /!\ DEPRECATION WARNING (beisner):
3287+ # This method is prone to races in that no before-time is known.
3288+ # Use validate_service_config_changed instead.
3289+
3290+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
3291+ # used instead of pgrep. pgrep_full is still passed through to ensure
3292+ # deprecation WARNS. lp1474030
3293+ self.log.warn('DEPRECATION WARNING: use '
3294+ 'validate_service_config_changed instead of '
3295+ 'service_restarted due to known races.')
3296+
3297+ time.sleep(sleep_time)
3298+ if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
3299+ self._get_file_mtime(sentry_unit, filename)):
3300+ return True
3301+ else:
3302+ return False
3303+
3304+ def service_restarted_since(self, sentry_unit, mtime, service,
3305+ pgrep_full=None, sleep_time=20,
3306+ retry_count=30, retry_sleep_time=10):
3307+ """Check if service was been started after a given time.
3308+
3309+ Args:
3310+ sentry_unit (sentry): The sentry unit to check for the service on
3311+ mtime (float): The epoch time to check against
3312+ service (string): service name to look for in process table
3313+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
3314+ sleep_time (int): Initial sleep time (s) before looking for file
3315+ retry_sleep_time (int): Time (s) to sleep between retries
3316+ retry_count (int): If file is not found, how many times to retry
3317+
3318+ Returns:
3319+ bool: True if service found and its start time it newer than mtime,
3320+ False if service is older than mtime or if service was
3321+ not found.
3322+ """
3323+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
3324+ # used instead of pgrep. pgrep_full is still passed through to ensure
3325+ # deprecation WARNS. lp1474030
3326+
3327+ unit_name = sentry_unit.info['unit_name']
3328+ self.log.debug('Checking that %s service restarted since %s on '
3329+ '%s' % (service, mtime, unit_name))
3330+ time.sleep(sleep_time)
3331+ proc_start_time = None
3332+ tries = 0
3333+ while tries <= retry_count and not proc_start_time:
3334+ try:
3335+ proc_start_time = self._get_proc_start_time(sentry_unit,
3336+ service,
3337+ pgrep_full)
3338+ self.log.debug('Attempt {} to get {} proc start time on {} '
3339+ 'OK'.format(tries, service, unit_name))
3340+ except IOError as e:
3341+ # NOTE(beisner) - race avoidance, proc may not exist yet.
3342+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
3343+ self.log.debug('Attempt {} to get {} proc start time on {} '
3344+ 'failed\n{}'.format(tries, service,
3345+ unit_name, e))
3346+ time.sleep(retry_sleep_time)
3347+ tries += 1
3348+
3349+ if not proc_start_time:
3350+ self.log.warn('No proc start time found, assuming service did '
3351+ 'not start')
3352+ return False
3353+ if proc_start_time >= mtime:
3354+ self.log.debug('Proc start time is newer than provided mtime'
3355+ '(%s >= %s) on %s (OK)' % (proc_start_time,
3356+ mtime, unit_name))
3357+ return True
3358+ else:
3359+ self.log.warn('Proc start time (%s) is older than provided mtime '
3360+ '(%s) on %s, service did not '
3361+ 'restart' % (proc_start_time, mtime, unit_name))
3362+ return False
3363+
3364+ def config_updated_since(self, sentry_unit, filename, mtime,
3365+ sleep_time=20, retry_count=30,
3366+ retry_sleep_time=10):
3367+ """Check if file was modified after a given time.
3368+
3369+ Args:
3370+ sentry_unit (sentry): The sentry unit to check the file mtime on
3371+ filename (string): The file to check mtime of
3372+ mtime (float): The epoch time to check against
3373+ sleep_time (int): Initial sleep time (s) before looking for file
3374+ retry_sleep_time (int): Time (s) to sleep between retries
3375+ retry_count (int): If file is not found, how many times to retry
3376+
3377+ Returns:
3378+ bool: True if file was modified more recently than mtime, False if
3379+ file was modified before mtime, or if file not found.
3380+ """
3381+ unit_name = sentry_unit.info['unit_name']
3382+ self.log.debug('Checking that %s updated since %s on '
3383+ '%s' % (filename, mtime, unit_name))
3384+ time.sleep(sleep_time)
3385+ file_mtime = None
3386+ tries = 0
3387+ while tries <= retry_count and not file_mtime:
3388+ try:
3389+ file_mtime = self._get_file_mtime(sentry_unit, filename)
3390+ self.log.debug('Attempt {} to get {} file mtime on {} '
3391+ 'OK'.format(tries, filename, unit_name))
3392+ except IOError as e:
3393+ # NOTE(beisner) - race avoidance, file may not exist yet.
3394+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
3395+ self.log.debug('Attempt {} to get {} file mtime on {} '
3396+ 'failed\n{}'.format(tries, filename,
3397+ unit_name, e))
3398+ time.sleep(retry_sleep_time)
3399+ tries += 1
3400+
3401+ if not file_mtime:
3402+ self.log.warn('Could not determine file mtime, assuming '
3403+ 'file does not exist')
3404+ return False
3405+
3406+ if file_mtime >= mtime:
3407+ self.log.debug('File mtime is newer than provided mtime '
3408+ '(%s >= %s) on %s (OK)' % (file_mtime,
3409+ mtime, unit_name))
3410+ return True
3411+ else:
3412+ self.log.warn('File mtime is older than provided mtime'
3413+ '(%s < on %s) on %s' % (file_mtime,
3414+ mtime, unit_name))
3415+ return False
3416+
3417+ def validate_service_config_changed(self, sentry_unit, mtime, service,
3418+ filename, pgrep_full=None,
3419+ sleep_time=20, retry_count=30,
3420+ retry_sleep_time=10):
3421+ """Check service and file were updated after mtime
3422+
3423+ Args:
3424+ sentry_unit (sentry): The sentry unit to check for the service on
3425+ mtime (float): The epoch time to check against
3426+ service (string): service name to look for in process table
3427+ filename (string): The file to check mtime of
3428+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
3429+ sleep_time (int): Initial sleep in seconds to pass to test helpers
3430+ retry_count (int): If service is not found, how many times to retry
3431+ retry_sleep_time (int): Time in seconds to wait between retries
3432+
3433+ Typical Usage:
3434+ u = OpenStackAmuletUtils(ERROR)
3435+ ...
3436+ mtime = u.get_sentry_time(self.cinder_sentry)
3437+ self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
3438+ if not u.validate_service_config_changed(self.cinder_sentry,
3439+ mtime,
3440+ 'cinder-api',
3441+ '/etc/cinder/cinder.conf')
3442+ amulet.raise_status(amulet.FAIL, msg='update failed')
3443+ Returns:
3444+ bool: True if both service and file where updated/restarted after
3445+ mtime, False if service is older than mtime or if service was
3446+ not found or if filename was modified before mtime.
3447+ """
3448+
3449+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
3450+ # used instead of pgrep. pgrep_full is still passed through to ensure
3451+ # deprecation WARNS. lp1474030
3452+
3453+ service_restart = self.service_restarted_since(
3454+ sentry_unit, mtime,
3455+ service,
3456+ pgrep_full=pgrep_full,
3457+ sleep_time=sleep_time,
3458+ retry_count=retry_count,
3459+ retry_sleep_time=retry_sleep_time)
3460+
3461+ config_update = self.config_updated_since(
3462+ sentry_unit,
3463+ filename,
3464+ mtime,
3465+ sleep_time=sleep_time,
3466+ retry_count=retry_count,
3467+ retry_sleep_time=retry_sleep_time)
3468+
3469+ return service_restart and config_update
3470+
3471+ def get_sentry_time(self, sentry_unit):
3472+ """Return current epoch time on a sentry"""
3473+ cmd = "date +'%s'"
3474+ return float(sentry_unit.run(cmd)[0])
3475+
3476+ def relation_error(self, name, data):
3477+ return 'unexpected relation data in {} - {}'.format(name, data)
3478+
3479+ def endpoint_error(self, name, data):
3480+ return 'unexpected endpoint data in {} - {}'.format(name, data)
3481+
3482+ def get_ubuntu_releases(self):
3483+ """Return a list of all Ubuntu releases in order of release."""
3484+ _d = distro_info.UbuntuDistroInfo()
3485+ _release_list = _d.all
3486+ return _release_list
3487+
3488+ def file_to_url(self, file_rel_path):
3489+ """Convert a relative file path to a file URL."""
3490+ _abs_path = os.path.abspath(file_rel_path)
3491+ return urlparse.urlparse(_abs_path, scheme='file').geturl()
3492+
3493+ def check_commands_on_units(self, commands, sentry_units):
3494+ """Check that all commands in a list exit zero on all
3495+ sentry units in a list.
3496+
3497+ :param commands: list of bash commands
3498+ :param sentry_units: list of sentry unit pointers
3499+ :returns: None if successful; Failure message otherwise
3500+ """
3501+ self.log.debug('Checking exit codes for {} commands on {} '
3502+ 'sentry units...'.format(len(commands),
3503+ len(sentry_units)))
3504+ for sentry_unit in sentry_units:
3505+ for cmd in commands:
3506+ output, code = sentry_unit.run(cmd)
3507+ if code == 0:
3508+ self.log.debug('{} `{}` returned {} '
3509+ '(OK)'.format(sentry_unit.info['unit_name'],
3510+ cmd, code))
3511+ else:
3512+ return ('{} `{}` returned {} '
3513+ '{}'.format(sentry_unit.info['unit_name'],
3514+ cmd, code, output))
3515+ return None
3516+
3517+ def get_process_id_list(self, sentry_unit, process_name,
3518+ expect_success=True):
3519+ """Get a list of process ID(s) from a single sentry juju unit
3520+ for a single process name.
3521+
3522+ :param sentry_unit: Amulet sentry instance (juju unit)
3523+ :param process_name: Process name
3524+ :param expect_success: If False, expect the PID to be missing,
3525+ raise if it is present.
3526+ :returns: List of process IDs
3527+ """
3528+ cmd = 'pidof -x {}'.format(process_name)
3529+ if not expect_success:
3530+ cmd += " || exit 0 && exit 1"
3531+ output, code = sentry_unit.run(cmd)
3532+ if code != 0:
3533+ msg = ('{} `{}` returned {} '
3534+ '{}'.format(sentry_unit.info['unit_name'],
3535+ cmd, code, output))
3536+ amulet.raise_status(amulet.FAIL, msg=msg)
3537+ return str(output).split()
3538+
3539+ def get_unit_process_ids(self, unit_processes, expect_success=True):
3540+ """Construct a dict containing unit sentries, process names, and
3541+ process IDs.
3542+
3543+ :param unit_processes: A dictionary of Amulet sentry instance
3544+ to list of process names.
3545+ :param expect_success: if False expect the processes to not be
3546+ running, raise if they are.
3547+ :returns: Dictionary of Amulet sentry instance to dictionary
3548+ of process names to PIDs.
3549+ """
3550+ pid_dict = {}
3551+ for sentry_unit, process_list in six.iteritems(unit_processes):
3552+ pid_dict[sentry_unit] = {}
3553+ for process in process_list:
3554+ pids = self.get_process_id_list(
3555+ sentry_unit, process, expect_success=expect_success)
3556+ pid_dict[sentry_unit].update({process: pids})
3557+ return pid_dict
3558+
3559+ def validate_unit_process_ids(self, expected, actual):
3560+ """Validate process id quantities for services on units."""
3561+ self.log.debug('Checking units for running processes...')
3562+ self.log.debug('Expected PIDs: {}'.format(expected))
3563+ self.log.debug('Actual PIDs: {}'.format(actual))
3564+
3565+ if len(actual) != len(expected):
3566+ return ('Unit count mismatch. expected, actual: {}, '
3567+ '{} '.format(len(expected), len(actual)))
3568+
3569+ for (e_sentry, e_proc_names) in six.iteritems(expected):
3570+ e_sentry_name = e_sentry.info['unit_name']
3571+ if e_sentry in actual.keys():
3572+ a_proc_names = actual[e_sentry]
3573+ else:
3574+ return ('Expected sentry ({}) not found in actual dict data.'
3575+ '{}'.format(e_sentry_name, e_sentry))
3576+
3577+ if len(e_proc_names.keys()) != len(a_proc_names.keys()):
3578+ return ('Process name count mismatch. expected, actual: {}, '
3579+ '{}'.format(len(expected), len(actual)))
3580+
3581+ for (e_proc_name, e_pids), (a_proc_name, a_pids) in \
3582+ zip(e_proc_names.items(), a_proc_names.items()):
3583+ if e_proc_name != a_proc_name:
3584+ return ('Process name mismatch. expected, actual: {}, '
3585+ '{}'.format(e_proc_name, a_proc_name))
3586+
3587+ a_pids_length = len(a_pids)
3588+ fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
3589+ '{}, {} ({})'.format(e_sentry_name, e_proc_name,
3590+ e_pids, a_pids_length,
3591+ a_pids))
3592+
3593+ # If expected is a list, ensure at least one PID quantity match
3594+ if isinstance(e_pids, list) and \
3595+ a_pids_length not in e_pids:
3596+ return fail_msg
3597+ # If expected is not bool and not list,
3598+ # ensure PID quantities match
3599+ elif not isinstance(e_pids, bool) and \
3600+ not isinstance(e_pids, list) and \
3601+ a_pids_length != e_pids:
3602+ return fail_msg
3603+ # If expected is bool True, ensure 1 or more PIDs exist
3604+ elif isinstance(e_pids, bool) and \
3605+ e_pids is True and a_pids_length < 1:
3606+ return fail_msg
3607+ # If expected is bool False, ensure 0 PIDs exist
3608+ elif isinstance(e_pids, bool) and \
3609+ e_pids is False and a_pids_length != 0:
3610+ return fail_msg
3611+ else:
3612+ self.log.debug('PID check OK: {} {} {}: '
3613+ '{}'.format(e_sentry_name, e_proc_name,
3614+ e_pids, a_pids))
3615+ return None
3616+
3617+ def validate_list_of_identical_dicts(self, list_of_dicts):
3618+ """Check that all dicts within a list are identical."""
3619+ hashes = []
3620+ for _dict in list_of_dicts:
3621+ hashes.append(hash(frozenset(_dict.items())))
3622+
3623+ self.log.debug('Hashes: {}'.format(hashes))
3624+ if len(set(hashes)) == 1:
3625+ self.log.debug('Dicts within list are identical')
3626+ else:
3627+ return 'Dicts within list are not identical'
3628+
3629+ return None
3630+
3631+ def validate_sectionless_conf(self, file_contents, expected):
3632+ """A crude conf parser. Useful to inspect configuration files which
3633+ do not have section headers (as would be necessary in order to use
3634+ the configparser). Such as openstack-dashboard or rabbitmq confs."""
3635+ for line in file_contents.split('\n'):
3636+ if '=' in line:
3637+ args = line.split('=')
3638+ if len(args) <= 1:
3639+ continue
3640+ key = args[0].strip()
3641+ value = args[1].strip()
3642+ if key in expected.keys():
3643+ if expected[key] != value:
3644+ msg = ('Config mismatch. Expected, actual: {}, '
3645+ '{}'.format(expected[key], value))
3646+ amulet.raise_status(amulet.FAIL, msg=msg)
3647+
3648+ def get_unit_hostnames(self, units):
3649+ """Return a dict of juju unit names to hostnames."""
3650+ host_names = {}
3651+ for unit in units:
3652+ host_names[unit.info['unit_name']] = \
3653+ str(unit.file_contents('/etc/hostname').strip())
3654+ self.log.debug('Unit host names: {}'.format(host_names))
3655+ return host_names
3656+
3657+ def run_cmd_unit(self, sentry_unit, cmd):
3658+ """Run a command on a unit, return the output and exit code."""
3659+ output, code = sentry_unit.run(cmd)
3660+ if code == 0:
3661+ self.log.debug('{} `{}` command returned {} '
3662+ '(OK)'.format(sentry_unit.info['unit_name'],
3663+ cmd, code))
3664+ else:
3665+ msg = ('{} `{}` command returned {} '
3666+ '{}'.format(sentry_unit.info['unit_name'],
3667+ cmd, code, output))
3668+ amulet.raise_status(amulet.FAIL, msg=msg)
3669+ return str(output), code
3670+
3671+ def file_exists_on_unit(self, sentry_unit, file_name):
3672+ """Check if a file exists on a unit."""
3673+ try:
3674+ sentry_unit.file_stat(file_name)
3675+ return True
3676+ except IOError:
3677+ return False
3678+ except Exception as e:
3679+ msg = 'Error checking file {}: {}'.format(file_name, e)
3680+ amulet.raise_status(amulet.FAIL, msg=msg)
3681+
3682+ def file_contents_safe(self, sentry_unit, file_name,
3683+ max_wait=60, fatal=False):
3684+ """Get file contents from a sentry unit. Wrap amulet file_contents
3685+ with retry logic to address races where a file checks as existing,
3686+ but no longer exists by the time file_contents is called.
3687+ Return None if file not found. Optionally raise if fatal is True."""
3688+ unit_name = sentry_unit.info['unit_name']
3689+ file_contents = False
3690+ tries = 0
3691+ while not file_contents and tries < (max_wait / 4):
3692+ try:
3693+ file_contents = sentry_unit.file_contents(file_name)
3694+ except IOError:
3695+ self.log.debug('Attempt {} to open file {} from {} '
3696+ 'failed'.format(tries, file_name,
3697+ unit_name))
3698+ time.sleep(4)
3699+ tries += 1
3700+
3701+ if file_contents:
3702+ return file_contents
3703+ elif not fatal:
3704+ return None
3705+ elif fatal:
3706+ msg = 'Failed to get file contents from unit.'
3707+ amulet.raise_status(amulet.FAIL, msg)
3708+
3709+ def port_knock_tcp(self, host="localhost", port=22, timeout=15):
3710+ """Open a TCP socket to check for a listening sevice on a host.
3711+
3712+ :param host: host name or IP address, default to localhost
3713+ :param port: TCP port number, default to 22
3714+ :param timeout: Connect timeout, default to 15 seconds
3715+ :returns: True if successful, False if connect failed
3716+ """
3717+
3718+ # Resolve host name if possible
3719+ try:
3720+ connect_host = socket.gethostbyname(host)
3721+ host_human = "{} ({})".format(connect_host, host)
3722+ except socket.error as e:
3723+ self.log.warn('Unable to resolve address: '
3724+ '{} ({}) Trying anyway!'.format(host, e))
3725+ connect_host = host
3726+ host_human = connect_host
3727+
3728+ # Attempt socket connection
3729+ try:
3730+ knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3731+ knock.settimeout(timeout)
3732+ knock.connect((connect_host, port))
3733+ knock.close()
3734+ self.log.debug('Socket connect OK for host '
3735+ '{} on port {}.'.format(host_human, port))
3736+ return True
3737+ except socket.error as e:
3738+ self.log.debug('Socket connect FAIL for'
3739+ ' {} port {} ({})'.format(host_human, port, e))
3740+ return False
3741+
3742+ def port_knock_units(self, sentry_units, port=22,
3743+ timeout=15, expect_success=True):
3744+ """Open a TCP socket to check for a listening sevice on each
3745+ listed juju unit.
3746+
3747+ :param sentry_units: list of sentry unit pointers
3748+ :param port: TCP port number, default to 22
3749+ :param timeout: Connect timeout, default to 15 seconds
3750+ :expect_success: True by default, set False to invert logic
3751+ :returns: None if successful, Failure message otherwise
3752+ """
3753+ for unit in sentry_units:
3754+ host = unit.info['public-address']
3755+ connected = self.port_knock_tcp(host, port, timeout)
3756+ if not connected and expect_success:
3757+ return 'Socket connect failed.'
3758+ elif connected and not expect_success:
3759+ return 'Socket connected unexpectedly.'
3760+
3761+ def get_uuid_epoch_stamp(self):
3762+ """Returns a stamp string based on uuid4 and epoch time. Useful in
3763+ generating test messages which need to be unique-ish."""
3764+ return '[{}-{}]'.format(uuid.uuid4(), time.time())
3765+
3766+# amulet juju action helpers:
3767+ def run_action(self, unit_sentry, action,
3768+ _check_output=subprocess.check_output,
3769+ params=None):
3770+ """Run the named action on a given unit sentry.
3771+
3772+ params a dict of parameters to use
3773+ _check_output parameter is used for dependency injection.
3774+
3775+ @return action_id.
3776+ """
3777+ unit_id = unit_sentry.info["unit_name"]
3778+ command = ["juju", "action", "do", "--format=json", unit_id, action]
3779+ if params is not None:
3780+ for key, value in params.iteritems():
3781+ command.append("{}={}".format(key, value))
3782+ self.log.info("Running command: %s\n" % " ".join(command))
3783+ output = _check_output(command, universal_newlines=True)
3784+ data = json.loads(output)
3785+ action_id = data[u'Action queued with id']
3786+ return action_id
3787+
3788+ def wait_on_action(self, action_id, _check_output=subprocess.check_output):
3789+ """Wait for a given action, returning if it completed or not.
3790+
3791+ _check_output parameter is used for dependency injection.
3792+ """
3793+ command = ["juju", "action", "fetch", "--format=json", "--wait=0",
3794+ action_id]
3795+ output = _check_output(command, universal_newlines=True)
3796+ data = json.loads(output)
3797+ return data.get(u"status") == "completed"
3798+
3799+ def status_get(self, unit):
3800+ """Return the current service status of this unit."""
3801+ raw_status, return_code = unit.run(
3802+ "status-get --format=json --include-data")
3803+ if return_code != 0:
3804+ return ("unknown", "")
3805+ status = json.loads(raw_status)
3806+ return (status["status"], status["message"])
3807
3808=== added directory 'tests/charmhelpers/contrib/openstack'
3809=== added file 'tests/charmhelpers/contrib/openstack/__init__.py'
3810--- tests/charmhelpers/contrib/openstack/__init__.py 1970-01-01 00:00:00 +0000
3811+++ tests/charmhelpers/contrib/openstack/__init__.py 2016-09-22 21:57:09 +0000
3812@@ -0,0 +1,13 @@
3813+# Copyright 2014-2015 Canonical Limited.
3814+#
3815+# Licensed under the Apache License, Version 2.0 (the "License");
3816+# you may not use this file except in compliance with the License.
3817+# You may obtain a copy of the License at
3818+#
3819+# http://www.apache.org/licenses/LICENSE-2.0
3820+#
3821+# Unless required by applicable law or agreed to in writing, software
3822+# distributed under the License is distributed on an "AS IS" BASIS,
3823+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3824+# See the License for the specific language governing permissions and
3825+# limitations under the License.
3826
3827=== added directory 'tests/charmhelpers/contrib/openstack/amulet'
3828=== added file 'tests/charmhelpers/contrib/openstack/amulet/__init__.py'
3829--- tests/charmhelpers/contrib/openstack/amulet/__init__.py 1970-01-01 00:00:00 +0000
3830+++ tests/charmhelpers/contrib/openstack/amulet/__init__.py 2016-09-22 21:57:09 +0000
3831@@ -0,0 +1,13 @@
3832+# Copyright 2014-2015 Canonical Limited.
3833+#
3834+# Licensed under the Apache License, Version 2.0 (the "License");
3835+# you may not use this file except in compliance with the License.
3836+# You may obtain a copy of the License at
3837+#
3838+# http://www.apache.org/licenses/LICENSE-2.0
3839+#
3840+# Unless required by applicable law or agreed to in writing, software
3841+# distributed under the License is distributed on an "AS IS" BASIS,
3842+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3843+# See the License for the specific language governing permissions and
3844+# limitations under the License.
3845
3846=== added file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
3847--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
3848+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2016-09-22 21:57:09 +0000
3849@@ -0,0 +1,300 @@
3850+# Copyright 2014-2015 Canonical Limited.
3851+#
3852+# Licensed under the Apache License, Version 2.0 (the "License");
3853+# you may not use this file except in compliance with the License.
3854+# You may obtain a copy of the License at
3855+#
3856+# http://www.apache.org/licenses/LICENSE-2.0
3857+#
3858+# Unless required by applicable law or agreed to in writing, software
3859+# distributed under the License is distributed on an "AS IS" BASIS,
3860+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3861+# See the License for the specific language governing permissions and
3862+# limitations under the License.
3863+
3864+import logging
3865+import re
3866+import sys
3867+import six
3868+from collections import OrderedDict
3869+from charmhelpers.contrib.amulet.deployment import (
3870+ AmuletDeployment
3871+)
3872+
3873+DEBUG = logging.DEBUG
3874+ERROR = logging.ERROR
3875+
3876+
3877+class OpenStackAmuletDeployment(AmuletDeployment):
3878+ """OpenStack amulet deployment.
3879+
3880+ This class inherits from AmuletDeployment and has additional support
3881+ that is specifically for use by OpenStack charms.
3882+ """
3883+
3884+ def __init__(self, series=None, openstack=None, source=None,
3885+ stable=True, log_level=DEBUG):
3886+ """Initialize the deployment environment."""
3887+ super(OpenStackAmuletDeployment, self).__init__(series)
3888+ self.log = self.get_logger(level=log_level)
3889+ self.log.info('OpenStackAmuletDeployment: init')
3890+ self.openstack = openstack
3891+ self.source = source
3892+ self.stable = stable
3893+
3894+ def get_logger(self, name="deployment-logger", level=logging.DEBUG):
3895+ """Get a logger object that will log to stdout."""
3896+ log = logging
3897+ logger = log.getLogger(name)
3898+ fmt = log.Formatter("%(asctime)s %(funcName)s "
3899+ "%(levelname)s: %(message)s")
3900+
3901+ handler = log.StreamHandler(stream=sys.stdout)
3902+ handler.setLevel(level)
3903+ handler.setFormatter(fmt)
3904+
3905+ logger.addHandler(handler)
3906+ logger.setLevel(level)
3907+
3908+ return logger
3909+
3910+ def _determine_branch_locations(self, other_services):
3911+ """Determine the branch locations for the other services.
3912+
3913+ Determine if the local branch being tested is derived from its
3914+ stable or next (dev) branch, and based on this, use the corresonding
3915+ stable or next branches for the other_services."""
3916+
3917+ self.log.info('OpenStackAmuletDeployment: determine branch locations')
3918+
3919+ # Charms outside the ~openstack-charmers
3920+ base_charms = {
3921+ 'mysql': ['precise', 'trusty'],
3922+ 'mongodb': ['precise', 'trusty'],
3923+ 'nrpe': ['precise', 'trusty', 'wily', 'xenial'],
3924+ }
3925+
3926+ for svc in other_services:
3927+ # If a location has been explicitly set, use it
3928+ if svc.get('location'):
3929+ continue
3930+ if svc['name'] in base_charms:
3931+ # NOTE: not all charms have support for all series we
3932+ # want/need to test against, so fix to most recent
3933+ # that each base charm supports
3934+ target_series = self.series
3935+ if self.series not in base_charms[svc['name']]:
3936+ target_series = base_charms[svc['name']][-1]
3937+ svc['location'] = 'cs:{}/{}'.format(target_series,
3938+ svc['name'])
3939+ elif self.stable:
3940+ svc['location'] = 'cs:{}/{}'.format(self.series,
3941+ svc['name'])
3942+ else:
3943+ svc['location'] = 'cs:~openstack-charmers-next/{}/{}'.format(
3944+ self.series,
3945+ svc['name']
3946+ )
3947+
3948+ return other_services
3949+
3950+ def _add_services(self, this_service, other_services):
3951+ """Add services to the deployment and set openstack-origin/source."""
3952+ self.log.info('OpenStackAmuletDeployment: adding services')
3953+
3954+ other_services = self._determine_branch_locations(other_services)
3955+
3956+ super(OpenStackAmuletDeployment, self)._add_services(this_service,
3957+ other_services)
3958+
3959+ services = other_services
3960+ services.append(this_service)
3961+
3962+ # Charms which should use the source config option
3963+ use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
3964+ 'ceph-osd', 'ceph-radosgw', 'ceph-mon', 'ceph-proxy']
3965+
3966+ # Charms which can not use openstack-origin, ie. many subordinates
3967+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',
3968+ 'openvswitch-odl', 'neutron-api-odl', 'odl-controller',
3969+ 'cinder-backup', 'nexentaedge-data',
3970+ 'nexentaedge-iscsi-gw', 'nexentaedge-swift-gw',
3971+ 'cinder-nexentaedge', 'nexentaedge-mgmt']
3972+
3973+ if self.openstack:
3974+ for svc in services:
3975+ if svc['name'] not in use_source + no_origin:
3976+ config = {'openstack-origin': self.openstack}
3977+ self.d.configure(svc['name'], config)
3978+
3979+ if self.source:
3980+ for svc in services:
3981+ if svc['name'] in use_source and svc['name'] not in no_origin:
3982+ config = {'source': self.source}
3983+ self.d.configure(svc['name'], config)
3984+
3985+ def _configure_services(self, configs):
3986+ """Configure all of the services."""
3987+ self.log.info('OpenStackAmuletDeployment: configure services')
3988+ for service, config in six.iteritems(configs):
3989+ self.d.configure(service, config)
3990+
3991+ def _auto_wait_for_status(self, message=None, exclude_services=None,
3992+ include_only=None, timeout=1800):
3993+ """Wait for all units to have a specific extended status, except
3994+ for any defined as excluded. Unless specified via message, any
3995+ status containing any case of 'ready' will be considered a match.
3996+
3997+ Examples of message usage:
3998+
3999+ Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
4000+ message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
4001+
4002+ Wait for all units to reach this status (exact match):
4003+ message = re.compile('^Unit is ready and clustered$')
4004+
4005+ Wait for all units to reach any one of these (exact match):
4006+ message = re.compile('Unit is ready|OK|Ready')
4007+
4008+ Wait for at least one unit to reach this status (exact match):
4009+ message = {'ready'}
4010+
4011+ See Amulet's sentry.wait_for_messages() for message usage detail.
4012+ https://github.com/juju/amulet/blob/master/amulet/sentry.py
4013+
4014+ :param message: Expected status match
4015+ :param exclude_services: List of juju service names to ignore,
4016+ not to be used in conjuction with include_only.
4017+ :param include_only: List of juju service names to exclusively check,
4018+ not to be used in conjuction with exclude_services.
4019+ :param timeout: Maximum time in seconds to wait for status match
4020+ :returns: None. Raises if timeout is hit.
4021+ """
4022+ self.log.info('Waiting for extended status on units...')
4023+
4024+ all_services = self.d.services.keys()
4025+
4026+ if exclude_services and include_only:
4027+ raise ValueError('exclude_services can not be used '
4028+ 'with include_only')
4029+
4030+ if message:
4031+ if isinstance(message, re._pattern_type):
4032+ match = message.pattern
4033+ else:
4034+ match = message
4035+
4036+ self.log.debug('Custom extended status wait match: '
4037+ '{}'.format(match))
4038+ else:
4039+ self.log.debug('Default extended status wait match: contains '
4040+ 'READY (case-insensitive)')
4041+ message = re.compile('.*ready.*', re.IGNORECASE)
4042+
4043+ if exclude_services:
4044+ self.log.debug('Excluding services from extended status match: '
4045+ '{}'.format(exclude_services))
4046+ else:
4047+ exclude_services = []
4048+
4049+ if include_only:
4050+ services = include_only
4051+ else:
4052+ services = list(set(all_services) - set(exclude_services))
4053+
4054+ self.log.debug('Waiting up to {}s for extended status on services: '
4055+ '{}'.format(timeout, services))
4056+ service_messages = {service: message for service in services}
4057+ self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
4058+ self.log.info('OK')
4059+
4060+ def _get_openstack_release(self):
4061+ """Get openstack release.
4062+
4063+ Return an integer representing the enum value of the openstack
4064+ release.
4065+ """
4066+ # Must be ordered by OpenStack release (not by Ubuntu release):
4067+ (self.precise_essex, self.precise_folsom, self.precise_grizzly,
4068+ self.precise_havana, self.precise_icehouse,
4069+ self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
4070+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
4071+ self.wily_liberty, self.trusty_mitaka,
4072+ self.xenial_mitaka, self.xenial_newton,
4073+ self.yakkety_newton) = range(16)
4074+
4075+ releases = {
4076+ ('precise', None): self.precise_essex,
4077+ ('precise', 'cloud:precise-folsom'): self.precise_folsom,
4078+ ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
4079+ ('precise', 'cloud:precise-havana'): self.precise_havana,
4080+ ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
4081+ ('trusty', None): self.trusty_icehouse,
4082+ ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
4083+ ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
4084+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
4085+ ('trusty', 'cloud:trusty-mitaka'): self.trusty_mitaka,
4086+ ('utopic', None): self.utopic_juno,
4087+ ('vivid', None): self.vivid_kilo,
4088+ ('wily', None): self.wily_liberty,
4089+ ('xenial', None): self.xenial_mitaka,
4090+ ('xenial', 'cloud:xenial-newton'): self.xenial_newton,
4091+ ('yakkety', None): self.yakkety_newton,
4092+ }
4093+ return releases[(self.series, self.openstack)]
4094+
4095+ def _get_openstack_release_string(self):
4096+ """Get openstack release string.
4097+
4098+ Return a string representing the openstack release.
4099+ """
4100+ releases = OrderedDict([
4101+ ('precise', 'essex'),
4102+ ('quantal', 'folsom'),
4103+ ('raring', 'grizzly'),
4104+ ('saucy', 'havana'),
4105+ ('trusty', 'icehouse'),
4106+ ('utopic', 'juno'),
4107+ ('vivid', 'kilo'),
4108+ ('wily', 'liberty'),
4109+ ('xenial', 'mitaka'),
4110+ ('yakkety', 'newton'),
4111+ ])
4112+ if self.openstack:
4113+ os_origin = self.openstack.split(':')[1]
4114+ return os_origin.split('%s-' % self.series)[1].split('/')[0]
4115+ else:
4116+ return releases[self.series]
4117+
4118+ def get_ceph_expected_pools(self, radosgw=False):
4119+ """Return a list of expected ceph pools in a ceph + cinder + glance
4120+ test scenario, based on OpenStack release and whether ceph radosgw
4121+ is flagged as present or not."""
4122+
4123+ if self._get_openstack_release() >= self.trusty_kilo:
4124+ # Kilo or later
4125+ pools = [
4126+ 'rbd',
4127+ 'cinder',
4128+ 'glance'
4129+ ]
4130+ else:
4131+ # Juno or earlier
4132+ pools = [
4133+ 'data',
4134+ 'metadata',
4135+ 'rbd',
4136+ 'cinder',
4137+ 'glance'
4138+ ]
4139+
4140+ if radosgw:
4141+ pools.extend([
4142+ '.rgw.root',
4143+ '.rgw.control',
4144+ '.rgw',
4145+ '.rgw.gc',
4146+ '.users.uid'
4147+ ])
4148+
4149+ return pools
4150
4151=== added file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
4152--- tests/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
4153+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2016-09-22 21:57:09 +0000
4154@@ -0,0 +1,1127 @@
4155+# Copyright 2014-2015 Canonical Limited.
4156+#
4157+# Licensed under the Apache License, Version 2.0 (the "License");
4158+# you may not use this file except in compliance with the License.
4159+# You may obtain a copy of the License at
4160+#
4161+# http://www.apache.org/licenses/LICENSE-2.0
4162+#
4163+# Unless required by applicable law or agreed to in writing, software
4164+# distributed under the License is distributed on an "AS IS" BASIS,
4165+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4166+# See the License for the specific language governing permissions and
4167+# limitations under the License.
4168+
4169+import amulet
4170+import json
4171+import logging
4172+import os
4173+import re
4174+import six
4175+import time
4176+import urllib
4177+
4178+import cinderclient.v1.client as cinder_client
4179+import glanceclient.v1.client as glance_client
4180+import heatclient.v1.client as heat_client
4181+import keystoneclient.v2_0 as keystone_client
4182+from keystoneclient.auth.identity import v3 as keystone_id_v3
4183+from keystoneclient import session as keystone_session
4184+from keystoneclient.v3 import client as keystone_client_v3
4185+
4186+import novaclient.client as nova_client
4187+import pika
4188+import swiftclient
4189+
4190+from charmhelpers.contrib.amulet.utils import (
4191+ AmuletUtils
4192+)
4193+
4194+DEBUG = logging.DEBUG
4195+ERROR = logging.ERROR
4196+
4197+NOVA_CLIENT_VERSION = "2"
4198+
4199+
4200+class OpenStackAmuletUtils(AmuletUtils):
4201+ """OpenStack amulet utilities.
4202+
4203+ This class inherits from AmuletUtils and has additional support
4204+ that is specifically for use by OpenStack charm tests.
4205+ """
4206+
4207+ def __init__(self, log_level=ERROR):
4208+ """Initialize the deployment environment."""
4209+ super(OpenStackAmuletUtils, self).__init__(log_level)
4210+
4211+ def validate_endpoint_data(self, endpoints, admin_port, internal_port,
4212+ public_port, expected):
4213+ """Validate endpoint data.
4214+
4215+ Validate actual endpoint data vs expected endpoint data. The ports
4216+ are used to find the matching endpoint.
4217+ """
4218+ self.log.debug('Validating endpoint data...')
4219+ self.log.debug('actual: {}'.format(repr(endpoints)))
4220+ found = False
4221+ for ep in endpoints:
4222+ self.log.debug('endpoint: {}'.format(repr(ep)))
4223+ if (admin_port in ep.adminurl and
4224+ internal_port in ep.internalurl and
4225+ public_port in ep.publicurl):
4226+ found = True
4227+ actual = {'id': ep.id,
4228+ 'region': ep.region,
4229+ 'adminurl': ep.adminurl,
4230+ 'internalurl': ep.internalurl,
4231+ 'publicurl': ep.publicurl,
4232+ 'service_id': ep.service_id}
4233+ ret = self._validate_dict_data(expected, actual)
4234+ if ret:
4235+ return 'unexpected endpoint data - {}'.format(ret)
4236+
4237+ if not found:
4238+ return 'endpoint not found'
4239+
4240+ def validate_v3_endpoint_data(self, endpoints, admin_port, internal_port,
4241+ public_port, expected):
4242+ """Validate keystone v3 endpoint data.
4243+
4244+ Validate the v3 endpoint data which has changed from v2. The
4245+ ports are used to find the matching endpoint.
4246+
4247+ The new v3 endpoint data looks like:
4248+
4249+ [<Endpoint enabled=True,
4250+ id=0432655fc2f74d1e9fa17bdaa6f6e60b,
4251+ interface=admin,
4252+ links={u'self': u'<RESTful URL of this endpoint>'},
4253+ region=RegionOne,
4254+ region_id=RegionOne,
4255+ service_id=17f842a0dc084b928e476fafe67e4095,
4256+ url=http://10.5.6.5:9312>,
4257+ <Endpoint enabled=True,
4258+ id=6536cb6cb92f4f41bf22b079935c7707,
4259+ interface=admin,
4260+ links={u'self': u'<RESTful url of this endpoint>'},
4261+ region=RegionOne,
4262+ region_id=RegionOne,
4263+ service_id=72fc8736fb41435e8b3584205bb2cfa3,
4264+ url=http://10.5.6.6:35357/v3>,
4265+ ... ]
4266+ """
4267+ self.log.debug('Validating v3 endpoint data...')
4268+ self.log.debug('actual: {}'.format(repr(endpoints)))
4269+ found = []
4270+ for ep in endpoints:
4271+ self.log.debug('endpoint: {}'.format(repr(ep)))
4272+ if ((admin_port in ep.url and ep.interface == 'admin') or
4273+ (internal_port in ep.url and ep.interface == 'internal') or
4274+ (public_port in ep.url and ep.interface == 'public')):
4275+ found.append(ep.interface)
4276+ # note we ignore the links member.
4277+ actual = {'id': ep.id,
4278+ 'region': ep.region,
4279+ 'region_id': ep.region_id,
4280+ 'interface': self.not_null,
4281+ 'url': ep.url,
4282+ 'service_id': ep.service_id, }
4283+ ret = self._validate_dict_data(expected, actual)
4284+ if ret:
4285+ return 'unexpected endpoint data - {}'.format(ret)
4286+
4287+ if len(found) != 3:
4288+ return 'Unexpected number of endpoints found'
4289+
4290+ def validate_svc_catalog_endpoint_data(self, expected, actual):
4291+ """Validate service catalog endpoint data.
4292+
4293+ Validate a list of actual service catalog endpoints vs a list of
4294+ expected service catalog endpoints.
4295+ """
4296+ self.log.debug('Validating service catalog endpoint data...')
4297+ self.log.debug('actual: {}'.format(repr(actual)))
4298+ for k, v in six.iteritems(expected):
4299+ if k in actual:
4300+ ret = self._validate_dict_data(expected[k][0], actual[k][0])
4301+ if ret:
4302+ return self.endpoint_error(k, ret)
4303+ else:
4304+ return "endpoint {} does not exist".format(k)
4305+ return ret
4306+
4307+ def validate_v3_svc_catalog_endpoint_data(self, expected, actual):
4308+ """Validate the keystone v3 catalog endpoint data.
4309+
4310+ Validate a list of dictinaries that make up the keystone v3 service
4311+ catalogue.
4312+
4313+ It is in the form of:
4314+
4315+
4316+ {u'identity': [{u'id': u'48346b01c6804b298cdd7349aadb732e',
4317+ u'interface': u'admin',
4318+ u'region': u'RegionOne',
4319+ u'region_id': u'RegionOne',
4320+ u'url': u'http://10.5.5.224:35357/v3'},
4321+ {u'id': u'8414f7352a4b47a69fddd9dbd2aef5cf',
4322+ u'interface': u'public',
4323+ u'region': u'RegionOne',
4324+ u'region_id': u'RegionOne',
4325+ u'url': u'http://10.5.5.224:5000/v3'},
4326+ {u'id': u'd5ca31440cc24ee1bf625e2996fb6a5b',
4327+ u'interface': u'internal',
4328+ u'region': u'RegionOne',
4329+ u'region_id': u'RegionOne',
4330+ u'url': u'http://10.5.5.224:5000/v3'}],
4331+ u'key-manager': [{u'id': u'68ebc17df0b045fcb8a8a433ebea9e62',
4332+ u'interface': u'public',
4333+ u'region': u'RegionOne',
4334+ u'region_id': u'RegionOne',
4335+ u'url': u'http://10.5.5.223:9311'},
4336+ {u'id': u'9cdfe2a893c34afd8f504eb218cd2f9d',
4337+ u'interface': u'internal',
4338+ u'region': u'RegionOne',
4339+ u'region_id': u'RegionOne',
4340+ u'url': u'http://10.5.5.223:9311'},
4341+ {u'id': u'f629388955bc407f8b11d8b7ca168086',
4342+ u'interface': u'admin',
4343+ u'region': u'RegionOne',
4344+ u'region_id': u'RegionOne',
4345+ u'url': u'http://10.5.5.223:9312'}]}
4346+
4347+ Note, that an added complication is that the order of admin, public,
4348+ internal against 'interface' in each region.
4349+
4350+ Thus, the function sorts the expected and actual lists using the
4351+ interface key as a sort key, prior to the comparison.
4352+ """
4353+ self.log.debug('Validating v3 service catalog endpoint data...')
4354+ self.log.debug('actual: {}'.format(repr(actual)))
4355+ for k, v in six.iteritems(expected):
4356+ if k in actual:
4357+ l_expected = sorted(v, key=lambda x: x['interface'])
4358+ l_actual = sorted(actual[k], key=lambda x: x['interface'])
4359+ if len(l_actual) != len(l_expected):
4360+ return ("endpoint {} has differing number of interfaces "
4361+ " - expected({}), actual({})"
4362+ .format(k, len(l_expected), len(l_actual)))
4363+ for i_expected, i_actual in zip(l_expected, l_actual):
4364+ self.log.debug("checking interface {}"
4365+ .format(i_expected['interface']))
4366+ ret = self._validate_dict_data(i_expected, i_actual)
4367+ if ret:
4368+ return self.endpoint_error(k, ret)
4369+ else:
4370+ return "endpoint {} does not exist".format(k)
4371+ return ret
4372+
4373+ def validate_tenant_data(self, expected, actual):
4374+ """Validate tenant data.
4375+
4376+ Validate a list of actual tenant data vs list of expected tenant
4377+ data.
4378+ """
4379+ self.log.debug('Validating tenant data...')
4380+ self.log.debug('actual: {}'.format(repr(actual)))
4381+ for e in expected:
4382+ found = False
4383+ for act in actual:
4384+ a = {'enabled': act.enabled, 'description': act.description,
4385+ 'name': act.name, 'id': act.id}
4386+ if e['name'] == a['name']:
4387+ found = True
4388+ ret = self._validate_dict_data(e, a)
4389+ if ret:
4390+ return "unexpected tenant data - {}".format(ret)
4391+ if not found:
4392+ return "tenant {} does not exist".format(e['name'])
4393+ return ret
4394+
4395+ def validate_role_data(self, expected, actual):
4396+ """Validate role data.
4397+
4398+ Validate a list of actual role data vs a list of expected role
4399+ data.
4400+ """
4401+ self.log.debug('Validating role data...')
4402+ self.log.debug('actual: {}'.format(repr(actual)))
4403+ for e in expected:
4404+ found = False
4405+ for act in actual:
4406+ a = {'name': act.name, 'id': act.id}
4407+ if e['name'] == a['name']:
4408+ found = True
4409+ ret = self._validate_dict_data(e, a)
4410+ if ret:
4411+ return "unexpected role data - {}".format(ret)
4412+ if not found:
4413+ return "role {} does not exist".format(e['name'])
4414+ return ret
4415+
4416+ def validate_user_data(self, expected, actual, api_version=None):
4417+ """Validate user data.
4418+
4419+ Validate a list of actual user data vs a list of expected user
4420+ data.
4421+ """
4422+ self.log.debug('Validating user data...')
4423+ self.log.debug('actual: {}'.format(repr(actual)))
4424+ for e in expected:
4425+ found = False
4426+ for act in actual:
4427+ if e['name'] == act.name:
4428+ a = {'enabled': act.enabled, 'name': act.name,
4429+ 'email': act.email, 'id': act.id}
4430+ if api_version == 3:
4431+ a['default_project_id'] = getattr(act,
4432+ 'default_project_id',
4433+ 'none')
4434+ else:
4435+ a['tenantId'] = act.tenantId
4436+ found = True
4437+ ret = self._validate_dict_data(e, a)
4438+ if ret:
4439+ return "unexpected user data - {}".format(ret)
4440+ if not found:
4441+ return "user {} does not exist".format(e['name'])
4442+ return ret
4443+
4444+ def validate_flavor_data(self, expected, actual):
4445+ """Validate flavor data.
4446+
4447+ Validate a list of actual flavors vs a list of expected flavors.
4448+ """
4449+ self.log.debug('Validating flavor data...')
4450+ self.log.debug('actual: {}'.format(repr(actual)))
4451+ act = [a.name for a in actual]
4452+ return self._validate_list_data(expected, act)
4453+
4454+ def tenant_exists(self, keystone, tenant):
4455+ """Return True if tenant exists."""
4456+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
4457+ return tenant in [t.name for t in keystone.tenants.list()]
4458+
4459+ def authenticate_cinder_admin(self, keystone_sentry, username,
4460+ password, tenant):
4461+ """Authenticates admin user with cinder."""
4462+ # NOTE(beisner): cinder python client doesn't accept tokens.
4463+ service_ip = \
4464+ keystone_sentry.relation('shared-db',
4465+ 'mysql:shared-db')['private-address']
4466+ ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
4467+ return cinder_client.Client(username, password, tenant, ept)
4468+
4469+ def authenticate_keystone_admin(self, keystone_sentry, user, password,
4470+ tenant=None, api_version=None,
4471+ keystone_ip=None):
4472+ """Authenticates admin user with the keystone admin endpoint."""
4473+ self.log.debug('Authenticating keystone admin...')
4474+ unit = keystone_sentry
4475+ if not keystone_ip:
4476+ keystone_ip = unit.relation('shared-db',
4477+ 'mysql:shared-db')['private-address']
4478+ base_ep = "http://{}:35357".format(keystone_ip.strip().decode('utf-8'))
4479+ if not api_version or api_version == 2:
4480+ ep = base_ep + "/v2.0"
4481+ return keystone_client.Client(username=user, password=password,
4482+ tenant_name=tenant, auth_url=ep)
4483+ else:
4484+ ep = base_ep + "/v3"
4485+ auth = keystone_id_v3.Password(
4486+ user_domain_name='admin_domain',
4487+ username=user,
4488+ password=password,
4489+ domain_name='admin_domain',
4490+ auth_url=ep,
4491+ )
4492+ sess = keystone_session.Session(auth=auth)
4493+ return keystone_client_v3.Client(session=sess)
4494+
4495+ def authenticate_keystone_user(self, keystone, user, password, tenant):
4496+ """Authenticates a regular user with the keystone public endpoint."""
4497+ self.log.debug('Authenticating keystone user ({})...'.format(user))
4498+ ep = keystone.service_catalog.url_for(service_type='identity',
4499+ endpoint_type='publicURL')
4500+ return keystone_client.Client(username=user, password=password,
4501+ tenant_name=tenant, auth_url=ep)
4502+
4503+ def authenticate_glance_admin(self, keystone):
4504+ """Authenticates admin user with glance."""
4505+ self.log.debug('Authenticating glance admin...')
4506+ ep = keystone.service_catalog.url_for(service_type='image',
4507+ endpoint_type='adminURL')
4508+ return glance_client.Client(ep, token=keystone.auth_token)
4509+
4510+ def authenticate_heat_admin(self, keystone):
4511+ """Authenticates the admin user with heat."""
4512+ self.log.debug('Authenticating heat admin...')
4513+ ep = keystone.service_catalog.url_for(service_type='orchestration',
4514+ endpoint_type='publicURL')
4515+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
4516+
4517+ def authenticate_nova_user(self, keystone, user, password, tenant):
4518+ """Authenticates a regular user with nova-api."""
4519+ self.log.debug('Authenticating nova user ({})...'.format(user))
4520+ ep = keystone.service_catalog.url_for(service_type='identity',
4521+ endpoint_type='publicURL')
4522+ return nova_client.Client(NOVA_CLIENT_VERSION,
4523+ username=user, api_key=password,
4524+ project_id=tenant, auth_url=ep)
4525+
4526+ def authenticate_swift_user(self, keystone, user, password, tenant):
4527+ """Authenticates a regular user with swift api."""
4528+ self.log.debug('Authenticating swift user ({})...'.format(user))
4529+ ep = keystone.service_catalog.url_for(service_type='identity',
4530+ endpoint_type='publicURL')
4531+ return swiftclient.Connection(authurl=ep,
4532+ user=user,
4533+ key=password,
4534+ tenant_name=tenant,
4535+ auth_version='2.0')
4536+
4537+ def create_cirros_image(self, glance, image_name):
4538+ """Download the latest cirros image and upload it to glance,
4539+ validate and return a resource pointer.
4540+
4541+ :param glance: pointer to authenticated glance connection
4542+ :param image_name: display name for new image
4543+ :returns: glance image pointer
4544+ """
4545+ self.log.debug('Creating glance cirros image '
4546+ '({})...'.format(image_name))
4547+
4548+ # Download cirros image
4549+ http_proxy = os.getenv('AMULET_HTTP_PROXY')
4550+ self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
4551+ if http_proxy:
4552+ proxies = {'http': http_proxy}
4553+ opener = urllib.FancyURLopener(proxies)
4554+ else:
4555+ opener = urllib.FancyURLopener()
4556+
4557+ f = opener.open('http://download.cirros-cloud.net/version/released')
4558+ version = f.read().strip()
4559+ cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
4560+ local_path = os.path.join('tests', cirros_img)
4561+
4562+ if not os.path.exists(local_path):
4563+ cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
4564+ version, cirros_img)
4565+ opener.retrieve(cirros_url, local_path)
4566+ f.close()
4567+
4568+ # Create glance image
4569+ with open(local_path) as f:
4570+ image = glance.images.create(name=image_name, is_public=True,
4571+ disk_format='qcow2',
4572+ container_format='bare', data=f)
4573+
4574+ # Wait for image to reach active status
4575+ img_id = image.id
4576+ ret = self.resource_reaches_status(glance.images, img_id,
4577+ expected_stat='active',
4578+ msg='Image status wait')
4579+ if not ret:
4580+ msg = 'Glance image failed to reach expected state.'
4581+ amulet.raise_status(amulet.FAIL, msg=msg)
4582+
4583+ # Re-validate new image
4584+ self.log.debug('Validating image attributes...')
4585+ val_img_name = glance.images.get(img_id).name
4586+ val_img_stat = glance.images.get(img_id).status
4587+ val_img_pub = glance.images.get(img_id).is_public
4588+ val_img_cfmt = glance.images.get(img_id).container_format
4589+ val_img_dfmt = glance.images.get(img_id).disk_format
4590+ msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
4591+ 'container fmt:{} disk fmt:{}'.format(
4592+ val_img_name, val_img_pub, img_id,
4593+ val_img_stat, val_img_cfmt, val_img_dfmt))
4594+
4595+ if val_img_name == image_name and val_img_stat == 'active' \
4596+ and val_img_pub is True and val_img_cfmt == 'bare' \
4597+ and val_img_dfmt == 'qcow2':
4598+ self.log.debug(msg_attr)
4599+ else:
4600+ msg = ('Volume validation failed, {}'.format(msg_attr))
4601+ amulet.raise_status(amulet.FAIL, msg=msg)
4602+
4603+ return image
4604+
4605+ def delete_image(self, glance, image):
4606+ """Delete the specified image."""
4607+
4608+ # /!\ DEPRECATION WARNING
4609+ self.log.warn('/!\\ DEPRECATION WARNING: use '
4610+ 'delete_resource instead of delete_image.')
4611+ self.log.debug('Deleting glance image ({})...'.format(image))
4612+ return self.delete_resource(glance.images, image, msg='glance image')
4613+
4614+ def create_instance(self, nova, image_name, instance_name, flavor):
4615+ """Create the specified instance."""
4616+ self.log.debug('Creating instance '
4617+ '({}|{}|{})'.format(instance_name, image_name, flavor))
4618+ image = nova.images.find(name=image_name)
4619+ flavor = nova.flavors.find(name=flavor)
4620+ instance = nova.servers.create(name=instance_name, image=image,
4621+ flavor=flavor)
4622+
4623+ count = 1
4624+ status = instance.status
4625+ while status != 'ACTIVE' and count < 60:
4626+ time.sleep(3)
4627+ instance = nova.servers.get(instance.id)
4628+ status = instance.status
4629+ self.log.debug('instance status: {}'.format(status))
4630+ count += 1
4631+
4632+ if status != 'ACTIVE':
4633+ self.log.error('instance creation timed out')
4634+ return None
4635+
4636+ return instance
4637+
4638+ def delete_instance(self, nova, instance):
4639+ """Delete the specified instance."""
4640+
4641+ # /!\ DEPRECATION WARNING
4642+ self.log.warn('/!\\ DEPRECATION WARNING: use '
4643+ 'delete_resource instead of delete_instance.')
4644+ self.log.debug('Deleting instance ({})...'.format(instance))
4645+ return self.delete_resource(nova.servers, instance,
4646+ msg='nova instance')
4647+
4648+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
4649+ """Create a new keypair, or return pointer if it already exists."""
4650+ try:
4651+ _keypair = nova.keypairs.get(keypair_name)
4652+ self.log.debug('Keypair ({}) already exists, '
4653+ 'using it.'.format(keypair_name))
4654+ return _keypair
4655+ except:
4656+ self.log.debug('Keypair ({}) does not exist, '
4657+ 'creating it.'.format(keypair_name))
4658+
4659+ _keypair = nova.keypairs.create(name=keypair_name)
4660+ return _keypair
4661+
4662+ def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
4663+ img_id=None, src_vol_id=None, snap_id=None):
4664+ """Create cinder volume, optionally from a glance image, OR
4665+ optionally as a clone of an existing volume, OR optionally
4666+ from a snapshot. Wait for the new volume status to reach
4667+ the expected status, validate and return a resource pointer.
4668+
4669+ :param vol_name: cinder volume display name
4670+ :param vol_size: size in gigabytes
4671+ :param img_id: optional glance image id
4672+ :param src_vol_id: optional source volume id to clone
4673+ :param snap_id: optional snapshot id to use
4674+ :returns: cinder volume pointer
4675+ """
4676+ # Handle parameter input and avoid impossible combinations
4677+ if img_id and not src_vol_id and not snap_id:
4678+ # Create volume from image
4679+ self.log.debug('Creating cinder volume from glance image...')
4680+ bootable = 'true'
4681+ elif src_vol_id and not img_id and not snap_id:
4682+ # Clone an existing volume
4683+ self.log.debug('Cloning cinder volume...')
4684+ bootable = cinder.volumes.get(src_vol_id).bootable
4685+ elif snap_id and not src_vol_id and not img_id:
4686+ # Create volume from snapshot
4687+ self.log.debug('Creating cinder volume from snapshot...')
4688+ snap = cinder.volume_snapshots.find(id=snap_id)
4689+ vol_size = snap.size
4690+ snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
4691+ bootable = cinder.volumes.get(snap_vol_id).bootable
4692+ elif not img_id and not src_vol_id and not snap_id:
4693+ # Create volume
4694+ self.log.debug('Creating cinder volume...')
4695+ bootable = 'false'
4696+ else:
4697+ # Impossible combination of parameters
4698+ msg = ('Invalid method use - name:{} size:{} img_id:{} '
4699+ 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
4700+ img_id, src_vol_id,
4701+ snap_id))
4702+ amulet.raise_status(amulet.FAIL, msg=msg)
4703+
4704+ # Create new volume
4705+ try:
4706+ vol_new = cinder.volumes.create(display_name=vol_name,
4707+ imageRef=img_id,
4708+ size=vol_size,
4709+ source_volid=src_vol_id,
4710+ snapshot_id=snap_id)
4711+ vol_id = vol_new.id
4712+ except Exception as e:
4713+ msg = 'Failed to create volume: {}'.format(e)
4714+ amulet.raise_status(amulet.FAIL, msg=msg)
4715+
4716+ # Wait for volume to reach available status
4717+ ret = self.resource_reaches_status(cinder.volumes, vol_id,
4718+ expected_stat="available",
4719+ msg="Volume status wait")
4720+ if not ret:
4721+ msg = 'Cinder volume failed to reach expected state.'
4722+ amulet.raise_status(amulet.FAIL, msg=msg)
4723+
4724+ # Re-validate new volume
4725+ self.log.debug('Validating volume attributes...')
4726+ val_vol_name = cinder.volumes.get(vol_id).display_name
4727+ val_vol_boot = cinder.volumes.get(vol_id).bootable
4728+ val_vol_stat = cinder.volumes.get(vol_id).status
4729+ val_vol_size = cinder.volumes.get(vol_id).size
4730+ msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
4731+ '{} size:{}'.format(val_vol_name, vol_id,
4732+ val_vol_stat, val_vol_boot,
4733+ val_vol_size))
4734+
4735+ if val_vol_boot == bootable and val_vol_stat == 'available' \
4736+ and val_vol_name == vol_name and val_vol_size == vol_size:
4737+ self.log.debug(msg_attr)
4738+ else:
4739+ msg = ('Volume validation failed, {}'.format(msg_attr))
4740+ amulet.raise_status(amulet.FAIL, msg=msg)
4741+
4742+ return vol_new
4743+
4744+ def delete_resource(self, resource, resource_id,
4745+ msg="resource", max_wait=120):
4746+ """Delete one openstack resource, such as one instance, keypair,
4747+ image, volume, stack, etc., and confirm deletion within max wait time.
4748+
4749+ :param resource: pointer to os resource type, ex:glance_client.images
4750+ :param resource_id: unique name or id for the openstack resource
4751+ :param msg: text to identify purpose in logging
4752+ :param max_wait: maximum wait time in seconds
4753+ :returns: True if successful, otherwise False
4754+ """
4755+ self.log.debug('Deleting OpenStack resource '
4756+ '{} ({})'.format(resource_id, msg))
4757+ num_before = len(list(resource.list()))
4758+ resource.delete(resource_id)
4759+
4760+ tries = 0
4761+ num_after = len(list(resource.list()))
4762+ while num_after != (num_before - 1) and tries < (max_wait / 4):
4763+ self.log.debug('{} delete check: '
4764+ '{} [{}:{}] {}'.format(msg, tries,
4765+ num_before,
4766+ num_after,
4767+ resource_id))
4768+ time.sleep(4)
4769+ num_after = len(list(resource.list()))
4770+ tries += 1
4771+
4772+ self.log.debug('{}: expected, actual count = {}, '
4773+ '{}'.format(msg, num_before - 1, num_after))
4774+
4775+ if num_after == (num_before - 1):
4776+ return True
4777+ else:
4778+ self.log.error('{} delete timed out'.format(msg))
4779+ return False
4780+
4781+ def resource_reaches_status(self, resource, resource_id,
4782+ expected_stat='available',
4783+ msg='resource', max_wait=120):
4784+ """Wait for an openstack resources status to reach an
4785+ expected status within a specified time. Useful to confirm that
4786+ nova instances, cinder vols, snapshots, glance images, heat stacks
4787+ and other resources eventually reach the expected status.
4788+
4789+ :param resource: pointer to os resource type, ex: heat_client.stacks
4790+ :param resource_id: unique id for the openstack resource
4791+ :param expected_stat: status to expect resource to reach
4792+ :param msg: text to identify purpose in logging
4793+ :param max_wait: maximum wait time in seconds
4794+ :returns: True if successful, False if status is not reached
4795+ """
4796+
4797+ tries = 0
4798+ resource_stat = resource.get(resource_id).status
4799+ while resource_stat != expected_stat and tries < (max_wait / 4):
4800+ self.log.debug('{} status check: '
4801+ '{} [{}:{}] {}'.format(msg, tries,
4802+ resource_stat,
4803+ expected_stat,
4804+ resource_id))
4805+ time.sleep(4)
4806+ resource_stat = resource.get(resource_id).status
4807+ tries += 1
4808+
4809+ self.log.debug('{}: expected, actual status = {}, '
4810+ '{}'.format(msg, resource_stat, expected_stat))
4811+
4812+ if resource_stat == expected_stat:
4813+ return True
4814+ else:
4815+ self.log.debug('{} never reached expected status: '
4816+ '{}'.format(resource_id, expected_stat))
4817+ return False
4818+
4819+ def get_ceph_osd_id_cmd(self, index):
4820+ """Produce a shell command that will return a ceph-osd id."""
4821+ return ("`initctl list | grep 'ceph-osd ' | "
4822+ "awk 'NR=={} {{ print $2 }}' | "
4823+ "grep -o '[0-9]*'`".format(index + 1))
4824+
4825+ def get_ceph_pools(self, sentry_unit):
4826+ """Return a dict of ceph pools from a single ceph unit, with
4827+ pool name as keys, pool id as vals."""
4828+ pools = {}
4829+ cmd = 'sudo ceph osd lspools'
4830+ output, code = sentry_unit.run(cmd)
4831+ if code != 0:
4832+ msg = ('{} `{}` returned {} '
4833+ '{}'.format(sentry_unit.info['unit_name'],
4834+ cmd, code, output))
4835+ amulet.raise_status(amulet.FAIL, msg=msg)
4836+
4837+ # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
4838+ for pool in str(output).split(','):
4839+ pool_id_name = pool.split(' ')
4840+ if len(pool_id_name) == 2:
4841+ pool_id = pool_id_name[0]
4842+ pool_name = pool_id_name[1]
4843+ pools[pool_name] = int(pool_id)
4844+
4845+ self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
4846+ pools))
4847+ return pools
4848+
4849+ def get_ceph_df(self, sentry_unit):
4850+ """Return dict of ceph df json output, including ceph pool state.
4851+
4852+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
4853+ :returns: Dict of ceph df output
4854+ """
4855+ cmd = 'sudo ceph df --format=json'
4856+ output, code = sentry_unit.run(cmd)
4857+ if code != 0:
4858+ msg = ('{} `{}` returned {} '
4859+ '{}'.format(sentry_unit.info['unit_name'],
4860+ cmd, code, output))
4861+ amulet.raise_status(amulet.FAIL, msg=msg)
4862+ return json.loads(output)
4863+
4864+ def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
4865+ """Take a sample of attributes of a ceph pool, returning ceph
4866+ pool name, object count and disk space used for the specified
4867+ pool ID number.
4868+
4869+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
4870+ :param pool_id: Ceph pool ID
4871+ :returns: List of pool name, object count, kb disk space used
4872+ """
4873+ df = self.get_ceph_df(sentry_unit)
4874+ pool_name = df['pools'][pool_id]['name']
4875+ obj_count = df['pools'][pool_id]['stats']['objects']
4876+ kb_used = df['pools'][pool_id]['stats']['kb_used']
4877+ self.log.debug('Ceph {} pool (ID {}): {} objects, '
4878+ '{} kb used'.format(pool_name, pool_id,
4879+ obj_count, kb_used))
4880+ return pool_name, obj_count, kb_used
4881+
4882+ def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
4883+ """Validate ceph pool samples taken over time, such as pool
4884+ object counts or pool kb used, before adding, after adding, and
4885+ after deleting items which affect those pool attributes. The
4886+ 2nd element is expected to be greater than the 1st; 3rd is expected
4887+ to be less than the 2nd.
4888+
4889+ :param samples: List containing 3 data samples
4890+ :param sample_type: String for logging and usage context
4891+ :returns: None if successful, Failure message otherwise
4892+ """
4893+ original, created, deleted = range(3)
4894+ if samples[created] <= samples[original] or \
4895+ samples[deleted] >= samples[created]:
4896+ return ('Ceph {} samples ({}) '
4897+ 'unexpected.'.format(sample_type, samples))
4898+ else:
4899+ self.log.debug('Ceph {} samples (OK): '
4900+ '{}'.format(sample_type, samples))
4901+ return None
4902+
4903+ # rabbitmq/amqp specific helpers:
4904+
4905+ def rmq_wait_for_cluster(self, deployment, init_sleep=15, timeout=1200):
4906+ """Wait for rmq units extended status to show cluster readiness,
4907+ after an optional initial sleep period. Initial sleep is likely
4908+ necessary to be effective following a config change, as status
4909+ message may not instantly update to non-ready."""
4910+
4911+ if init_sleep:
4912+ time.sleep(init_sleep)
4913+
4914+ message = re.compile('^Unit is ready and clustered$')
4915+ deployment._auto_wait_for_status(message=message,
4916+ timeout=timeout,
4917+ include_only=['rabbitmq-server'])
4918+
4919+ def add_rmq_test_user(self, sentry_units,
4920+ username="testuser1", password="changeme"):
4921+ """Add a test user via the first rmq juju unit, check connection as
4922+ the new user against all sentry units.
4923+
4924+ :param sentry_units: list of sentry unit pointers
4925+ :param username: amqp user name, default to testuser1
4926+ :param password: amqp user password
4927+ :returns: None if successful. Raise on error.
4928+ """
4929+ self.log.debug('Adding rmq user ({})...'.format(username))
4930+
4931+ # Check that user does not already exist
4932+ cmd_user_list = 'rabbitmqctl list_users'
4933+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
4934+ if username in output:
4935+ self.log.warning('User ({}) already exists, returning '
4936+ 'gracefully.'.format(username))
4937+ return
4938+
4939+ perms = '".*" ".*" ".*"'
4940+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
4941+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
4942+
4943+ # Add user via first unit
4944+ for cmd in cmds:
4945+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
4946+
4947+ # Check connection against the other sentry_units
4948+ self.log.debug('Checking user connect against units...')
4949+ for sentry_unit in sentry_units:
4950+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
4951+ username=username,
4952+ password=password)
4953+ connection.close()
4954+
4955+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
4956+ """Delete a rabbitmq user via the first rmq juju unit.
4957+
4958+ :param sentry_units: list of sentry unit pointers
4959+ :param username: amqp user name, default to testuser1
4960+ :param password: amqp user password
4961+ :returns: None if successful or no such user.
4962+ """
4963+ self.log.debug('Deleting rmq user ({})...'.format(username))
4964+
4965+ # Check that the user exists
4966+ cmd_user_list = 'rabbitmqctl list_users'
4967+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
4968+
4969+ if username not in output:
4970+ self.log.warning('User ({}) does not exist, returning '
4971+ 'gracefully.'.format(username))
4972+ return
4973+
4974+ # Delete the user
4975+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
4976+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
4977+
4978+ def get_rmq_cluster_status(self, sentry_unit):
4979+ """Execute rabbitmq cluster status command on a unit and return
4980+ the full output.
4981+
4982+ :param unit: sentry unit
4983+ :returns: String containing console output of cluster status command
4984+ """
4985+ cmd = 'rabbitmqctl cluster_status'
4986+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
4987+ self.log.debug('{} cluster_status:\n{}'.format(
4988+ sentry_unit.info['unit_name'], output))
4989+ return str(output)
4990+
4991+ def get_rmq_cluster_running_nodes(self, sentry_unit):
4992+ """Parse rabbitmqctl cluster_status output string, return list of
4993+ running rabbitmq cluster nodes.
4994+
4995+ :param unit: sentry unit
4996+ :returns: List containing node names of running nodes
4997+ """
4998+ # NOTE(beisner): rabbitmqctl cluster_status output is not
4999+ # json-parsable, do string chop foo, then json.loads that.
5000+ str_stat = self.get_rmq_cluster_status(sentry_unit)
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: