Merge lp:~ajkavanagh/charm-helpers/ppc64le_proposed_fix_plus_refactor into lp:charm-helpers

Proposed by Alex Kavanagh
Status: Superseded
Proposed branch: lp:~ajkavanagh/charm-helpers/ppc64le_proposed_fix_plus_refactor
Merge into: lp:charm-helpers
Diff against target: 3645 lines (+1602/-1375)
11 files modified
Makefile (+1/-0)
charmhelpers/__init__.py (+61/-0)
charmhelpers/contrib/openstack/utils.py (+78/-142)
charmhelpers/fetch/__init__.py (+17/-9)
charmhelpers/fetch/centos.py (+1/-1)
charmhelpers/fetch/ubuntu.py (+244/-57)
tests/contrib/openstack/test_openstack_utils.py (+163/-112)
tests/contrib/openstack/test_os_utils.py (+1/-1)
tests/fetch/test_fetch.py (+9/-1053)
tests/fetch/test_fetch_centos.py (+315/-0)
tests/fetch/test_fetch_ubuntu.py (+712/-0)
To merge this branch: bzr merge lp:~ajkavanagh/charm-helpers/ppc64le_proposed_fix_plus_refactor
Reviewer Review Type Date Requested Status
Ryan Beisner (community) Needs Fixing
James Page Pending
Review via email: mp+303495@code.launchpad.net

This proposal has been superseded by a proposal from 2017-04-10.

Description of the change

  Refactor configure_installation_source() and add_source() + fix bug 1611134

  Bug#1611134: fetch helper assumes amd64 arch (cannot use with ports such as ppc64el)

  The refactor is because the two functions do almost exactly the same thing in two
  different places, and the core function does OpenStack sources, which ought to be in
  the contrib/openstack module set.

  After the refactor, the bug fix was implemented on the 'proposed' source to pick the
  correct ports specification for ppc64le:

  deb http://ports.ubuntu.com/ubuntu-ports {}-proposed main universe multiverse restricted

  where {} is the ubuntu release.

To post a comment you must log in.
622. By Alex Kavanagh

Merge in r619 in prep for r620 which is very different

623. By Alex Kavanagh

Reworking of the ports_fetch code and the dbuliga CentOS support

The CentOS support changed the same code as the fetch code for
ports (i.e. other architectures that x86_64). This merge changes
the way the testing works for the CentOS/Ubuntu dbuliga work by
moving the specific tests for each distro into their own files
and keeping a common file. This has simplified the testing, and
also fixed a few tests.

Also, this merge tidies up quite a few PEP8 issues with the library.

624. By Alex Kavanagh

Merge main branch to 626

625. By Alex Kavanagh

Merge/resolve conflicts with main 627

626. By Alex Kavanagh

Add back in DEBUG to c/o/utils.py

627. By Alex Kavanagh

Merge/resolve conflicts to main branch 628

628. By Alex Kavanagh

Merge to main branch r630

629. By Alex Kavanagh

Merge/resolve conflicts with main branch r631

630. By Alex Kavanagh

Merge to main branch r647

631. By Alex Kavanagh

Merge/resolve conflicts with main branch r648

632. By Alex Kavanagh

Merge up to main branch r657

633. By Alex Kavanagh

Merge/resolve conflicts for main branch r658

The issue is that the different architecture ports has changed where the
cloud pockets are represented in the code, and simplified them slightly.
This merge resolves the conflicts with r658 and also fixes a bug with
'zesty' in the list of OpenStack releases (which confuses them with
Ubuntu release names).

634. By Alex Kavanagh

Merge to main branch r660

635. By Alex Kavanagh

Merge r661 from main branch + resolve conflicts

636. By Alex Kavanagh

Merge & resolve conflicts up to main branch r663

This is because the ports refactor has removed one set of (obsolete)
cloud pockets.

637. By Alex Kavanagh

Merge up to r671 from main devel branch

Revision history for this message
Ryan Beisner (1chb1n) wrote :

As you might expect, there are a number of (what seem to be small) merge conflicts with Trunk by now. But I merged this into a local copy of Trunk, took a stab at resolving those conflicts, and found the following issues:

1. Some of the added tests appear to assume that the tests will be executed from a Xenial machine, which will not always be the case. It might be best to base the tests on Xenial (LTS) regardless of the release of the developer's machine which is executing the unit tests.

2. There are a number of "local variable 'env' referenced before assignment" errors in the tests which cover _run_apt_command. This may have been missed with the rebase against that big centos change. This is causing unit and lint check failures.
| charmhelpers/fetch/ubuntu.py:642:57: F821 undefined name 'env'

3. There has been a recent change in the apt retry logic which needs to be reconciled on a rebase. I didn't fully (or carefully) resolve those conflicts in my WIP as I needed to plow on for a separate test case.

http://bazaar.launchpad.net/~charm-helpers/charm-helpers/devel/revision/705

This is causing:
| charmhelpers/fetch/ubuntu.py:645:34: F821 undefined name 'APT_NO_LOCK_RETRY_COUNT'
| charmhelpers/fetch/ubuntu.py:649:29: F821 undefined name 'APT_NO_LOCK_RETRY_DELAY'
| charmhelpers/fetch/ubuntu.py:650:28: F821 undefined name 'APT_NO_LOCK_RETRY_DELAY'

See nosetests results:
 - http://paste.ubuntu.com/24278293/

My temporary trunk merge branch is here (NOT intended to land or to be proposed as my merge conflict resolution approach was quite fast and furious):
 - https://code.launchpad.net/~1chb1n/charm-helpers/lp1611134

I synced it into the nova-compute charm as a WIP TEST here (also NOT for reals):
 - https://github.com/ryan-beisner/charm-nova-compute-wip

And that is what I'm using on arm64 at the moment (!), because I need to consume arm64 ports. I'll chime back in here with how that goes. :-)

review: Needs Fixing
Revision history for this message
Ryan Beisner (1chb1n) wrote :

A test deployment on arm64 xenial-mitaka, with openstack-origin set to "distro-proposed" failed:

2017-03-30 03:05:04 DEBUG juju-log No hardening applied to 'install'
2017-03-30 03:05:05 INFO juju-log DEPRECATION WARNING: Function configure_installation_source is being removed on/around 2017-07 : use charmhelpers.fetch.add_source() instead.
2017-03-30 03:05:05 ERROR juju-log FATAL ERROR: Arch aarch64 not supported for (distro-)proposed
2017-03-30 03:05:05 ERROR juju.worker.uniter.operation runhook.go:107 hook "install" failed: exit status 1

review: Needs Fixing
Revision history for this message
Ryan Beisner (1chb1n) wrote :

FYI, this (in my WIP temp branch) resolved the failure in my previous comment:
 - http://bazaar.launchpad.net/~1chb1n/charm-helpers/lp1611134/revision/729

Revision history for this message
Alex Kavanagh (ajkavanagh) wrote :

Hi Ryan

Thanks for taking a look at it. I'm going to swing back to this shortly and have another go at bring it up to date. I'll also test it against the glance charm to check that it does indeed work.

Thanks
Alex.

638. By Alex Kavanagh

Remove redundant plugins() funct from c.f.ubuntu.py

639. By Alex Kavanagh

Remove duplicated code in fetch module(s)

640. By Alex Kavanagh

bring upto date to r704

641. By Alex Kavanagh

Merge in r705 (retry-add-apt-repository)

This needed a little work as the fetch code in this branch is very different
and reorganised from the dev branch.

642. By Alex Kavanagh

Merge up to r731

643. By Alex Kavanagh

Add ARM 64 arch to proposed pockets for OpenStack

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2017-01-25 17:11:37 +0000
3+++ Makefile 2017-04-10 18:12:16 +0000
4@@ -27,6 +27,7 @@
5 -python setup.py clean
6 rm -rf build/ MANIFEST
7 find . -name '*.pyc' -delete
8+ find . -name '__pycache__' -delete
9 rm -rf dist/*
10 rm -rf .venv
11 rm -rf .venv3
12
13=== modified file 'charmhelpers/__init__.py'
14--- charmhelpers/__init__.py 2016-07-06 14:41:05 +0000
15+++ charmhelpers/__init__.py 2017-04-10 18:12:16 +0000
16@@ -14,6 +14,11 @@
17
18 # Bootstrap charm-helpers, installing its dependencies if necessary using
19 # only standard libraries.
20+from __future__ import print_function
21+from __future__ import absolute_import
22+
23+import functools
24+import inspect
25 import subprocess
26 import sys
27
28@@ -34,3 +39,59 @@
29 else:
30 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
31 import yaml # flake8: noqa
32+
33+
34+# Holds a list of mapping of mangled function names that have been deprecated
35+# using the @deprecate decorator below. This is so that the warning is only
36+# printed once for each usage of the function.
37+__deprecated_functions = {}
38+
39+
40+def deprecate(warning, date=None, log=None):
41+ """Add a deprecation warning the first time the function is used.
42+ The date, which is a string in semi-ISO8660 format indicate the year-month
43+ that the function is officially going to be removed.
44+
45+ usage:
46+
47+ @deprecate('use core/fetch/add_source() instead', '2017-04')
48+ def contributed_add_source_thing(...):
49+ ...
50+
51+ And it then prints to the log ONCE that the function is deprecated.
52+ The reason for passing the logging function (log) is so that hookenv.log
53+ can be used for a charm if needed.
54+
55+ :param warning: String to indicat where it has moved ot.
56+ :param date: optional sting, in YYYY-MM format to indicate when the
57+ function will definitely (probably) be removed.
58+ :param log: The log function to call to log. If not, logs to stdout
59+ """
60+ def wrap(f):
61+
62+ @functools.wraps(f)
63+ def wrapped_f(*args, **kwargs):
64+ try:
65+ module = inspect.getmodule(f)
66+ file = inspect.getsourcefile(f)
67+ lines = inspect.getsourcelines(f)
68+ f_name = "{}-{}-{}..{}-{}".format(
69+ module.__name__, file, lines[0], lines[-1], f.__name__)
70+ except (IOError, TypeError):
71+ # assume it was local, so just use the name of the function
72+ f_name = f.__name__
73+ if f_name not in __deprecated_functions:
74+ __deprecated_functions[f_name] = True
75+ s = "DEPRECATION WARNING: Function {} is being removed".format(
76+ f.__name__)
77+ if date:
78+ s = "{} on/around {}".format(s, date)
79+ if warning:
80+ s = "{} : {}".format(s, warning)
81+ if log:
82+ log(s)
83+ else:
84+ print(s)
85+ return f(*args, **kwargs)
86+ return wrapped_f
87+ return wrap
88
89=== modified file 'charmhelpers/contrib/openstack/utils.py'
90--- charmhelpers/contrib/openstack/utils.py 2017-03-24 16:50:31 +0000
91+++ charmhelpers/contrib/openstack/utils.py 2017-04-10 18:12:16 +0000
92@@ -26,11 +26,12 @@
93 import shutil
94
95 import six
96-import tempfile
97 import traceback
98 import uuid
99 import yaml
100
101+from charmhelpers import deprecate
102+
103 from charmhelpers.contrib.network import ip
104
105 from charmhelpers.core import unitdata
106@@ -41,7 +42,6 @@
107 config,
108 log as juju_log,
109 charm_dir,
110- DEBUG,
111 INFO,
112 ERROR,
113 related_units,
114@@ -82,9 +82,12 @@
115 restart_on_change_helper,
116 )
117 from charmhelpers.fetch import (
118- apt_install,
119 apt_cache,
120 install_remote,
121+ import_key as fetch_import_key,
122+ add_source as fetch_add_source,
123+ SourceConfigError,
124+ GPGKeyError,
125 get_upstream_version
126 )
127 from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
128@@ -436,13 +439,14 @@
129 # error_out(e)
130
131
132-os_rel = None
133+# Module local cache variable for the os_release.
134+_os_rel = None
135
136
137 def reset_os_release():
138 '''Unset the cached os_release version'''
139- global os_rel
140- os_rel = None
141+ global _os_rel
142+ _os_rel = None
143
144
145 def os_release(package, base='essex', reset_cache=False):
146@@ -456,144 +460,77 @@
147 the installation source, the earliest release supported by the charm should
148 be returned.
149 '''
150- global os_rel
151+ global _os_rel
152 if reset_cache:
153 reset_os_release()
154- if os_rel:
155- return os_rel
156- os_rel = (git_os_codename_install_source(config('openstack-origin-git')) or
157- get_os_codename_package(package, fatal=False) or
158- get_os_codename_install_source(config('openstack-origin')) or
159- base)
160- return os_rel
161-
162-
163+ if _os_rel:
164+ return _os_rel
165+ _os_rel = (
166+ git_os_codename_install_source(config('openstack-origin-git')) or
167+ get_os_codename_package(package, fatal=False) or
168+ get_os_codename_install_source(config('openstack-origin')) or
169+ base)
170+ return _os_rel
171+
172+
173+@deprecate("moved to charmhelpers.fetch.import_key()", "2017-07", log=juju_log)
174 def import_key(keyid):
175- key = keyid.strip()
176- if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
177- key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
178- juju_log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
179- juju_log("Importing ASCII Armor PGP key", level=DEBUG)
180- with tempfile.NamedTemporaryFile() as keyfile:
181- with open(keyfile.name, 'w') as fd:
182- fd.write(key)
183- fd.write("\n")
184-
185- cmd = ['apt-key', 'add', keyfile.name]
186- try:
187- subprocess.check_call(cmd)
188- except subprocess.CalledProcessError:
189- error_out("Error importing PGP key '%s'" % key)
190- else:
191- juju_log("PGP key found (looks like Radix64 format)", level=DEBUG)
192- juju_log("Importing PGP key from keyserver", level=DEBUG)
193- cmd = ['apt-key', 'adv', '--keyserver',
194- 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
195- try:
196- subprocess.check_call(cmd)
197- except subprocess.CalledProcessError:
198- error_out("Error importing PGP key '%s'" % key)
199-
200-
201-def get_source_and_pgp_key(input):
202- """Look for a pgp key ID or ascii-armor key in the given input."""
203- index = input.strip()
204- index = input.rfind('|')
205- if index < 0:
206- return input, None
207-
208- key = input[index + 1:].strip('|')
209- source = input[:index]
210- return source, key
211-
212-
213-def configure_installation_source(rel):
214- '''Configure apt installation source.'''
215- if rel == 'distro':
216- return
217- elif rel == 'distro-proposed':
218- ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
219- with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
220- f.write(DISTRO_PROPOSED % ubuntu_rel)
221- elif rel[:4] == "ppa:":
222- src, key = get_source_and_pgp_key(rel)
223- if key:
224- import_key(key)
225-
226- subprocess.check_call(["add-apt-repository", "-y", src])
227- elif rel[:3] == "deb":
228- src, key = get_source_and_pgp_key(rel)
229- if key:
230- import_key(key)
231-
232- with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
233- f.write(src)
234- elif rel[:6] == 'cloud:':
235- ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
236- rel = rel.split(':')[1]
237- u_rel = rel.split('-')[0]
238- ca_rel = rel.split('-')[1]
239-
240- if u_rel != ubuntu_rel:
241- e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
242- 'version (%s)' % (ca_rel, ubuntu_rel)
243- error_out(e)
244-
245- if 'staging' in ca_rel:
246- # staging is just a regular PPA.
247- os_rel = ca_rel.split('/')[0]
248- ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
249- cmd = 'add-apt-repository -y %s' % ppa
250- subprocess.check_call(cmd.split(' '))
251- return
252-
253- # map charm config options to actual archive pockets.
254- pockets = {
255- 'folsom': 'precise-updates/folsom',
256- 'folsom/updates': 'precise-updates/folsom',
257- 'folsom/proposed': 'precise-proposed/folsom',
258- 'grizzly': 'precise-updates/grizzly',
259- 'grizzly/updates': 'precise-updates/grizzly',
260- 'grizzly/proposed': 'precise-proposed/grizzly',
261- 'havana': 'precise-updates/havana',
262- 'havana/updates': 'precise-updates/havana',
263- 'havana/proposed': 'precise-proposed/havana',
264- 'icehouse': 'precise-updates/icehouse',
265- 'icehouse/updates': 'precise-updates/icehouse',
266- 'icehouse/proposed': 'precise-proposed/icehouse',
267- 'juno': 'trusty-updates/juno',
268- 'juno/updates': 'trusty-updates/juno',
269- 'juno/proposed': 'trusty-proposed/juno',
270- 'kilo': 'trusty-updates/kilo',
271- 'kilo/updates': 'trusty-updates/kilo',
272- 'kilo/proposed': 'trusty-proposed/kilo',
273- 'liberty': 'trusty-updates/liberty',
274- 'liberty/updates': 'trusty-updates/liberty',
275- 'liberty/proposed': 'trusty-proposed/liberty',
276- 'mitaka': 'trusty-updates/mitaka',
277- 'mitaka/updates': 'trusty-updates/mitaka',
278- 'mitaka/proposed': 'trusty-proposed/mitaka',
279- 'newton': 'xenial-updates/newton',
280- 'newton/updates': 'xenial-updates/newton',
281- 'newton/proposed': 'xenial-proposed/newton',
282- 'ocata': 'xenial-updates/ocata',
283- 'ocata/updates': 'xenial-updates/ocata',
284- 'ocata/proposed': 'xenial-proposed/ocata',
285- }
286-
287- try:
288- pocket = pockets[ca_rel]
289- except KeyError:
290- e = 'Invalid Cloud Archive release specified: %s' % rel
291- error_out(e)
292-
293- src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
294- apt_install('ubuntu-cloud-keyring', fatal=True)
295-
296- with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
297- f.write(src)
298- else:
299- error_out("Invalid openstack-release specified: %s" % rel)
300+ """Import a key, either ASCII armored, or a GPG key id.
301+
302+ @param keyid: the key in ASCII armor format, or a GPG key id.
303+ @raises SystemExit() via sys.exit() on failure.
304+ """
305+ try:
306+ return fetch_import_key(keyid)
307+ except GPGKeyError as e:
308+ error_out("Could not import key: {}".format(str(e)))
309+
310+
311+def get_source_and_pgp_key(source_and_key):
312+ """Look for a pgp key ID or ascii-armor key in the given input.
313+
314+ :param source_and_key: Sting, "source_spec|keyid" where '|keyid' is
315+ optional.
316+ :returns (source_spec, key_id OR None) as a tuple. Returns None for key_id
317+ if there was no '|' in the source_and_key string.
318+ """
319+ try:
320+ source, key = source_and_key.split('|', 2)
321+ return source, key or None
322+ except ValueError:
323+ return source_and_key, None
324+
325+
326+@deprecate("use charmhelpers.fetch.add_source() instead.",
327+ "2017-07", log=juju_log)
328+def configure_installation_source(source_plus_key):
329+ """Configure an installation source.
330+
331+ The functionality is provided by charmhelpers.fetch.add_source()
332+ The difference between the two functions is that add_source() signature
333+ requires the key to be passed directly, whereas this function passes an
334+ optional key by appending '|<key>' to the end of the source specificiation
335+ 'source'.
336+
337+ Another difference from add_source() is that the function calls sys.exit(1)
338+ if the configuration fails, whereas add_source() raises
339+ SourceConfigurationError(). Another difference, is that add_source()
340+ silently fails (with a juju_log command) if there is no matching source to
341+ configure, whereas this function fails with a sys.exit(1)
342+
343+ :param source: String_plus_key -- see above for details.
344+
345+ Note that the behaviour on error is to log the error to the juju log and
346+ then call sys.exit(1).
347+ """
348+ # extract the key if there is one, denoted by a '|' in the rel
349+ source, key = get_source_and_pgp_key(source_plus_key)
350+
351+ # handle the ordinary sources via add_source
352+ try:
353+ fetch_add_source(source, key, fail_invalid=True)
354+ except SourceConfigError as se:
355+ error_out(str(se))
356
357
358 def config_value_changed(option):
359@@ -638,7 +575,6 @@
360
361 :returns: bool: : Returns True if configured installation source offers
362 a newer version of package.
363-
364 """
365
366 import apt_pkg as apt
367
368=== modified file 'charmhelpers/fetch/__init__.py'
369--- charmhelpers/fetch/__init__.py 2016-09-20 17:07:51 +0000
370+++ charmhelpers/fetch/__init__.py 2017-04-10 18:12:16 +0000
371@@ -48,6 +48,13 @@
372 pass
373
374
375+class GPGKeyError(Exception):
376+ """Exception occurs when a GPG key cannot be fetched or used. The message
377+ indicates what the problem is.
378+ """
379+ pass
380+
381+
382 class BaseFetchHandler(object):
383
384 """Base class for FetchHandler implementations in fetch plugins"""
385@@ -77,21 +84,22 @@
386 fetch = importlib.import_module(module)
387
388 filter_installed_packages = fetch.filter_installed_packages
389-install = fetch.install
390-upgrade = fetch.upgrade
391-update = fetch.update
392-purge = fetch.purge
393+install = fetch.apt_install
394+upgrade = fetch.apt_upgrade
395+update = _fetch_update = fetch.apt_update
396+purge = fetch.apt_purge
397 add_source = fetch.add_source
398
399 if __platform__ == "ubuntu":
400 apt_cache = fetch.apt_cache
401- apt_install = fetch.install
402- apt_update = fetch.update
403- apt_upgrade = fetch.upgrade
404- apt_purge = fetch.purge
405+ apt_install = fetch.apt_install
406+ apt_update = fetch.apt_update
407+ apt_upgrade = fetch.apt_upgrade
408+ apt_purge = fetch.apt_purge
409 apt_mark = fetch.apt_mark
410 apt_hold = fetch.apt_hold
411 apt_unhold = fetch.apt_unhold
412+ import_key = fetch.import_key
413 get_upstream_version = fetch.get_upstream_version
414 elif __platform__ == "centos":
415 yum_search = fetch.yum_search
416@@ -135,7 +143,7 @@
417 for source, key in zip(sources, keys):
418 add_source(source, key)
419 if update:
420- fetch.update(fatal=True)
421+ _fetch_update(fatal=True)
422
423
424 def install_remote(source, *args, **kwargs):
425
426=== modified file 'charmhelpers/fetch/centos.py'
427--- charmhelpers/fetch/centos.py 2016-08-11 15:47:44 +0000
428+++ charmhelpers/fetch/centos.py 2017-04-10 18:12:16 +0000
429@@ -132,7 +132,7 @@
430 key_file.write(key)
431 key_file.flush()
432 key_file.seek(0)
433- subprocess.check_call(['rpm', '--import', key_file])
434+ subprocess.check_call(['rpm', '--import', key_file.name])
435 else:
436 subprocess.check_call(['rpm', '--import', key])
437
438
439=== modified file 'charmhelpers/fetch/ubuntu.py'
440--- charmhelpers/fetch/ubuntu.py 2017-03-03 20:37:37 +0000
441+++ charmhelpers/fetch/ubuntu.py 2017-04-10 18:12:16 +0000
442@@ -12,29 +12,46 @@
443 # See the License for the specific language governing permissions and
444 # limitations under the License.
445
446+from collections import OrderedDict
447 import os
448+import platform
449+import re
450 import six
451 import time
452 import subprocess
453-
454 from tempfile import NamedTemporaryFile
455+
456 from charmhelpers.core.host import (
457 lsb_release
458 )
459-from charmhelpers.core.hookenv import log
460-from charmhelpers.fetch import SourceConfigError
461+from charmhelpers.core.hookenv import (
462+ log,
463+ DEBUG,
464+)
465+from charmhelpers.fetch import SourceConfigError, GPGKeyError
466
467+PROPOSED_POCKET = (
468+ "# Proposed\n"
469+ "deb http://archive.ubuntu.com/ubuntu {}-proposed main universe "
470+ "multiverse restricted\n")
471+PROPOSED_PORTS_POCKET = (
472+ "# Proposed\n"
473+ "deb http://ports.ubuntu.com/ubuntu-ports {}-proposed main universe "
474+ "multiverse restricted\n")
475+# Only supports 64bit and ppc64 at the moment.
476+ARCH_TO_PROPOSED_POCKET = {
477+ 'x86_64': PROPOSED_POCKET,
478+ 'ppc64le': PROPOSED_PORTS_POCKET,
479+}
480+CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
481+CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
482 CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
483 deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
484 """
485-
486-PROPOSED_POCKET = """# Proposed
487-deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
488-"""
489-
490 CLOUD_ARCHIVE_POCKETS = {
491 # Folsom
492 'folsom': 'precise-updates/folsom',
493+ 'folsom/updates': 'precise-updates/folsom',
494 'precise-folsom': 'precise-updates/folsom',
495 'precise-folsom/updates': 'precise-updates/folsom',
496 'precise-updates/folsom': 'precise-updates/folsom',
497@@ -43,6 +60,7 @@
498 'precise-proposed/folsom': 'precise-proposed/folsom',
499 # Grizzly
500 'grizzly': 'precise-updates/grizzly',
501+ 'grizzly/updates': 'precise-updates/grizzly',
502 'precise-grizzly': 'precise-updates/grizzly',
503 'precise-grizzly/updates': 'precise-updates/grizzly',
504 'precise-updates/grizzly': 'precise-updates/grizzly',
505@@ -51,6 +69,7 @@
506 'precise-proposed/grizzly': 'precise-proposed/grizzly',
507 # Havana
508 'havana': 'precise-updates/havana',
509+ 'havana/updates': 'precise-updates/havana',
510 'precise-havana': 'precise-updates/havana',
511 'precise-havana/updates': 'precise-updates/havana',
512 'precise-updates/havana': 'precise-updates/havana',
513@@ -59,6 +78,7 @@
514 'precise-proposed/havana': 'precise-proposed/havana',
515 # Icehouse
516 'icehouse': 'precise-updates/icehouse',
517+ 'icehouse/updates': 'precise-updates/icehouse',
518 'precise-icehouse': 'precise-updates/icehouse',
519 'precise-icehouse/updates': 'precise-updates/icehouse',
520 'precise-updates/icehouse': 'precise-updates/icehouse',
521@@ -67,6 +87,7 @@
522 'precise-proposed/icehouse': 'precise-proposed/icehouse',
523 # Juno
524 'juno': 'trusty-updates/juno',
525+ 'juno/updates': 'trusty-updates/juno',
526 'trusty-juno': 'trusty-updates/juno',
527 'trusty-juno/updates': 'trusty-updates/juno',
528 'trusty-updates/juno': 'trusty-updates/juno',
529@@ -75,6 +96,7 @@
530 'trusty-proposed/juno': 'trusty-proposed/juno',
531 # Kilo
532 'kilo': 'trusty-updates/kilo',
533+ 'kilo/updates': 'trusty-updates/kilo',
534 'trusty-kilo': 'trusty-updates/kilo',
535 'trusty-kilo/updates': 'trusty-updates/kilo',
536 'trusty-updates/kilo': 'trusty-updates/kilo',
537@@ -83,6 +105,7 @@
538 'trusty-proposed/kilo': 'trusty-proposed/kilo',
539 # Liberty
540 'liberty': 'trusty-updates/liberty',
541+ 'liberty/updates': 'trusty-updates/liberty',
542 'trusty-liberty': 'trusty-updates/liberty',
543 'trusty-liberty/updates': 'trusty-updates/liberty',
544 'trusty-updates/liberty': 'trusty-updates/liberty',
545@@ -91,6 +114,7 @@
546 'trusty-proposed/liberty': 'trusty-proposed/liberty',
547 # Mitaka
548 'mitaka': 'trusty-updates/mitaka',
549+ 'mitaka/updates': 'trusty-updates/mitaka',
550 'trusty-mitaka': 'trusty-updates/mitaka',
551 'trusty-mitaka/updates': 'trusty-updates/mitaka',
552 'trusty-updates/mitaka': 'trusty-updates/mitaka',
553@@ -99,6 +123,7 @@
554 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
555 # Newton
556 'newton': 'xenial-updates/newton',
557+ 'newton/updates': 'xenial-updates/newton',
558 'xenial-newton': 'xenial-updates/newton',
559 'xenial-newton/updates': 'xenial-updates/newton',
560 'xenial-updates/newton': 'xenial-updates/newton',
561@@ -107,6 +132,7 @@
562 'xenial-proposed/newton': 'xenial-proposed/newton',
563 # Ocata
564 'ocata': 'xenial-updates/ocata',
565+ 'ocata/updates': 'xenial-updates/ocata',
566 'xenial-ocata': 'xenial-updates/ocata',
567 'xenial-ocata/updates': 'xenial-updates/ocata',
568 'xenial-updates/ocata': 'xenial-updates/ocata',
569@@ -115,6 +141,7 @@
570 'xenial-ocata/newton': 'xenial-proposed/ocata',
571 }
572
573+
574 APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
575 CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
576 CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times.
577@@ -145,7 +172,7 @@
578 return apt_pkg.Cache(progress)
579
580
581-def install(packages, options=None, fatal=False):
582+def apt_install(packages, options=None, fatal=False):
583 """Install one or more packages."""
584 if options is None:
585 options = ['--option=Dpkg::Options::=--force-confold']
586@@ -162,7 +189,7 @@
587 _run_apt_command(cmd, fatal)
588
589
590-def upgrade(options=None, fatal=False, dist=False):
591+def apt_upgrade(options=None, fatal=False, dist=False):
592 """Upgrade all packages."""
593 if options is None:
594 options = ['--option=Dpkg::Options::=--force-confold']
595@@ -177,13 +204,13 @@
596 _run_apt_command(cmd, fatal)
597
598
599-def update(fatal=False):
600+def apt_update(fatal=False):
601 """Update local apt cache."""
602 cmd = ['apt-get', 'update']
603 _run_apt_command(cmd, fatal)
604
605
606-def purge(packages, fatal=False):
607+def apt_purge(packages, fatal=False):
608 """Purge one or more packages."""
609 cmd = ['apt-get', '--assume-yes', 'purge']
610 if isinstance(packages, six.string_types):
611@@ -217,7 +244,45 @@
612 return apt_mark(packages, 'unhold', fatal=fatal)
613
614
615-def add_source(source, key=None):
616+def import_key(keyid):
617+ """Import a key in either ASCII Armor or Radix64 format.
618+
619+ `keyid` is either the keyid to fetch from a PGP server, or
620+ the key in ASCII armor foramt.
621+
622+ :param keyid: String of key (or key id).
623+ :raises: GPGKeyError if the key could not be imported
624+ """
625+ key = keyid.strip()
626+ if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
627+ key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
628+ log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
629+ log("Importing ASCII Armor PGP key", level=DEBUG)
630+ with NamedTemporaryFile() as keyfile:
631+ with open(keyfile.name, 'w') as fd:
632+ fd.write(key)
633+ fd.write("\n")
634+ cmd = ['apt-key', 'add', keyfile.name]
635+ try:
636+ subprocess.check_call(cmd)
637+ except subprocess.CalledProcessError:
638+ error = "Error importing PGP key '{}'".format(key)
639+ log(error)
640+ raise GPGKeyError(error)
641+ else:
642+ log("PGP key found (looks like Radix64 format)", level=DEBUG)
643+ log("Importing PGP key from keyserver", level=DEBUG)
644+ cmd = ['apt-key', 'adv', '--keyserver',
645+ 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
646+ try:
647+ subprocess.check_call(cmd)
648+ except subprocess.CalledProcessError:
649+ error = "Error importing PGP key '{}'".format(key)
650+ log(error)
651+ raise GPGKeyError(error)
652+
653+
654+def add_source(source, key=None, fail_invalid=False):
655 """Add a package source to this system.
656
657 @param source: a URL or sources.list entry, as supported by
658@@ -233,6 +298,33 @@
659 such as 'cloud:icehouse'
660 'distro' may be used as a noop
661
662+ Full list of source specifications supported by the function are:
663+
664+ 'distro': A NOP; i.e. it has no effect.
665+ 'proposed': the proposed deb spec [2] is wrtten to
666+ /etc/apt/sources.list/proposed
667+ 'distro-proposed': adds <version>-proposed to the debs [2]
668+ 'ppa:<ppa-name>': add-apt-repository --yes <ppa_name>
669+ 'deb <deb-spec>': add-apt-repository --yes deb <deb-spec>
670+ 'http://....': add-apt-repository --yes http://...
671+ 'cloud-archive:<spec>': add-apt-repository -yes cloud-archive:<spec>
672+ 'cloud:<release>[-staging]': specify a Cloud Archive pocket <release> with
673+ optional staging version. If staging is used then the staging PPA [2]
674+ with be used. If staging is NOT used then the cloud archive [3] will be
675+ added, and the 'ubuntu-cloud-keyring' package will be added for the
676+ current distro.
677+
678+ Otherwise the source is not recognised and this is logged to the juju log.
679+ However, no error is raised, unless sys_error_on_exit is True.
680+
681+ [1] deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
682+ where {} is replaced with the derived pocket name.
683+ [2] deb http://archive.ubuntu.com/ubuntu {}-proposed \
684+ main universe multiverse restricted
685+ where {} is replaced with the lsb_release codename (e.g. xenial)
686+ [3] deb http://ubuntu-cloud.archive.canonical.com/ubuntu <pocket>
687+ to /etc/apt/sources.list.d/cloud-archive-list
688+
689 @param key: A key to be added to the system's APT keyring and used
690 to verify the signatures on packages. Ideally, this should be an
691 ASCII format GPG public key including the block headers. A GPG key
692@@ -240,51 +332,141 @@
693 available to retrieve the actual public key from a public keyserver
694 placing your Juju environment at risk. ppa and cloud archive keys
695 are securely added automtically, so sould not be provided.
696+
697+ @param fail_invalid: (boolean) if True, then the function raises a
698+ SourceConfigError is there is no matching installation source.
699+
700+ @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a
701+ valid pocket in CLOUD_ARCHIVE_POCKETS
702 """
703+ _mapping = OrderedDict([
704+ (r"^distro$", lambda: None), # This is a NOP
705+ (r"^(?:proposed|distro-proposed)$", _add_proposed),
706+ (r"^cloud-archive:(.*)$", _add_apt_repository),
707+ (r"^((?:deb |http:|https:|ppa:).*)$", _add_apt_repository),
708+ (r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging),
709+ (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
710+ (r"^cloud:(.*)$", _add_cloud_pocket),
711+ ])
712 if source is None:
713- log('Source is not present. Skipping')
714- return
715-
716- if (source.startswith('ppa:') or
717- source.startswith('http') or
718- source.startswith('deb ') or
719- source.startswith('cloud-archive:')):
720- cmd = ['add-apt-repository', '--yes', source]
721- _run_with_retries(cmd)
722- elif source.startswith('cloud:'):
723- install(filter_installed_packages(['ubuntu-cloud-keyring']),
724+ source = ''
725+ for r, fn in six.iteritems(_mapping):
726+ m = re.match(r, source)
727+ if m:
728+ # call the assoicated function with the captured groups
729+ # raises SourceConfigError on error.
730+ fn(*m.groups())
731+ if key:
732+ try:
733+ import_key(key)
734+ except GPGKeyError as e:
735+ raise SourceConfigError(str(e))
736+ break
737+ else:
738+ # nothing matched. log an error and maybe sys.exit
739+ err = "Unknown source: {!r}".format(source)
740+ log(err)
741+ if fail_invalid:
742+ raise SourceConfigError(err)
743+
744+
745+def _add_proposed():
746+ """Add the PROPOSED_POCKET as /etc/apt/source.list.d/proposed.list
747+
748+ Uses lsb_release()['DISTRIB_CODENAME'] to determine the correct staza for
749+ the deb line.
750+
751+ For intel architecutres PROPOSED_POCKET is used for the release, but for
752+ other architectures PROPOSED_PORTS_POCKET is used for the release.
753+ """
754+ release = lsb_release()['DISTRIB_CODENAME']
755+ arch = platform.machine()
756+ if arch not in six.iterkeys(ARCH_TO_PROPOSED_POCKET):
757+ raise SourceConfigError("Arch {} not supported for (distro-)proposed"
758+ .format(arch))
759+ with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
760+ apt.write(ARCH_TO_PROPOSED_POCKET[arch].format(release))
761+
762+
763+def _add_apt_repository(spec):
764+ """Add the spec using add_apt_repository
765+
766+ :param spec: the parameter to pass to add_apt_repository
767+ """
768+ _run_with_retries(['add-apt-repository', '--yes', spec])
769+
770+
771+def _add_cloud_pocket(pocket):
772+ """Add a cloud pocket as /etc/apt/sources.d/cloud-archive.list
773+
774+ Note that this overwrites the existing file if there is one.
775+
776+ This function also converts the simple pocket in to the actual pocket using
777+ the CLOUD_ARCHIVE_POCKETS mapping.
778+
779+ :param pocket: string representing the pocket to add a deb spec for.
780+ :raises: SourceConfigError if the cloud pocket doesn't exist or the
781+ requested release doesn't match the current distro version.
782+ """
783+ apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
784 fatal=True)
785- pocket = source.split(':')[-1]
786- if pocket not in CLOUD_ARCHIVE_POCKETS:
787- raise SourceConfigError(
788- 'Unsupported cloud: source option %s' %
789- pocket)
790- actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
791- with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
792- apt.write(CLOUD_ARCHIVE.format(actual_pocket))
793- elif source == 'proposed':
794- release = lsb_release()['DISTRIB_CODENAME']
795- with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
796- apt.write(PROPOSED_POCKET.format(release))
797- elif source == 'distro':
798- pass
799- else:
800- log("Unknown source: {!r}".format(source))
801-
802- if key:
803- if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
804- with NamedTemporaryFile('w+') as key_file:
805- key_file.write(key)
806- key_file.flush()
807- key_file.seek(0)
808- subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
809- else:
810- # Note that hkp: is in no way a secure protocol. Using a
811- # GPG key id is pointless from a security POV unless you
812- # absolutely trust your network and DNS.
813- subprocess.check_call(['apt-key', 'adv', '--keyserver',
814- 'hkp://keyserver.ubuntu.com:80', '--recv',
815- key])
816+ if pocket not in CLOUD_ARCHIVE_POCKETS:
817+ raise SourceConfigError(
818+ 'Unsupported cloud: source option %s' %
819+ pocket)
820+ actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
821+ with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
822+ apt.write(CLOUD_ARCHIVE.format(actual_pocket))
823+
824+
825+def _add_cloud_staging(cloud_archive_release, openstack_release):
826+ """Add the cloud staging repository which is in
827+ ppa:ubuntu-cloud-archive/<openstack_release>-staging
828+
829+ This function checks that the cloud_archive_release matches the current
830+ codename for the distro that charm is being installed on.
831+
832+ :param cloud_archive_release: string, codename for the release.
833+ :param openstack_release: String, codename for the openstack release.
834+ :raises: SourceConfigError if the cloud_archive_release doesn't match the
835+ current version of the os.
836+ """
837+ _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
838+ ppa = 'ppa:ubuntu-cloud-archive/{}-staging'.format(openstack_release)
839+ cmd = 'add-apt-repository -y {}'.format(ppa)
840+ _run_with_retries(cmd.split(' '))
841+
842+
843+def _add_cloud_distro_check(cloud_archive_release, openstack_release):
844+ """Add the cloud pocket, but also check the cloud_archive_release against
845+ the current distro, and use the openstack_release as the full lookup.
846+
847+ This just calls _add_cloud_pocket() with the openstack_release as pocket
848+ to get the correct cloud-archive.list for dpkg to work with.
849+
850+ :param cloud_archive_release:String, codename for the distro release.
851+ :param openstack_release: String, spec for the release to look up in the
852+ CLOUD_ARCHIVE_POCKETS
853+ :raises: SourceConfigError if this is the wrong distro, or the pocket spec
854+ doesn't exist.
855+ """
856+ _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
857+ _add_cloud_pocket("{}-{}".format(cloud_archive_release, openstack_release))
858+
859+
860+def _verify_is_ubuntu_rel(release, os_release):
861+ """Verify that the release is in the same as the current ubuntu release.
862+
863+ :param release: String, lowercase for the release.
864+ :param os_release: String, the os_release being asked for
865+ :raises: SourceConfigError if the release is not the same as the ubuntu
866+ release.
867+ """
868+ ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
869+ if release != ubuntu_rel:
870+ raise SourceConfigError(
871+ 'Invalid Cloud Archive release specified: {}-{} on this Ubuntu'
872+ 'version ({})'.format(release, os_release, ubuntu_rel))
873
874
875 def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
876@@ -300,9 +482,12 @@
877 :param: cmd_env: dict: Environment variables to add to the command run.
878 """
879
880- env = os.environ.copy()
881+ env = None
882+ kwargs = {}
883 if cmd_env:
884+ env = os.environ.copy()
885 env.update(cmd_env)
886+ kwargs['env'] = env
887
888 if not retry_message:
889 retry_message = "Failed executing '{}'".format(" ".join(cmd))
890@@ -314,7 +499,8 @@
891 retry_results = (None,) + retry_exitcodes
892 while result in retry_results:
893 try:
894- result = subprocess.check_call(cmd, env=env)
895+ # result = subprocess.check_call(cmd, env=env)
896+ result = subprocess.check_call(cmd, **kwargs)
897 except subprocess.CalledProcessError as e:
898 retry_count = retry_count + 1
899 if retry_count > max_retries:
900@@ -327,6 +513,7 @@
901 def _run_apt_command(cmd, fatal=False):
902 """Run an apt command with optional retries.
903
904+ :param: cmd: str: The apt command to run.
905 :param: fatal: bool: Whether the command's output should be checked and
906 retried.
907 """
908
909=== modified file 'tests/contrib/openstack/test_openstack_utils.py'
910--- tests/contrib/openstack/test_openstack_utils.py 2016-12-14 18:51:15 +0000
911+++ tests/contrib/openstack/test_openstack_utils.py 2017-04-10 18:12:16 +0000
912@@ -1,13 +1,13 @@
913 import io
914 import os
915-import subprocess
916-import tempfile
917 import contextlib
918 import unittest
919 from copy import copy
920+from tests.helpers import patch_open
921 from testtools import TestCase
922 from mock import MagicMock, patch, call
923
924+from charmhelpers.fetch import ubuntu as fetch
925 import charmhelpers.contrib.openstack.utils as openstack
926
927 import six
928@@ -123,23 +123,9 @@
929
930 # Mock python-dnspython resolver used by get_host_ip()
931
932-PGP_KEY_ASCII_ARMOR = """-----BEGIN PGP PUBLIC KEY BLOCK-----
933-Version: SKS 1.1.5
934-Comment: Hostname: keyserver.ubuntu.com
935-
936-mI0EUCEyTAEEAMuUxyfiegCCwn4J/c0nw5PUTSJdn5FqiUTq6iMfij65xf1vl0g/Mxqw0gfg
937-AJIsCDvO9N9dloLAwF6FUBMg5My7WyhRPTAKF505TKJboyX3Pp4J1fU1LV8QFVOp87vUh1Rz
938-B6GU7cSglhnbL85gmbJTllkzkb3h4Yw7W+edjcQ/ABEBAAG0K0xhdW5jaHBhZCBQUEEgZm9y
939-IFVidW50dSBDbG91ZCBBcmNoaXZlIFRlYW2IuAQTAQIAIgUCUCEyTAIbAwYLCQgHAwIGFQgC
940-CQoLBBYCAwECHgECF4AACgkQimhEop9oEE7kJAP/eTBgq3Mhbvo0d8elMOuqZx3nmU7gSyPh
941-ep0zYIRZ5TJWl/7PRtvp0CJA6N6ZywYTQ/4ANHhpibcHZkh8K0AzUvsGXnJRSFoJeqyDbD91
942-EhoO+4ZfHs2HvRBQEDZILMa2OyuB497E5Mmyua3HDEOrG2cVLllsUZzpTFCx8NgeMHk=
943-=jLBm
944------END PGP PUBLIC KEY BLOCK-----
945-"""
946-
947
948 class FakeAnswer(object):
949+
950 def __init__(self, ip):
951 self.ip = ip
952
953@@ -148,6 +134,7 @@
954
955
956 class FakeResolver(object):
957+
958 def __init__(self, ip):
959 self.ip = ip
960
961@@ -159,16 +146,19 @@
962
963
964 class FakeReverse(object):
965+
966 def from_address(self, address):
967 return '156.94.189.91.in-addr.arpa'
968
969
970 class FakeDNSName(object):
971+
972 def __init__(self, dnsname):
973 pass
974
975
976 class FakeDNS(object):
977+
978 def __init__(self, ip):
979 self.resolver = FakeResolver(ip)
980 self.reversename = FakeReverse()
981@@ -177,6 +167,7 @@
982
983
984 class OpenStackHelpersTestCase(TestCase):
985+
986 def _apt_cache(self):
987 # mocks out the apt cache
988 def cache_get(package):
989@@ -197,7 +188,7 @@
990
991 @patch('charmhelpers.contrib.openstack.utils.lsb_release')
992 def test_os_codename_from_install_source(self, mocked_lsb):
993- '''Test mapping install source to OpenStack release name'''
994+ """Test mapping install source to OpenStack release name"""
995 mocked_lsb.return_value = FAKE_RELEASE
996
997 # the openstack release shipped with respective ubuntu/lsb release.
998@@ -236,7 +227,7 @@
999
1000 @patch('charmhelpers.contrib.openstack.utils.lsb_release')
1001 def test_os_codename_from_bad_install_source(self, mocked_lsb):
1002- '''Test mapping install source to OpenStack release name'''
1003+ """Test mapping install source to OpenStack release name"""
1004 _fake_release = copy(FAKE_RELEASE)
1005 _fake_release['DISTRIB_CODENAME'] = 'natty'
1006
1007@@ -249,32 +240,32 @@
1008 mocked_err.assert_called_with(_er)
1009
1010 def test_os_codename_from_version(self):
1011- '''Test mapping OpenStack numerical versions to code name'''
1012+ """Test mapping OpenStack numerical versions to code name"""
1013 self.assertEquals(openstack.get_os_codename_version('2013.1'),
1014 'grizzly')
1015
1016 @patch('charmhelpers.contrib.openstack.utils.error_out')
1017 def test_os_codename_from_bad_version(self, mocked_error):
1018- '''Test mapping a bad OpenStack numerical versions to code name'''
1019+ """Test mapping a bad OpenStack numerical versions to code name"""
1020 openstack.get_os_codename_version('2014.5.5')
1021 expected_err = ('Could not determine OpenStack codename for '
1022 'version 2014.5.5')
1023 mocked_error.assert_called_with(expected_err)
1024
1025 def test_os_version_from_codename(self):
1026- '''Test mapping a OpenStack codename to numerical version'''
1027+ """Test mapping a OpenStack codename to numerical version"""
1028 self.assertEquals(openstack.get_os_version_codename('folsom'),
1029 '2012.2')
1030
1031 @patch('charmhelpers.contrib.openstack.utils.error_out')
1032 def test_os_version_from_bad_codename(self, mocked_error):
1033- '''Test mapping a bad OpenStack codename to numerical version'''
1034+ """Test mapping a bad OpenStack codename to numerical version"""
1035 openstack.get_os_version_codename('foo')
1036 expected_err = 'Could not derive OpenStack version for codename: foo'
1037 mocked_error.assert_called_with(expected_err)
1038
1039 def test_os_version_swift_from_codename(self):
1040- '''Test mapping a swift codename to numerical version'''
1041+ """Test mapping a swift codename to numerical version"""
1042 self.assertEquals(openstack.get_os_version_codename_swift('liberty'),
1043 '2.5.0')
1044
1045@@ -283,7 +274,7 @@
1046
1047 @patch('charmhelpers.contrib.openstack.utils.error_out')
1048 def test_os_version_swift_from_bad_codename(self, mocked_error):
1049- '''Test mapping a bad swift codename to numerical version'''
1050+ """Test mapping a bad swift codename to numerical version"""
1051 openstack.get_os_version_codename_swift('foo')
1052 expected_err = 'Could not derive swift version for codename: foo'
1053 mocked_error.assert_called_with(expected_err)
1054@@ -302,7 +293,7 @@
1055 self.assertEquals(openstack.get_swift_codename('1.2.3'), None)
1056
1057 def test_os_codename_from_package(self):
1058- '''Test deriving OpenStack codename from an installed package'''
1059+ """Test deriving OpenStack codename from an installed package"""
1060 with patch('apt_pkg.Cache') as cache:
1061 cache.return_value = self._apt_cache()
1062 for pkg, vers in six.iteritems(FAKE_REPO):
1063@@ -316,7 +307,7 @@
1064
1065 @patch('charmhelpers.contrib.openstack.utils.error_out')
1066 def test_os_codename_from_bad_package_version(self, mocked_error):
1067- '''Test deriving OpenStack codename for a poorly versioned package'''
1068+ """Test deriving OpenStack codename for a poorly versioned package"""
1069 with patch('apt_pkg.Cache') as cache:
1070 cache.return_value = self._apt_cache()
1071 openstack.get_os_codename_package('bad-version')
1072@@ -325,7 +316,7 @@
1073
1074 @patch('charmhelpers.contrib.openstack.utils.error_out')
1075 def test_os_codename_from_bad_package(self, mocked_error):
1076- '''Test deriving OpenStack codename from an unavailable package'''
1077+ """Test deriving OpenStack codename from an unavailable package"""
1078 with patch('apt_pkg.Cache') as cache:
1079 cache.return_value = self._apt_cache()
1080 try:
1081@@ -339,7 +330,7 @@
1082 mocked_error.assert_called_with(e)
1083
1084 def test_os_codename_from_bad_package_nonfatal(self):
1085- '''Test OpenStack codename from an unavailable package is non-fatal'''
1086+ """Test OpenStack codename from an unavailable package is non-fatal"""
1087 with patch('apt_pkg.Cache') as cache:
1088 cache.return_value = self._apt_cache()
1089 self.assertEquals(
1090@@ -349,7 +340,7 @@
1091
1092 @patch('charmhelpers.contrib.openstack.utils.error_out')
1093 def test_os_codename_from_uninstalled_package(self, mock_error):
1094- '''Test OpenStack codename from an available but uninstalled pkg'''
1095+ """Test OpenStack codename from an available but uninstalled pkg"""
1096 with patch('apt_pkg.Cache') as cache:
1097 cache.return_value = self._apt_cache()
1098 try:
1099@@ -361,7 +352,7 @@
1100 mock_error.assert_called_with(e)
1101
1102 def test_os_codename_from_uninstalled_package_nonfatal(self):
1103- '''Test OpenStack codename from avail uninstalled pkg is non fatal'''
1104+ """Test OpenStack codename from avail uninstalled pkg is non fatal"""
1105 with patch('apt_pkg.Cache') as cache:
1106 cache.return_value = self._apt_cache()
1107 self.assertEquals(
1108@@ -371,7 +362,7 @@
1109
1110 @patch('charmhelpers.contrib.openstack.utils.error_out')
1111 def test_os_version_from_package(self, mocked_error):
1112- '''Test deriving OpenStack version from an installed package'''
1113+ """Test deriving OpenStack version from an installed package"""
1114 with patch('apt_pkg.Cache') as cache:
1115 cache.return_value = self._apt_cache()
1116 for pkg, vers in six.iteritems(FAKE_REPO):
1117@@ -384,7 +375,7 @@
1118
1119 @patch('charmhelpers.contrib.openstack.utils.error_out')
1120 def test_os_version_from_bad_package(self, mocked_error):
1121- '''Test deriving OpenStack version from an uninstalled package'''
1122+ """Test deriving OpenStack version from an uninstalled package"""
1123 with patch('apt_pkg.Cache') as cache:
1124 cache.return_value = self._apt_cache()
1125 try:
1126@@ -398,7 +389,7 @@
1127 mocked_error.assert_called_with(e)
1128
1129 def test_os_version_from_bad_package_nonfatal(self):
1130- '''Test OpenStack version from an uninstalled package is non-fatal'''
1131+ """Test OpenStack version from an uninstalled package is non-fatal"""
1132 with patch('apt_pkg.Cache') as cache:
1133 cache.return_value = self._apt_cache()
1134 self.assertEquals(
1135@@ -410,7 +401,7 @@
1136 @patch.object(openstack, 'git_os_codename_install_source')
1137 @patch('charmhelpers.contrib.openstack.utils.config')
1138 def test_os_release_uncached(self, config, git_cn, get_cn):
1139- openstack.os_rel = None
1140+ openstack._os_rel = None
1141 get_cn.return_value = 'folsom'
1142 git_cn.return_value = None
1143
1144@@ -420,69 +411,142 @@
1145 self.assertEquals('folsom', openstack.os_release('nova-common'))
1146
1147 def test_os_release_cached(self):
1148- openstack.os_rel = 'foo'
1149+ openstack._os_rel = 'foo'
1150 self.assertEquals('foo', openstack.os_release('nova-common'))
1151
1152 @patch.object(openstack, 'juju_log')
1153 @patch('sys.exit')
1154 def test_error_out(self, mocked_exit, juju_log):
1155- '''Test erroring out'''
1156+ """Test erroring out"""
1157 openstack.error_out('Everything broke.')
1158 _log = 'FATAL ERROR: Everything broke.'
1159 juju_log.assert_called_with(_log, level='ERROR')
1160 mocked_exit.assert_called_with(1)
1161
1162+ def test_get_source_and_pgp_key(self):
1163+ tests = {
1164+ "source|key": ('source', 'key'),
1165+ "source|": ('source', None),
1166+ "|key": ('', 'key'),
1167+ "source": ('source', None),
1168+ }
1169+ for k, v in six.iteritems(tests):
1170+ self.assertEqual(openstack.get_source_and_pgp_key(k), v)
1171+
1172+ # These should still work, even though the bulk of the functionality has
1173+ # moved to charmhelpers.fetch.add_source()
1174 def test_configure_install_source_distro(self):
1175- '''Test configuring installation from distro'''
1176+ """Test configuring installation from distro"""
1177 self.assertIsNone(openstack.configure_installation_source('distro'))
1178
1179 def test_configure_install_source_ppa(self):
1180- '''Test configuring installation source from PPA'''
1181+ """Test configuring installation source from PPA"""
1182 with patch('subprocess.check_call') as mock:
1183 src = 'ppa:gandelman-a/openstack'
1184 openstack.configure_installation_source(src)
1185- ex_cmd = ['add-apt-repository', '-y', 'ppa:gandelman-a/openstack']
1186+ ex_cmd = [
1187+ 'add-apt-repository', '--yes', 'ppa:gandelman-a/openstack']
1188 mock.assert_called_with(ex_cmd)
1189
1190- @patch(builtin_open)
1191- @patch('charmhelpers.contrib.openstack.utils.juju_log')
1192- @patch('charmhelpers.contrib.openstack.utils.import_key')
1193- def test_configure_install_source_deb_url(self, _import, _log, _open):
1194- '''Test configuring installation source from deb repo url'''
1195- _file = MagicMock(spec=io.FileIO)
1196- _open.return_value = _file
1197+ @patch('subprocess.check_call')
1198+ @patch.object(fetch, 'import_key')
1199+ def test_configure_install_source_deb_url(self, _import, _spcc):
1200+ """Test configuring installation source from deb repo url"""
1201 src = ('deb http://ubuntu-cloud.archive.canonical.com/ubuntu '
1202 'precise-havana main|KEYID')
1203 openstack.configure_installation_source(src)
1204 _import.assert_called_with('KEYID')
1205- _file.__enter__().write.assert_called_with(src.split('|')[0])
1206- src = ('deb http://ubuntu-cloud.archive.canonical.com/ubuntu '
1207- 'precise-havana main')
1208- openstack.configure_installation_source(src)
1209- _file.__enter__().write.assert_called_with(src)
1210+ _spcc.assert_called_once_with(
1211+ ['add-apt-repository', '--yes',
1212+ 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu '
1213+ 'precise-havana main'])
1214
1215- @patch('charmhelpers.contrib.openstack.utils.lsb_release')
1216+ @patch.object(fetch, 'lsb_release')
1217 @patch(builtin_open)
1218- @patch('charmhelpers.contrib.openstack.utils.juju_log')
1219- def test_configure_install_source_distro_proposed(self, _log, _open, _lsb):
1220- '''Test configuring installation source from deb repo url'''
1221+ @patch('subprocess.check_call')
1222+ def test_configure_install_source_distro_proposed(
1223+ self, _spcc, _open, _lsb):
1224+ """Test configuring installation source from deb repo url"""
1225 _lsb.return_value = FAKE_RELEASE
1226 _file = MagicMock(spec=io.FileIO)
1227 _open.return_value = _file
1228 openstack.configure_installation_source('distro-proposed')
1229+ _file.__enter__().write.assert_called_once_with(
1230+ '# Proposed\ndeb http://archive.ubuntu.com/ubuntu '
1231+ 'precise-proposed main universe multiverse restricted\n')
1232 src = ('deb http://archive.ubuntu.com/ubuntu/ precise-proposed '
1233 'restricted main multiverse universe')
1234 openstack.configure_installation_source(src)
1235- _file.__enter__().write.assert_called_with(src)
1236+ _spcc.assert_called_once_with(
1237+ ['add-apt-repository', '--yes',
1238+ 'deb http://archive.ubuntu.com/ubuntu/ precise-proposed '
1239+ 'restricted main multiverse universe'])
1240+
1241+ @patch('charmhelpers.fetch.filter_installed_packages')
1242+ @patch('charmhelpers.fetch.apt_install')
1243+ @patch.object(openstack, 'error_out')
1244+ @patch.object(openstack, 'juju_log')
1245+ def test_add_source_cloud_invalid_pocket(self, _log, _out,
1246+ apt_install, filter_pkg):
1247+ openstack.configure_installation_source("cloud:havana-updates")
1248+ _e = ('Invalid Cloud Archive release specified: '
1249+ 'havana-updates on this Ubuntuversion')
1250+ _s = _out.call_args[0][0]
1251+ self.assertTrue(_s.startswith(_e))
1252+
1253+ @patch.object(fetch, 'filter_installed_packages')
1254+ @patch.object(fetch, 'apt_install')
1255+ @patch.object(fetch, 'lsb_release')
1256+ def test_add_source_cloud_pocket_style(self, lsb_release,
1257+ apt_install, filter_pkg):
1258+ source = "cloud:precise-updates/havana"
1259+ lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
1260+ result = (
1261+ "# Ubuntu Cloud Archive\n"
1262+ "deb http://ubuntu-cloud.archive.canonical.com/ubuntu "
1263+ "precise-updates/havana main\n")
1264+ with patch_open() as (mock_open, mock_file):
1265+ openstack.configure_installation_source(source)
1266+ mock_file.write.assert_called_with(result)
1267+ filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
1268+
1269+ @patch.object(fetch, 'filter_installed_packages')
1270+ @patch.object(fetch, 'apt_install')
1271+ @patch.object(fetch, 'lsb_release')
1272+ def test_add_source_cloud_os_style(self, lsb_release,
1273+ apt_install, filter_pkg):
1274+ source = "cloud:precise-havana"
1275+ lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
1276+ result = (
1277+ "# Ubuntu Cloud Archive\n"
1278+ "deb http://ubuntu-cloud.archive.canonical.com/ubuntu "
1279+ "precise-updates/havana main\n")
1280+ with patch_open() as (mock_open, mock_file):
1281+ openstack.configure_installation_source(source)
1282+ mock_file.write.assert_called_with(result)
1283+ filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
1284+
1285+ @patch.object(fetch, 'filter_installed_packages')
1286+ @patch.object(fetch, 'apt_install')
1287+ def test_add_source_cloud_distroless_style(self, apt_install, filter_pkg):
1288+ source = "cloud:havana"
1289+ result = (
1290+ "# Ubuntu Cloud Archive\n"
1291+ "deb http://ubuntu-cloud.archive.canonical.com/ubuntu "
1292+ "precise-updates/havana main\n")
1293+ with patch_open() as (mock_open, mock_file):
1294+ openstack.configure_installation_source(source)
1295+ mock_file.write.assert_called_with(result)
1296+ filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
1297
1298 @patch('charmhelpers.contrib.openstack.utils.error_out')
1299 def test_configure_bad_install_source(self, _error):
1300 openstack.configure_installation_source('foo')
1301- _error.assert_called_with('Invalid openstack-release specified: foo')
1302+ _error.assert_called_with("Unknown source: 'foo'")
1303
1304- @patch('charmhelpers.contrib.openstack.utils.lsb_release')
1305+ @patch.object(fetch, 'lsb_release')
1306 def test_configure_install_source_uca_staging(self, _lsb):
1307- '''Test configuring installation source from UCA staging sources'''
1308+ """Test configuring installation source from UCA staging sources"""
1309 _lsb.return_value = FAKE_RELEASE
1310 # staging pockets are configured as PPAs
1311 with patch('subprocess.check_call') as _subp:
1312@@ -493,76 +557,63 @@
1313 _subp.assert_called_with(cmd)
1314
1315 @patch(builtin_open)
1316- @patch('charmhelpers.contrib.openstack.utils.apt_install')
1317- @patch('charmhelpers.contrib.openstack.utils.lsb_release')
1318- def test_configure_install_source_uca_repos(self, _lsb, _install, _open):
1319- '''Test configuring installation source from UCA sources'''
1320+ @patch.object(fetch, 'apt_install')
1321+ @patch.object(fetch, 'lsb_release')
1322+ @patch.object(fetch, 'filter_installed_packages')
1323+ def test_configure_install_source_uca_repos(
1324+ self, _fip, _lsb, _install, _open):
1325+ """Test configuring installation source from UCA sources"""
1326 _lsb.return_value = FAKE_RELEASE
1327 _file = MagicMock(spec=io.FileIO)
1328 _open.return_value = _file
1329+ _fip.side_effect = lambda x: x
1330 for src, url in UCA_SOURCES:
1331+ actual_url = "# Ubuntu Cloud Archive\n{}\n".format(url)
1332 openstack.configure_installation_source(src)
1333- _install.assert_called_with('ubuntu-cloud-keyring',
1334+ _install.assert_called_with(['ubuntu-cloud-keyring'],
1335 fatal=True)
1336 _open.assert_called_with(
1337 '/etc/apt/sources.list.d/cloud-archive.list',
1338 'w'
1339 )
1340- _file.__enter__().write.assert_called_with(url)
1341+ _file.__enter__().write.assert_called_with(actual_url)
1342
1343 @patch('charmhelpers.contrib.openstack.utils.error_out')
1344 def test_configure_install_source_bad_uca(self, mocked_error):
1345- '''Test configuring installation source from bad UCA source'''
1346+ """Test configuring installation source from bad UCA source"""
1347 try:
1348 openstack.configure_installation_source('cloud:foo-bar')
1349 except:
1350 # ignore exceptions that raise when error_out is mocked
1351 # and doesn't sys.exit(1)
1352 pass
1353- _e = 'Invalid Cloud Archive release specified: foo-bar'
1354- mocked_error.assert_called_with(_e)
1355-
1356- @patch.object(openstack, 'juju_log', lambda *args, **kwargs: None)
1357- def test_import_apt_key_radix(self):
1358- '''Ensure shell out apt-key during key import'''
1359- with patch('subprocess.check_call') as _subp:
1360- openstack.import_key('foo')
1361- cmd = ['apt-key', 'adv', '--keyserver',
1362- 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']
1363- _subp.assert_called_with(cmd)
1364-
1365- @patch.object(openstack, 'juju_log', lambda *args, **kwargs: None)
1366- def test_import_apt_key_ascii_armor(self):
1367- with tempfile.NamedTemporaryFile() as tmp:
1368- with patch.object(openstack, 'tempfile') as \
1369- mock_tmpfile:
1370- tmpfile = mock_tmpfile.NamedTemporaryFile.return_value
1371- tmpfile.__enter__.return_value = tmpfile
1372- tmpfile.name = tmp.name
1373- with patch('subprocess.check_call') as _subp:
1374- openstack.import_key(PGP_KEY_ASCII_ARMOR)
1375- cmd = ['apt-key', 'add', tmp.name]
1376- _subp.assert_called_with(cmd)
1377-
1378- @patch.object(openstack, 'juju_log', lambda *args, **kwargs: None)
1379- @patch('charmhelpers.contrib.openstack.utils.error_out')
1380- def test_import_bad_apt_key(self, mocked_error):
1381- '''Ensure error when importing apt key fails'''
1382- with patch('subprocess.check_call') as _subp:
1383- cmd = ['apt-key', 'adv', '--keyserver',
1384- 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']
1385- _subp.side_effect = subprocess.CalledProcessError(1, cmd, '')
1386- openstack.import_key('foo')
1387- cmd = ['apt-key', 'adv', '--keyserver',
1388- 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']
1389- mocked_error.assert_called_with("Error importing PGP key 'foo'")
1390+ _e = ('Invalid Cloud Archive release specified: foo-bar'
1391+ ' on this Ubuntuversion')
1392+ _s = mocked_error.call_args[0][0]
1393+ self.assertTrue(_s.startswith(_e))
1394+
1395+ @patch.object(openstack, 'fetch_import_key')
1396+ def test_import_key_calls_fetch_import_key(self, fetch_import_key):
1397+ openstack.import_key('random-string')
1398+ fetch_import_key.assert_called_once_with('random-string')
1399+
1400+ @patch.object(openstack, 'fetch_import_key')
1401+ @patch.object(openstack, 'sys')
1402+ def test_import_key_calls_sys_exit_on_error(self, mock_sys,
1403+ fetch_import_key):
1404+
1405+ def raiser(_):
1406+ raise openstack.GPGKeyError("an error occurred")
1407+ fetch_import_key.side_effect = raiser
1408+ openstack.import_key('random failure')
1409+ mock_sys.exit.assert_called_once_with(1)
1410
1411 @patch('os.mkdir')
1412 @patch('os.path.exists')
1413 @patch('charmhelpers.contrib.openstack.utils.charm_dir')
1414 @patch(builtin_open)
1415 def test_save_scriptrc(self, _open, _charm_dir, _exists, _mkdir):
1416- '''Test generation of scriptrc from environment'''
1417+ """Test generation of scriptrc from environment"""
1418 scriptrc = ['#!/bin/bash\n',
1419 'export setting1=foo\n',
1420 'export setting2=bar\n']
1421@@ -618,14 +669,14 @@
1422 @patch.object(openstack, 'is_block_device')
1423 @patch.object(openstack, 'error_out')
1424 def test_ensure_block_device_bad_config(self, err, is_bd):
1425- '''Test it doesn't prepare storage with bad config'''
1426+ """Test it doesn't prepare storage with bad config"""
1427 openstack.ensure_block_device(block_device='none')
1428 self.assertTrue(err.called)
1429
1430 @patch.object(openstack, 'is_block_device')
1431 @patch.object(openstack, 'ensure_loopback_device')
1432 def test_ensure_block_device_loopback(self, ensure_loopback, is_bd):
1433- '''Test it ensures loopback device when checking block device'''
1434+ """Test it ensures loopback device when checking block device"""
1435 defsize = openstack.DEFAULT_LOOPBACK_SIZE
1436 is_bd.return_value = True
1437
1438@@ -641,7 +692,7 @@
1439
1440 @patch.object(openstack, 'is_block_device')
1441 def test_ensure_standard_block_device(self, is_bd):
1442- '''Test it looks for storage at both relative and full device path'''
1443+ """Test it looks for storage at both relative and full device path"""
1444 for dev in ['vdb', '/dev/vdb']:
1445 openstack.ensure_block_device(dev)
1446 is_bd.assert_called_with('/dev/vdb')
1447@@ -649,7 +700,7 @@
1448 @patch.object(openstack, 'is_block_device')
1449 @patch.object(openstack, 'error_out')
1450 def test_ensure_nonexistent_block_device(self, error_out, is_bd):
1451- '''Test it will not ensure a non-existant block device'''
1452+ """Test it will not ensure a non-existant block device"""
1453 is_bd.return_value = False
1454 openstack.ensure_block_device(block_device='foo')
1455 self.assertTrue(error_out.called)
1456@@ -660,7 +711,7 @@
1457 @patch.object(openstack, 'zap_disk')
1458 @patch.object(openstack, 'is_lvm_physical_volume')
1459 def test_clean_storage_unmount(self, is_pv, zap_disk, mounts, umount, log):
1460- '''Test it unmounts block device when cleaning storage'''
1461+ """Test it unmounts block device when cleaning storage"""
1462 is_pv.return_value = False
1463 zap_disk.return_value = True
1464 mounts.return_value = MOUNTS
1465@@ -673,7 +724,7 @@
1466 @patch.object(openstack, 'mounts')
1467 @patch.object(openstack, 'is_lvm_physical_volume')
1468 def test_clean_storage_lvm_wipe(self, is_pv, mounts, rm_lv, rm_vg, log):
1469- '''Test it removes traces of LVM when cleaning storage'''
1470+ """Test it removes traces of LVM when cleaning storage"""
1471 mounts.return_value = []
1472 is_pv.return_value = True
1473 openstack.clean_storage('/dev/vdb')
1474@@ -684,7 +735,7 @@
1475 @patch.object(openstack, 'is_lvm_physical_volume')
1476 @patch.object(openstack, 'mounts')
1477 def test_clean_storage_zap_disk(self, mounts, is_pv, zap_disk):
1478- '''It removes traces of LVM when cleaning storage'''
1479+ """It removes traces of LVM when cleaning storage"""
1480 mounts.return_value = []
1481 is_pv.return_value = False
1482 openstack.clean_storage('/dev/vdb')
1483
1484=== modified file 'tests/contrib/openstack/test_os_utils.py'
1485--- tests/contrib/openstack/test_os_utils.py 2017-03-24 16:50:31 +0000
1486+++ tests/contrib/openstack/test_os_utils.py 2017-04-10 18:12:16 +0000
1487@@ -126,7 +126,7 @@
1488 mock_git_os_codename_install_source,
1489 mock_config):
1490 # Wipe the modules cached os_rel
1491- utils.os_rel = None
1492+ utils._os_rel = None
1493 mock_get_os_codename_install_source.return_value = None
1494 mock_get_os_codename_package.return_value = None
1495 mock_git_os_codename_install_source.return_value = None
1496
1497=== modified file 'tests/fetch/test_fetch.py'
1498--- tests/fetch/test_fetch.py 2017-03-03 20:18:47 +0000
1499+++ tests/fetch/test_fetch.py 2017-04-10 18:12:16 +0000
1500@@ -1,25 +1,22 @@
1501-import subprocess
1502 import six
1503 import os
1504 import yaml
1505-import imp
1506-import tempfile
1507
1508-from charmhelpers import osplatform
1509-from tests.helpers import patch_open
1510 from testtools import TestCase
1511 from mock import (
1512 patch,
1513 MagicMock,
1514 call,
1515- sentinel,
1516 )
1517+
1518 from charmhelpers import fetch
1519-from six.moves import StringIO
1520+
1521 if six.PY3:
1522 from urllib.parse import urlparse
1523+ builtin_open = 'builtins.open'
1524 else:
1525 from urlparse import urlparse
1526+ builtin_open = '__builtin__.open'
1527
1528
1529 FAKE_APT_CACHE = {
1530@@ -60,479 +57,7 @@
1531
1532 class FetchTest(TestCase):
1533
1534- @patch("charmhelpers.fetch.ubuntu.log")
1535- @patch.object(osplatform, 'get_platform')
1536- @patch('apt_pkg.Cache')
1537- def test_filter_packages_missing_ubuntu(self, cache, platform, log):
1538- platform.return_value = 'ubuntu'
1539- imp.reload(fetch)
1540-
1541- cache.side_effect = fake_apt_cache
1542- result = fetch.filter_installed_packages(['vim', 'emacs'])
1543- self.assertEquals(result, ['emacs'])
1544-
1545- @patch("charmhelpers.fetch.centos.log")
1546- @patch('yum.YumBase.doPackageLists')
1547- @patch.object(osplatform, 'get_platform')
1548- def test_filter_packages_missing_centos(self, platform, yumBase, log):
1549- platform.return_value = 'centos'
1550- imp.reload(fetch)
1551-
1552- class MockPackage:
1553- def __init__(self, name):
1554- self.base_package_name = name
1555-
1556- yum_dict = {
1557- 'installed': {
1558- MockPackage('vim')
1559- },
1560- 'available': {
1561- MockPackage('vim')
1562- }
1563- }
1564- import yum
1565- yum.YumBase.return_value.doPackageLists.return_value = yum_dict
1566- result = fetch.filter_installed_packages(['vim', 'emacs'])
1567- self.assertEquals(result, ['emacs'])
1568-
1569- @patch("charmhelpers.fetch.ubuntu.log")
1570- @patch.object(osplatform, 'get_platform')
1571- @patch('apt_pkg.Cache')
1572- def test_filter_packages_none_missing_ubuntu(self, cache, platform, log):
1573- platform.return_value = 'ubuntu'
1574- imp.reload(fetch)
1575-
1576- cache.side_effect = fake_apt_cache
1577- result = fetch.filter_installed_packages(['vim'])
1578- self.assertEquals(result, [])
1579-
1580- @patch("charmhelpers.fetch.centos.log")
1581- @patch.object(osplatform, 'get_platform')
1582- def test_filter_packages_none_missing_centos(self, platform, log):
1583- platform.return_value = 'centos'
1584- imp.reload(fetch)
1585-
1586- class MockPackage:
1587- def __init__(self, name):
1588- self.base_package_name = name
1589-
1590- yum_dict = {
1591- 'installed': {
1592- MockPackage('vim')
1593- },
1594- 'available': {
1595- MockPackage('vim')
1596- }
1597- }
1598- import yum
1599- yum.yumBase.return_value.doPackageLists.return_value = yum_dict
1600- result = fetch.filter_installed_packages(['vim'])
1601- self.assertEquals(result, [])
1602-
1603- @patch.object(osplatform, 'get_platform')
1604- @patch('charmhelpers.fetch.ubuntu.log')
1605- @patch('apt_pkg.Cache')
1606- def test_filter_packages_not_available_ubuntu(self, cache, log, platform):
1607- platform.return_value = 'ubuntu'
1608- imp.reload(fetch)
1609-
1610- cache.side_effect = fake_apt_cache
1611- result = fetch.filter_installed_packages(['vim', 'joe'])
1612- self.assertEquals(result, ['joe'])
1613- log.assert_called_with('Package joe has no installation candidate.',
1614- level='WARNING')
1615-
1616- @patch.object(osplatform, 'get_platform')
1617- @patch('charmhelpers.fetch.centos.log')
1618- @patch('yum.YumBase.doPackageLists')
1619- def test_filter_packages_not_available_centos(self, yumBase,
1620- log, platform):
1621- platform.return_value = 'centos'
1622- imp.reload(fetch)
1623-
1624- class MockPackage:
1625- def __init__(self, name):
1626- self.base_package_name = name
1627-
1628- yum_dict = {
1629- 'installed': {
1630- MockPackage('vim')
1631- }
1632- }
1633- import yum
1634- yum.YumBase.return_value.doPackageLists.return_value = yum_dict
1635-
1636- result = fetch.filter_installed_packages(['vim', 'joe'])
1637- self.assertEquals(result, ['joe'])
1638-
1639- @patch.object(osplatform, 'get_platform')
1640- @patch('charmhelpers.fetch.ubuntu.log')
1641- def test_add_source_none_ubuntu(self, log, platform):
1642- platform.return_value = 'ubuntu'
1643- imp.reload(fetch)
1644-
1645- fetch.add_source(source=None)
1646- self.assertTrue(log.called)
1647-
1648- @patch.object(osplatform, 'get_platform')
1649- @patch('charmhelpers.fetch.centos.log')
1650- def test_add_source_none_centos(self, log, platform):
1651- platform.return_value = 'centos'
1652- imp.reload(fetch)
1653-
1654- fetch.add_source(source=None)
1655- self.assertTrue(log.called)
1656-
1657- @patch.object(osplatform, 'get_platform')
1658- @patch('subprocess.check_call')
1659- def test_add_source_ppa(self, check_call, platform):
1660- platform.return_value = 'ubuntu'
1661- imp.reload(fetch)
1662-
1663- source = "ppa:test-ppa"
1664- fetch.add_source(source=source)
1665- check_call.assert_called_with(
1666- ['add-apt-repository', '--yes', source], env=getenv())
1667-
1668- @patch("charmhelpers.fetch.ubuntu.log")
1669- @patch.object(osplatform, 'get_platform')
1670- @patch('subprocess.check_call')
1671- @patch('time.sleep')
1672- def test_add_source_ppa_retries_30_times(self, sleep, check_call,
1673- platform, log):
1674- platform.return_value = 'ubuntu'
1675- imp.reload(fetch)
1676-
1677- self.call_count = 0
1678-
1679- def side_effect(*args, **kwargs):
1680- """Raise an 3 times, then return 0 """
1681- self.call_count += 1
1682- if self.call_count <= fetch.ubuntu.CMD_RETRY_COUNT:
1683- raise subprocess.CalledProcessError(
1684- returncode=1, cmd="some add-apt-repository command")
1685- else:
1686- return 0
1687- check_call.side_effect = side_effect
1688-
1689- source = "ppa:test-ppa"
1690- fetch.add_source(source=source)
1691- check_call.assert_called_with(
1692- ['add-apt-repository', '--yes', source], env=getenv())
1693- sleep.assert_called_with(10)
1694- self.assertTrue(fetch.ubuntu.CMD_RETRY_COUNT, sleep.call_count)
1695-
1696- @patch('charmhelpers.fetch.ubuntu.log')
1697- @patch.object(osplatform, 'get_platform')
1698- @patch('subprocess.check_call')
1699- def test_add_source_http_ubuntu(self, check_call, platform, log):
1700- platform.return_value = 'ubuntu'
1701- imp.reload(fetch)
1702-
1703- source = "http://archive.ubuntu.com/ubuntu raring-backports main"
1704- fetch.add_source(source=source)
1705- check_call.assert_called_with(
1706- ['add-apt-repository', '--yes', source], env=getenv())
1707-
1708- @patch('charmhelpers.fetch.centos.log')
1709- @patch.object(osplatform, 'get_platform')
1710- @patch('os.listdir')
1711- def test_add_source_http_centos(self, listdir, platform, log):
1712- platform.return_value = 'centos'
1713- imp.reload(fetch)
1714-
1715- source = "http://archive.ubuntu.com/ubuntu raring-backports main"
1716- with patch_open() as (mock_open, mock_file):
1717- fetch.add_source(source=source)
1718- listdir.assert_called_with('/etc/yum.repos.d/')
1719- mock_file.write.assert_has_calls([
1720- call("[archive.ubuntu.com_ubuntu raring-backports main]\n"),
1721- call("name=archive.ubuntu.com/ubuntu raring-backports main\n"),
1722- call("baseurl=http://archive.ubuntu.com/ubuntu raring"
1723- "-backports main\n\n")])
1724-
1725- @patch('charmhelpers.fetch.ubuntu.log')
1726- @patch.object(osplatform, 'get_platform')
1727- @patch('subprocess.check_call')
1728- def test_add_source_https(self, check_call, platform, log):
1729- platform.return_value = 'ubuntu'
1730- imp.reload(fetch)
1731-
1732- source = "https://example.com"
1733- fetch.add_source(source=source)
1734- check_call.assert_called_with(
1735- ['add-apt-repository', '--yes', source], env=getenv())
1736-
1737- @patch('charmhelpers.fetch.ubuntu.log')
1738- @patch.object(osplatform, 'get_platform')
1739- @patch('subprocess.check_call')
1740- def test_add_source_deb(self, check_call, platform, log):
1741- """add-apt-repository behaves differently when using the deb prefix.
1742-
1743- $ add-apt-repository --yes "http://special.example.com/ubuntu
1744- precise-special main"
1745- $ grep special /etc/apt/sources.list
1746- deb http://special.example.com/ubuntu precise precise-special main
1747- deb-src http://special.example.com/ubuntu precise precise-special main
1748-
1749- $ add-apt-repository --yes "deb http://special.example.com/ubuntu
1750- precise-special main"
1751- $ grep special /etc/apt/sources.list
1752- deb http://special.example.com/ubuntu precise precise-special main
1753- deb-src http://special.example.com/ubuntu precise precise-special main
1754- deb http://special.example.com/ubuntu precise-special main
1755- deb-src http://special.example.com/ubuntu precise-special main
1756- """
1757- platform.return_value = 'ubuntu'
1758- imp.reload(fetch)
1759-
1760- source = "deb http://archive.ubuntu.com/ubuntu raring-backports main"
1761- fetch.add_source(source=source)
1762- check_call.assert_called_with(
1763- ['add-apt-repository', '--yes', source], env=getenv())
1764-
1765- @patch.object(osplatform, 'get_platform')
1766- @patch.object(fetch.ubuntu, 'filter_installed_packages')
1767- @patch.object(fetch.ubuntu, 'install')
1768- def test_add_source_cloud_invalid_pocket(self, install,
1769- filter_pkg, platform):
1770- platform.return_value = 'ubuntu'
1771- imp.reload(fetch)
1772-
1773- source = "cloud:havana-updates"
1774- self.assertRaises(fetch.ubuntu.SourceConfigError,
1775- fetch.add_source, source)
1776- filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
1777-
1778- @patch('charmhelpers.fetch.ubuntu.log')
1779- @patch.object(osplatform, 'get_platform')
1780- @patch.object(fetch.ubuntu, 'filter_installed_packages')
1781- @patch.object(fetch.ubuntu, 'install')
1782- def test_add_source_cloud_pocket_style(self, install, filter_pkg,
1783- platform, log):
1784- platform.return_value = 'ubuntu'
1785- imp.reload(fetch)
1786-
1787- source = "cloud:precise-updates/havana"
1788- result = ('# Ubuntu Cloud Archive\n'
1789- 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
1790- ' precise-updates/havana main\n')
1791-
1792- with patch_open() as (mock_open, mock_file):
1793- fetch.add_source(source=source)
1794- mock_file.write.assert_called_with(result)
1795- filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
1796-
1797- @patch('charmhelpers.fetch.ubuntu.log')
1798- @patch.object(osplatform, 'get_platform')
1799- @patch.object(fetch.ubuntu, 'filter_installed_packages')
1800- @patch.object(fetch.ubuntu, 'install')
1801- def test_add_source_cloud_os_style(self, install, filter_pkg,
1802- platform, log):
1803- platform.return_value = 'ubuntu'
1804- imp.reload(fetch)
1805-
1806- source = "cloud:precise-havana"
1807- result = ('# Ubuntu Cloud Archive\n'
1808- 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
1809- ' precise-updates/havana main\n')
1810- with patch_open() as (mock_open, mock_file):
1811- fetch.add_source(source=source)
1812- mock_file.write.assert_called_with(result)
1813- filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
1814-
1815- @patch('charmhelpers.fetch.ubuntu.log')
1816- @patch.object(osplatform, 'get_platform')
1817- @patch.object(fetch.ubuntu, 'filter_installed_packages')
1818- @patch.object(fetch.ubuntu, 'install')
1819- def test_add_source_cloud_distroless_style(self, install, filter_pkg,
1820- platform, log):
1821- platform.return_value = 'ubuntu'
1822- imp.reload(fetch)
1823-
1824- source = "cloud:havana"
1825- result = ('# Ubuntu Cloud Archive\n'
1826- 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
1827- ' precise-updates/havana main\n')
1828- with patch_open() as (mock_open, mock_file):
1829- fetch.add_source(source=source)
1830- mock_file.write.assert_called_with(result)
1831- filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
1832-
1833- @patch('charmhelpers.fetch.ubuntu.log')
1834- @patch.object(osplatform, 'get_platform')
1835- @patch.object(fetch.ubuntu, 'lsb_release')
1836- def test_add_source_proposed(self, lsb_release, platform, log):
1837- platform.return_value = 'ubuntu'
1838- imp.reload(fetch)
1839-
1840- source = "proposed"
1841- result = ('# Proposed\n'
1842- 'deb http://archive.ubuntu.com/ubuntu precise-proposed'
1843- ' main universe multiverse restricted\n')
1844- lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
1845- with patch_open() as (mock_open, mock_file):
1846- fetch.add_source(source=source)
1847- mock_file.write.assert_called_with(result)
1848-
1849- @patch('charmhelpers.fetch.ubuntu.log')
1850- @patch.object(osplatform, 'get_platform')
1851- @patch('subprocess.check_call')
1852- def test_add_source_http_and_key_id_ubuntu(self, check_call,
1853- platform, log):
1854- platform.return_value = 'ubuntu'
1855- imp.reload(fetch)
1856-
1857- source = "http://archive.ubuntu.com/ubuntu raring-backports main"
1858- key_id = "akey"
1859- check_call.return_value = 0 # Successful exit code
1860- fetch.add_source(source=source, key=key_id)
1861- check_call.assert_has_calls([
1862- call(['add-apt-repository', '--yes', source], env=getenv()),
1863- call(['apt-key', 'adv', '--keyserver',
1864- 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])
1865- ])
1866-
1867- @patch('charmhelpers.fetch.centos.log')
1868- @patch('os.listdir')
1869- @patch.object(osplatform, 'get_platform')
1870- @patch('subprocess.check_call')
1871- def test_add_source_http_and_key_id_centos(self, check_call,
1872- platform, listdir, log):
1873- platform.return_value = 'centos'
1874- imp.reload(fetch)
1875-
1876- source = "http://archive.ubuntu.com/ubuntu raring-backports main"
1877- key_id = "akey"
1878- with patch_open() as (mock_open, mock_file):
1879- fetch.add_source(source=source, key=key_id)
1880- listdir.assert_called_with('/etc/yum.repos.d/')
1881- mock_file.write.assert_has_calls([
1882- call("[archive.ubuntu.com_ubuntu raring-backports main]\n"),
1883- call("name=archive.ubuntu.com/ubuntu raring-backports main\n"),
1884- call("baseurl=http://archive.ubuntu.com/ubuntu raring"
1885- "-backports main\n\n")])
1886- check_call.assert_called_with(['rpm', '--import', key_id])
1887-
1888- @patch('charmhelpers.fetch.ubuntu.log')
1889- @patch.object(osplatform, 'get_platform')
1890- @patch('subprocess.check_call')
1891- def test_add_source_https_and_key_id_ubuntu(self, check_call,
1892- platform, log):
1893- check_call.return_value = 0 # Success from both calls
1894- platform.return_value = 'ubuntu'
1895- imp.reload(fetch)
1896-
1897- source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
1898- key_id = "GPGPGP"
1899- fetch.add_source(source=source, key=key_id)
1900- check_call.assert_has_calls([
1901- call(['add-apt-repository', '--yes', source], env=getenv()),
1902- call(['apt-key', 'adv', '--keyserver',
1903- 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])
1904- ])
1905-
1906- @patch('charmhelpers.fetch.centos.log')
1907- @patch('os.listdir')
1908- @patch.object(osplatform, 'get_platform')
1909- @patch('subprocess.check_call')
1910- def test_add_source_https_and_key_id_centos(self, check_call,
1911- platform, listdir, log):
1912- platform.return_value = 'centos'
1913- imp.reload(fetch)
1914-
1915- source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
1916- key_id = "GPGPGP"
1917- with patch_open() as (mock_open, mock_file):
1918- fetch.add_source(source=source, key=key_id)
1919- listdir.assert_called_with('/etc/yum.repos.d/')
1920- mock_file.write.assert_has_calls([
1921- call("[_USER:PASS@private-ppa.launchpad"
1922- ".net_project_awesome]\n"),
1923- call("name=/USER:PASS@private-ppa.launchpad.net"
1924- "/project/awesome\n"),
1925- call("baseurl=https://USER:PASS@private-ppa.launchpad.net"
1926- "/project/awesome\n\n")])
1927- check_call.assert_called_with(['rpm', '--import', key_id])
1928-
1929- @patch('charmhelpers.fetch.ubuntu.log')
1930- @patch.object(osplatform, 'get_platform')
1931- @patch('subprocess.check_call')
1932- def test_add_source_http_and_key_ubuntu(self, check_call, platform, log):
1933- platform.return_value = 'ubuntu'
1934- imp.reload(fetch)
1935-
1936- source = "http://archive.ubuntu.com/ubuntu raring-backports main"
1937- key = '''
1938- -----BEGIN PGP PUBLIC KEY BLOCK-----
1939- [...]
1940- -----END PGP PUBLIC KEY BLOCK-----
1941- '''
1942-
1943- received_args = []
1944- received_key = StringIO()
1945-
1946- def _check_call(arg, stdin=None, env=None):
1947- '''side_effect to store the stdin passed to check_call process.'''
1948- if stdin is not None:
1949- received_args.extend(arg)
1950- received_key.write(stdin.read())
1951- return 0 # Successful return code checked by add-apt-repository
1952-
1953- with patch('subprocess.check_call',
1954- side_effect=_check_call) as check_call:
1955- fetch.add_source(source=source, key=key)
1956- check_call.assert_any_call(
1957- ['add-apt-repository', '--yes', source], env=getenv())
1958- self.assertEqual(['apt-key', 'add', '-'], received_args)
1959- self.assertEqual(key, received_key.getvalue())
1960-
1961- @patch('charmhelpers.fetch.centos.log')
1962- @patch.object(tempfile, 'NamedTemporaryFile')
1963- @patch('os.listdir')
1964- @patch.object(osplatform, 'get_platform')
1965- @patch('subprocess.check_call')
1966- def test_add_source_http_and_key_centos(self, check_call, platform,
1967- listdir, temp_file, log):
1968- platform.return_value = 'centos'
1969- imp.reload(fetch)
1970-
1971- source = "http://archive.ubuntu.com/ubuntu raring-backports main"
1972- key = '''
1973- -----BEGIN PGP PUBLIC KEY BLOCK-----
1974- [...]
1975- -----END PGP PUBLIC KEY BLOCK-----
1976- '''
1977- temp_file.return_value.__enter__.return_value = key
1978- temp_file.return_value.__exit__.return_value = key
1979-
1980- with patch_open() as (mock_open, mock_file):
1981- fetch.add_source(source=source, key=key)
1982- listdir.assert_called_with('/etc/yum.repos.d/')
1983- self.assertTrue(log.called)
1984- check_call.assert_called_with(['rpm', '--import', key])
1985-
1986- @patch.object(osplatform, 'get_platform')
1987- @patch('charmhelpers.fetch.ubuntu.log')
1988- def test_add_unparsable_source(self, log_, platform):
1989- platform.return_value = 'ubuntu'
1990- imp.reload(fetch)
1991-
1992- source = "propsed" # Minor typo
1993- fetch.add_source(source=source)
1994- self.assertEqual(1, log_.call_count)
1995-
1996- @patch('charmhelpers.fetch.ubuntu.log')
1997- @patch.object(osplatform, 'get_platform')
1998- def test_add_distro_source(self, platform, log):
1999- platform.return_value = 'ubuntu'
2000- imp.reload(fetch)
2001-
2002- source = "distro"
2003- # distro is a noop but test validate no exception is thrown
2004- fetch.add_source(source=source)
2005-
2006- @patch('charmhelpers.fetch.ubuntu.log')
2007+ @patch('charmhelpers.fetch.log')
2008 @patch.object(fetch, 'config')
2009 @patch.object(fetch, 'add_source')
2010 def test_configure_sources_single_source(self, add_source, config, log):
2011@@ -587,12 +112,11 @@
2012 ]
2013 self.assertRaises(fetch.SourceConfigError, fetch.configure_sources)
2014
2015- @patch.object(osplatform, 'get_platform')
2016- @patch('charmhelpers.fetch.ubuntu.update')
2017+ @patch.object(fetch, '_fetch_update')
2018 @patch.object(fetch, 'config')
2019 @patch.object(fetch, 'add_source')
2020 def test_configure_sources_update_called_ubuntu(self, add_source, config,
2021- update, platform):
2022+ update):
2023 config.side_effect = ['source', 'key']
2024 fetch.configure_sources(update=True)
2025 add_source.assert_called_with('source', 'key')
2026@@ -703,7 +227,8 @@
2027 self.assertEqual(2, log_.call_count)
2028
2029 @patch('charmhelpers.fetch.log')
2030- def test_plugins_are_valid(self, log_):
2031+ @patch.object(fetch.importlib, 'import_module')
2032+ def test_plugins_are_valid(self, import_module, log_):
2033 plugins = fetch.plugins()
2034 self.assertEqual(len(fetch.FETCH_HANDLERS), len(plugins))
2035
2036@@ -742,572 +267,3 @@
2037 expected_url = "http://example.com/foo"
2038 u = self.fh.base_url(sample_url)
2039 self.assertEqual(u, expected_url)
2040-
2041-
2042-class AptTests(TestCase):
2043-
2044- @patch.object(osplatform, 'get_platform')
2045- @patch('subprocess.call')
2046- @patch('charmhelpers.fetch.ubuntu.log')
2047- def test_apt_upgrade_non_fatal(self, log, mock_call, platform):
2048- platform.return_value = 'ubuntu'
2049- imp.reload(fetch)
2050-
2051- options = ['--foo', '--bar']
2052- fetch.apt_upgrade(options)
2053-
2054- mock_call.assert_called_with(
2055- ['apt-get', '--assume-yes',
2056- '--foo', '--bar', 'upgrade'],
2057- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2058-
2059- @patch.object(osplatform, 'get_platform')
2060- @patch('subprocess.check_call')
2061- @patch('charmhelpers.fetch.ubuntu.log')
2062- def test_apt_upgrade_fatal(self, log, mock_call, platform):
2063- platform.return_value = 'ubuntu'
2064- imp.reload(fetch)
2065-
2066- options = ['--foo', '--bar']
2067- fetch.apt_upgrade(options, fatal=True)
2068-
2069- mock_call.assert_called_with(
2070- ['apt-get', '--assume-yes',
2071- '--foo', '--bar', 'upgrade'],
2072- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2073-
2074- @patch.object(osplatform, 'get_platform')
2075- @patch('subprocess.check_call')
2076- @patch('charmhelpers.fetch.ubuntu.log')
2077- def test_apt_dist_upgrade_fatal(self, log, mock_call, platform):
2078- platform.return_value = 'ubuntu'
2079- imp.reload(fetch)
2080-
2081- options = ['--foo', '--bar']
2082- fetch.apt_upgrade(options, fatal=True, dist=True)
2083-
2084- mock_call.assert_called_with(
2085- ['apt-get', '--assume-yes',
2086- '--foo', '--bar', 'dist-upgrade'],
2087- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2088-
2089- @patch.object(osplatform, 'get_platform')
2090- @patch('subprocess.call')
2091- @patch('charmhelpers.fetch.ubuntu.log')
2092- def test_installs_apt_packages(self, log, mock_call, platform):
2093- platform.return_value = 'ubuntu'
2094- imp.reload(fetch)
2095-
2096- packages = ['foo', 'bar']
2097- options = ['--foo', '--bar']
2098-
2099- fetch.apt_install(packages, options)
2100-
2101- mock_call.assert_called_with(
2102- ['apt-get', '--assume-yes',
2103- '--foo', '--bar', 'install', 'foo', 'bar'],
2104- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2105-
2106- @patch.object(osplatform, 'get_platform')
2107- @patch('subprocess.call')
2108- @patch('charmhelpers.fetch.ubuntu.log')
2109- def test_installs_apt_packages_without_options(self, log, mock_call,
2110- platform):
2111- platform.return_value = 'ubuntu'
2112- imp.reload(fetch)
2113-
2114- packages = ['foo', 'bar']
2115-
2116- fetch.apt_install(packages)
2117-
2118- mock_call.assert_called_with(
2119- ['apt-get', '--assume-yes',
2120- '--option=Dpkg::Options::=--force-confold',
2121- 'install', 'foo', 'bar'],
2122- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2123-
2124- @patch.object(osplatform, 'get_platform')
2125- @patch('subprocess.call')
2126- @patch('charmhelpers.fetch.ubuntu.log')
2127- def test_installs_apt_packages_as_string(self, log, mock_call, platform):
2128- platform.return_value = 'ubuntu'
2129- imp.reload(fetch)
2130-
2131- packages = 'foo bar'
2132- options = ['--foo', '--bar']
2133-
2134- fetch.apt_install(packages, options)
2135-
2136- mock_call.assert_called_with(
2137- ['apt-get', '--assume-yes',
2138- '--foo', '--bar', 'install', 'foo bar'],
2139- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2140-
2141- @patch.object(osplatform, 'get_platform')
2142- @patch('subprocess.check_call')
2143- @patch('charmhelpers.fetch.ubuntu.log')
2144- def test_installs_apt_packages_with_possible_errors(self, log,
2145- check_call, platform):
2146- platform.return_value = 'ubuntu'
2147- imp.reload(fetch)
2148-
2149- packages = ['foo', 'bar']
2150- options = ['--foo', '--bar']
2151-
2152- fetch.apt_install(packages, options, fatal=True)
2153-
2154- check_call.assert_called_with(
2155- ['apt-get', '--assume-yes',
2156- '--foo', '--bar', 'install', 'foo', 'bar'],
2157- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2158-
2159- @patch.object(osplatform, 'get_platform')
2160- @patch('subprocess.check_call')
2161- @patch('charmhelpers.fetch.ubuntu.log')
2162- def test_purges_apt_packages_as_string_fatal(self, log, mock_call,
2163- platform):
2164- platform.return_value = 'ubuntu'
2165- imp.reload(fetch)
2166-
2167- packages = 'irrelevant names'
2168- mock_call.side_effect = OSError('fail')
2169-
2170- self.assertRaises(OSError, fetch.apt_purge, packages, fatal=True)
2171- self.assertTrue(log.called)
2172-
2173- @patch.object(osplatform, 'get_platform')
2174- @patch('subprocess.check_call')
2175- @patch('charmhelpers.fetch.ubuntu.log')
2176- def test_purges_apt_packages_fatal(self, log, mock_call, platform):
2177- platform.return_value = 'ubuntu'
2178- imp.reload(fetch)
2179-
2180- packages = ['irrelevant', 'names']
2181- mock_call.side_effect = OSError('fail')
2182-
2183- self.assertRaises(OSError, fetch.apt_purge, packages, fatal=True)
2184- self.assertTrue(log.called)
2185-
2186- @patch.object(osplatform, 'get_platform')
2187- @patch('subprocess.call')
2188- @patch('charmhelpers.fetch.ubuntu.log')
2189- def test_purges_apt_packages_as_string_nofatal(self, log, mock_call,
2190- platform):
2191- platform.return_value = 'ubuntu'
2192- imp.reload(fetch)
2193-
2194- packages = 'foo bar'
2195-
2196- fetch.apt_purge(packages)
2197-
2198- self.assertTrue(log.called)
2199- mock_call.assert_called_with(
2200- ['apt-get', '--assume-yes', 'purge', 'foo bar'],
2201- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2202-
2203- @patch.object(osplatform, 'get_platform')
2204- @patch('subprocess.call')
2205- @patch('charmhelpers.fetch.ubuntu.log')
2206- def test_purges_apt_packages_nofatal(self, log, mock_call, platform):
2207- platform.return_value = 'ubuntu'
2208- imp.reload(fetch)
2209-
2210- packages = ['foo', 'bar']
2211-
2212- fetch.apt_purge(packages)
2213-
2214- self.assertTrue(log.called)
2215- mock_call.assert_called_with(
2216- ['apt-get', '--assume-yes', 'purge', 'foo', 'bar'],
2217- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2218-
2219- @patch.object(osplatform, 'get_platform')
2220- @patch('subprocess.check_call')
2221- @patch('charmhelpers.fetch.ubuntu.log')
2222- def test_mark_apt_packages_as_string_fatal(self, log, mock_call,
2223- platform):
2224- packages = 'irrelevant names'
2225- mock_call.side_effect = OSError('fail')
2226-
2227- self.assertRaises(OSError, fetch.apt_mark, packages, sentinel.mark,
2228- fatal=True)
2229- self.assertTrue(log.called)
2230-
2231- @patch.object(osplatform, 'get_platform')
2232- @patch('subprocess.check_call')
2233- @patch('charmhelpers.fetch.ubuntu.log')
2234- def test_mark_apt_packages_fatal(self, log, mock_call, platform):
2235- platform.return_value = 'ubuntu'
2236- imp.reload(fetch)
2237-
2238- packages = ['irrelevant', 'names']
2239- mock_call.side_effect = OSError('fail')
2240-
2241- self.assertRaises(OSError, fetch.apt_mark, packages, sentinel.mark,
2242- fatal=True)
2243- self.assertTrue(log.called)
2244-
2245- @patch.object(osplatform, 'get_platform')
2246- @patch('subprocess.call')
2247- @patch('charmhelpers.fetch.ubuntu.log')
2248- def test_mark_apt_packages_as_string_nofatal(self, log, mock_call,
2249- platform):
2250- platform.return_value = 'ubuntu'
2251- imp.reload(fetch)
2252-
2253- packages = 'foo bar'
2254-
2255- fetch.apt_mark(packages, sentinel.mark)
2256-
2257- self.assertTrue(log.called)
2258- mock_call.assert_called_with(
2259- ['apt-mark', sentinel.mark, 'foo bar'],
2260- universal_newlines=True)
2261-
2262- @patch.object(osplatform, 'get_platform')
2263- @patch('subprocess.call')
2264- @patch('charmhelpers.fetch.ubuntu.log')
2265- def test_mark_apt_packages_nofatal(self, log, mock_call,
2266- platform):
2267- platform.return_value = 'ubuntu'
2268- imp.reload(fetch)
2269-
2270- packages = ['foo', 'bar']
2271-
2272- fetch.apt_mark(packages, sentinel.mark)
2273-
2274- self.assertTrue(log.called)
2275- mock_call.assert_called_with(
2276- ['apt-mark', sentinel.mark, 'foo', 'bar'],
2277- universal_newlines=True)
2278-
2279- @patch.object(osplatform, 'get_platform')
2280- @patch('subprocess.check_call')
2281- @patch('charmhelpers.fetch.ubuntu.log')
2282- def test_mark_apt_packages_nofatal_abortonfatal(self, log, mock_call,
2283- platform):
2284- platform.return_value = 'ubuntu'
2285- imp.reload(fetch)
2286-
2287- packages = ['foo', 'bar']
2288-
2289- fetch.apt_mark(packages, sentinel.mark, fatal=True)
2290-
2291- self.assertTrue(log.called)
2292- mock_call.assert_called_with(
2293- ['apt-mark', sentinel.mark, 'foo', 'bar'],
2294- universal_newlines=True)
2295-
2296- @patch.object(osplatform, 'get_platform')
2297- @patch('charmhelpers.fetch.ubuntu.apt_mark')
2298- def test_apt_hold(self, apt_mark, platform):
2299- platform.return_value = 'ubuntu'
2300- imp.reload(fetch)
2301-
2302- fetch.apt_hold(sentinel.packages)
2303- apt_mark.assert_called_once_with(sentinel.packages, 'hold',
2304- fatal=False)
2305-
2306- @patch.object(osplatform, 'get_platform')
2307- @patch('charmhelpers.fetch.ubuntu.apt_mark')
2308- def test_apt_hold_fatal(self, apt_mark, platform):
2309- platform.return_value = 'ubuntu'
2310- imp.reload(fetch)
2311-
2312- fetch.apt_hold(sentinel.packages, fatal=sentinel.fatal)
2313- apt_mark.assert_called_once_with(sentinel.packages, 'hold',
2314- fatal=sentinel.fatal)
2315-
2316- @patch.object(osplatform, 'get_platform')
2317- @patch('charmhelpers.fetch.ubuntu.apt_mark')
2318- def test_apt_unhold(self, apt_mark, platform):
2319- platform.return_value = 'ubuntu'
2320- imp.reload(fetch)
2321-
2322- fetch.apt_unhold(sentinel.packages)
2323- apt_mark.assert_called_once_with(sentinel.packages, 'unhold',
2324- fatal=False)
2325-
2326- @patch.object(osplatform, 'get_platform')
2327- @patch('charmhelpers.fetch.ubuntu.apt_mark')
2328- def test_apt_unhold_fatal(self, apt_mark, platform):
2329- platform.return_value = 'ubuntu'
2330- imp.reload(fetch)
2331-
2332- fetch.apt_unhold(sentinel.packages, fatal=sentinel.fatal)
2333- apt_mark.assert_called_once_with(sentinel.packages, 'unhold',
2334- fatal=sentinel.fatal)
2335-
2336- @patch.object(osplatform, 'get_platform')
2337- @patch('subprocess.check_call')
2338- def test_apt_update_fatal(self, check_call, platform):
2339- platform.return_value = 'ubuntu'
2340- imp.reload(fetch)
2341-
2342- fetch.apt_update(fatal=True)
2343- check_call.assert_called_with(
2344- ['apt-get', 'update'],
2345- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2346-
2347- @patch.object(osplatform, 'get_platform')
2348- @patch('subprocess.call')
2349- def test_apt_update_nonfatal(self, call, platform):
2350- platform.return_value = 'ubuntu'
2351- imp.reload(fetch)
2352-
2353- fetch.apt_update()
2354- call.assert_called_with(
2355- ['apt-get', 'update'],
2356- env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
2357-
2358- @patch.object(osplatform, 'get_platform')
2359- @patch('subprocess.check_call')
2360- @patch('time.sleep')
2361- def test_run_apt_command_retries_if_fatal(self, check_call,
2362- sleep, platform):
2363- """The _run_apt_command function retries the command if it can't get
2364- the APT lock."""
2365- platform.return_value = 'ubuntu'
2366- imp.reload(fetch)
2367-
2368- self.called = False
2369-
2370- def side_effect(*args, **kwargs):
2371- """
2372- First, raise an exception (can't acquire lock), then return 0
2373- (the lock is grabbed).
2374- """
2375- if not self.called:
2376- self.called = True
2377- raise subprocess.CalledProcessError(
2378- returncode=100, cmd="some command")
2379- else:
2380- return 0
2381-
2382- check_call.side_effect = side_effect
2383- check_call.return_value = 0
2384-
2385- from charmhelpers.fetch.ubuntu import _run_apt_command
2386- _run_apt_command(["some", "command"], fatal=True)
2387- self.assertTrue(sleep.called)
2388-
2389-
2390-class YumTests(TestCase):
2391-
2392- @patch.object(osplatform, 'get_platform')
2393- @patch('subprocess.call')
2394- @patch('charmhelpers.fetch.centos.log')
2395- def test_yum_upgrade_non_fatal(self, log, mock_call, platform):
2396- platform.return_value = 'centos'
2397- imp.reload(fetch)
2398-
2399- options = ['--foo', '--bar']
2400- fetch.upgrade(options)
2401-
2402- mock_call.assert_called_with(['yum', '--assumeyes',
2403- '--foo', '--bar', 'upgrade'],
2404- env=getenv())
2405-
2406- @patch.object(osplatform, 'get_platform')
2407- @patch('subprocess.call')
2408- @patch('charmhelpers.fetch.centos.log')
2409- def test_yum_upgrade_fatal(self, log, mock_call, platform):
2410- platform.return_value = 'centos'
2411- imp.reload(fetch)
2412-
2413- options = ['--foo', '--bar']
2414- fetch.upgrade(options, fatal=True)
2415-
2416- mock_call.assert_called_with(['yum', '--assumeyes',
2417- '--foo', '--bar', 'upgrade'],
2418- env=getenv())
2419-
2420- @patch.object(osplatform, 'get_platform')
2421- @patch('subprocess.call')
2422- @patch('charmhelpers.fetch.centos.log')
2423- def test_installs_yum_packages(self, log, mock_call, platform):
2424- platform.return_value = 'centos'
2425- imp.reload(fetch)
2426-
2427- packages = ['foo', 'bar']
2428- options = ['--foo', '--bar']
2429-
2430- fetch.install(packages, options)
2431-
2432- mock_call.assert_called_with(['yum', '--assumeyes',
2433- '--foo', '--bar', 'install',
2434- 'foo', 'bar'],
2435- env=getenv())
2436-
2437- @patch.object(osplatform, 'get_platform')
2438- @patch('subprocess.call')
2439- @patch('charmhelpers.fetch.centos.log')
2440- def test_installs_yum_packages_without_options(self, log, mock_call,
2441- platform):
2442- platform.return_value = 'centos'
2443- imp.reload(fetch)
2444-
2445- packages = ['foo', 'bar']
2446- fetch.install(packages)
2447-
2448- mock_call.assert_called_with(['yum', '--assumeyes',
2449- 'install', 'foo', 'bar'],
2450- env=getenv())
2451-
2452- @patch.object(osplatform, 'get_platform')
2453- @patch('subprocess.call')
2454- @patch('charmhelpers.fetch.centos.log')
2455- def test_installs_yum_packages_as_string(self, log, mock_call,
2456- platform):
2457- platform.return_value = 'centos'
2458- imp.reload(fetch)
2459-
2460- packages = 'foo bar'
2461- fetch.install(packages)
2462-
2463- mock_call.assert_called_with(['yum', '--assumeyes',
2464- 'install', 'foo bar'],
2465- env=getenv())
2466-
2467- @patch.object(osplatform, 'get_platform')
2468- @patch('subprocess.call')
2469- @patch('charmhelpers.fetch.centos.log')
2470- def test_installs_yum_packages_with_possible_errors(self, log, mock_call,
2471- platform):
2472- platform.return_value = 'centos'
2473- imp.reload(fetch)
2474-
2475- packages = ['foo', 'bar']
2476- options = ['--foo', '--bar']
2477-
2478- fetch.install(packages, options, fatal=True)
2479-
2480- mock_call.assert_called_with(['yum', '--assumeyes',
2481- '--foo', '--bar',
2482- 'install', 'foo', 'bar'],
2483- env=getenv())
2484-
2485- @patch.object(osplatform, 'get_platform')
2486- @patch('subprocess.check_call')
2487- @patch('charmhelpers.fetch.centos.log')
2488- def test_purges_yum_packages_as_string_fatal(self, log, mock_call,
2489- platform):
2490- platform.return_value = 'centos'
2491- imp.reload(fetch)
2492-
2493- packages = 'irrelevant names'
2494- mock_call.side_effect = OSError('fail')
2495-
2496- self.assertRaises(OSError, fetch.purge, packages, fatal=True)
2497- self.assertTrue(log.called)
2498-
2499- @patch.object(osplatform, 'get_platform')
2500- @patch('subprocess.check_call')
2501- @patch('charmhelpers.fetch.centos.log')
2502- def test_purges_yum_packages_fatal(self, log, mock_call, platform):
2503- platform.return_value = 'centos'
2504- imp.reload(fetch)
2505-
2506- packages = ['irrelevant', 'names']
2507- mock_call.side_effect = OSError('fail')
2508-
2509- self.assertRaises(OSError, fetch.purge, packages, fatal=True)
2510- self.assertTrue(log.called)
2511-
2512- @patch.object(osplatform, 'get_platform')
2513- @patch('subprocess.call')
2514- @patch('charmhelpers.fetch.centos.log')
2515- def test_purges_yum_packages_as_string_nofatal(self, log, mock_call,
2516- platform):
2517- platform.return_value = 'centos'
2518- imp.reload(fetch)
2519-
2520- packages = 'foo bar'
2521- fetch.purge(packages)
2522-
2523- self.assertTrue(log.called)
2524- mock_call.assert_called_with(['yum', '--assumeyes',
2525- 'remove', 'foo bar'],
2526- env=getenv())
2527-
2528- @patch.object(osplatform, 'get_platform')
2529- @patch('subprocess.call')
2530- @patch('charmhelpers.fetch.centos.log')
2531- def test_purges_yum_packages_nofatal(self, log, mock_call,
2532- platform):
2533- platform.return_value = 'centos'
2534- imp.reload(fetch)
2535-
2536- packages = ['foo', 'bar']
2537- fetch.purge(packages)
2538-
2539- self.assertTrue(log.called)
2540- mock_call.assert_called_with(['yum', '--assumeyes',
2541- 'remove', 'foo', 'bar'],
2542- env=getenv())
2543-
2544- @patch.object(osplatform, 'get_platform')
2545- @patch('subprocess.check_call')
2546- @patch('charmhelpers.fetch.centos.log')
2547- def test_yum_update_fatal(self, log, check_call, platform):
2548- platform.return_value = 'centos'
2549- imp.reload(fetch)
2550-
2551- fetch.update(fatal=True)
2552- check_call.assert_called_with(['yum', '--assumeyes', 'update'],
2553- env=getenv())
2554- self.assertTrue(log.called)
2555-
2556- @patch.object(osplatform, 'get_platform')
2557- @patch('subprocess.check_output')
2558- @patch('charmhelpers.fetch.centos.log')
2559- def test_yum_search(self, log, check_output, platform):
2560- platform.return_value = 'centos'
2561- imp.reload(fetch)
2562-
2563- package = ['irrelevant']
2564-
2565- from charmhelpers.fetch.centos import yum_search
2566- yum_search(package)
2567- check_output.assert_called_with(['yum', 'search', 'irrelevant'])
2568- self.assertTrue(log.called)
2569-
2570- @patch.object(osplatform, 'get_platform')
2571- @patch('subprocess.check_call')
2572- @patch('time.sleep')
2573- def test_run_yum_command_retries_if_fatal(self, check_call,
2574- sleep, platform):
2575- """The _run_yum_command function retries the command if it can't get
2576- the YUM lock."""
2577- platform.return_value = 'centos'
2578- imp.reload(fetch)
2579-
2580- self.called = False
2581-
2582- def side_effect(*args, **kwargs):
2583- """
2584- First, raise an exception (can't acquire lock), then return 0
2585- (the lock is grabbed).
2586- """
2587- if not self.called:
2588- self.called = True
2589- raise subprocess.CalledProcessError(
2590- returncode=1, cmd="some command")
2591- else:
2592- return 0
2593-
2594- check_call.side_effect = side_effect
2595- check_call.return_value = 0
2596- from charmhelpers.fetch.centos import _run_yum_command
2597- _run_yum_command(["some", "command"], fatal=True)
2598- self.assertTrue(sleep.called)
2599-
2600- @patch.object(osplatform, 'get_platform')
2601- @patch('apt_pkg.Cache')
2602- def test_get_upstream_version(self, cache, platform):
2603- platform.return_value = 'ubuntu'
2604- imp.reload(fetch)
2605- cache.side_effect = fake_apt_cache
2606- self.assertEqual(fetch.get_upstream_version('vim'), '7.3.547')
2607- self.assertEqual(fetch.get_upstream_version('emacs'), None)
2608- self.assertEqual(fetch.get_upstream_version('unknown'), None)
2609
2610=== added file 'tests/fetch/test_fetch_centos.py'
2611--- tests/fetch/test_fetch_centos.py 1970-01-01 00:00:00 +0000
2612+++ tests/fetch/test_fetch_centos.py 2017-04-10 18:12:16 +0000
2613@@ -0,0 +1,315 @@
2614+import subprocess
2615+import os
2616+
2617+from tests.helpers import patch_open
2618+from testtools import TestCase
2619+from mock import (
2620+ patch,
2621+ MagicMock,
2622+ call,
2623+)
2624+from charmhelpers.fetch import centos as fetch
2625+
2626+
2627+def getenv(update=None):
2628+ # return a copy of os.environ with update applied.
2629+ # this was necessary because some modules modify os.environment directly
2630+ copy = os.environ.copy()
2631+ if update is not None:
2632+ copy.update(update)
2633+ return copy
2634+
2635+
2636+class FetchTest(TestCase):
2637+
2638+ @patch("charmhelpers.fetch.log")
2639+ @patch('yum.YumBase.doPackageLists')
2640+ def test_filter_packages_missing_centos(self, yumBase, log):
2641+
2642+ class MockPackage:
2643+ def __init__(self, name):
2644+ self.base_package_name = name
2645+
2646+ yum_dict = {
2647+ 'installed': {
2648+ MockPackage('vim')
2649+ },
2650+ 'available': {
2651+ MockPackage('vim')
2652+ }
2653+ }
2654+ import yum
2655+ yum.YumBase.return_value.doPackageLists.return_value = yum_dict
2656+ result = fetch.filter_installed_packages(['vim', 'emacs'])
2657+ self.assertEquals(result, ['emacs'])
2658+
2659+ @patch("charmhelpers.fetch.log")
2660+ def test_filter_packages_none_missing_centos(self, log):
2661+
2662+ class MockPackage:
2663+ def __init__(self, name):
2664+ self.base_package_name = name
2665+
2666+ yum_dict = {
2667+ 'installed': {
2668+ MockPackage('vim')
2669+ },
2670+ 'available': {
2671+ MockPackage('vim')
2672+ }
2673+ }
2674+ import yum
2675+ yum.yumBase.return_value.doPackageLists.return_value = yum_dict
2676+ result = fetch.filter_installed_packages(['vim'])
2677+ self.assertEquals(result, [])
2678+
2679+ @patch('charmhelpers.fetch.centos.log')
2680+ @patch('yum.YumBase.doPackageLists')
2681+ def test_filter_packages_not_available_centos(self, yumBase, log):
2682+
2683+ class MockPackage:
2684+ def __init__(self, name):
2685+ self.base_package_name = name
2686+
2687+ yum_dict = {
2688+ 'installed': {
2689+ MockPackage('vim')
2690+ }
2691+ }
2692+ import yum
2693+ yum.YumBase.return_value.doPackageLists.return_value = yum_dict
2694+
2695+ result = fetch.filter_installed_packages(['vim', 'joe'])
2696+ self.assertEquals(result, ['joe'])
2697+
2698+ @patch('charmhelpers.fetch.centos.log')
2699+ def test_add_source_none_centos(self, log):
2700+ fetch.add_source(source=None)
2701+ self.assertTrue(log.called)
2702+
2703+ @patch('charmhelpers.fetch.centos.log')
2704+ @patch('os.listdir')
2705+ def test_add_source_http_centos(self, listdir, log):
2706+ source = "http://archive.ubuntu.com/ubuntu raring-backports main"
2707+ with patch_open() as (mock_open, mock_file):
2708+ fetch.add_source(source=source)
2709+ listdir.assert_called_with('/etc/yum.repos.d/')
2710+ mock_file.write.assert_has_calls([
2711+ call("[archive.ubuntu.com_ubuntu raring-backports main]\n"),
2712+ call("name=archive.ubuntu.com/ubuntu raring-backports main\n"),
2713+ call("baseurl=http://archive.ubuntu.com/ubuntu raring"
2714+ "-backports main\n\n")])
2715+
2716+ @patch('charmhelpers.fetch.centos.log')
2717+ @patch('os.listdir')
2718+ @patch('subprocess.check_call')
2719+ def test_add_source_http_and_key_id_centos(self, check_call,
2720+ listdir, log):
2721+ source = "http://archive.ubuntu.com/ubuntu raring-backports main"
2722+ key_id = "akey"
2723+ with patch_open() as (mock_open, mock_file):
2724+ fetch.add_source(source=source, key=key_id)
2725+ listdir.assert_called_with('/etc/yum.repos.d/')
2726+ mock_file.write.assert_has_calls([
2727+ call("[archive.ubuntu.com_ubuntu raring-backports main]\n"),
2728+ call("name=archive.ubuntu.com/ubuntu raring-backports main\n"),
2729+ call("baseurl=http://archive.ubuntu.com/ubuntu raring"
2730+ "-backports main\n\n")])
2731+ check_call.assert_called_with(['rpm', '--import', key_id])
2732+
2733+ @patch('charmhelpers.fetch.centos.log')
2734+ @patch('os.listdir')
2735+ @patch('subprocess.check_call')
2736+ def test_add_source_https_and_key_id_centos(self, check_call,
2737+ listdir, log):
2738+ source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
2739+ key_id = "GPGPGP"
2740+ with patch_open() as (mock_open, mock_file):
2741+ fetch.add_source(source=source, key=key_id)
2742+ listdir.assert_called_with('/etc/yum.repos.d/')
2743+ mock_file.write.assert_has_calls([
2744+ call("[_USER:PASS@private-ppa.launchpad"
2745+ ".net_project_awesome]\n"),
2746+ call("name=/USER:PASS@private-ppa.launchpad.net"
2747+ "/project/awesome\n"),
2748+ call("baseurl=https://USER:PASS@private-ppa.launchpad.net"
2749+ "/project/awesome\n\n")])
2750+ check_call.assert_called_with(['rpm', '--import', key_id])
2751+
2752+ @patch('charmhelpers.fetch.centos.log')
2753+ @patch.object(fetch, 'NamedTemporaryFile')
2754+ @patch('os.listdir')
2755+ @patch('subprocess.check_call')
2756+ def test_add_source_http_and_key_centos(self, check_call,
2757+ listdir, temp_file, log):
2758+ source = "http://archive.ubuntu.com/ubuntu raring-backports main"
2759+ key = '''
2760+ -----BEGIN PGP PUBLIC KEY BLOCK-----
2761+ [...]
2762+ -----END PGP PUBLIC KEY BLOCK-----
2763+ '''
2764+ file_mock = MagicMock()
2765+ file_mock.name = 'temporary_file'
2766+ temp_file.return_value.__enter__.return_value = file_mock
2767+ listdir.return_value = []
2768+
2769+ with patch_open() as (mock_open, mock_file):
2770+ fetch.add_source(source=source, key=key)
2771+ listdir.assert_called_with('/etc/yum.repos.d/')
2772+ self.assertTrue(log.called)
2773+ check_call.assert_called_with(['rpm', '--import', file_mock.name])
2774+ file_mock.write.assert_called_once_with(key)
2775+ file_mock.flush.assert_called_once_with()
2776+ file_mock.seek.assert_called_once_with(0)
2777+
2778+
2779+class YumTests(TestCase):
2780+
2781+ @patch('subprocess.call')
2782+ @patch('charmhelpers.fetch.centos.log')
2783+ def test_yum_upgrade_non_fatal(self, log, mock_call):
2784+ options = ['--foo', '--bar']
2785+ fetch.upgrade(options)
2786+
2787+ mock_call.assert_called_with(['yum', '--assumeyes',
2788+ '--foo', '--bar', 'upgrade'],
2789+ env=getenv())
2790+
2791+ @patch('subprocess.check_call')
2792+ @patch('charmhelpers.fetch.centos.log')
2793+ def test_yum_upgrade_fatal(self, log, mock_call):
2794+ options = ['--foo', '--bar']
2795+ fetch.upgrade(options, fatal=True)
2796+
2797+ mock_call.assert_called_with(['yum', '--assumeyes',
2798+ '--foo', '--bar', 'upgrade'],
2799+ env=getenv())
2800+
2801+ @patch('subprocess.call')
2802+ @patch('charmhelpers.fetch.centos.log')
2803+ def test_installs_yum_packages(self, log, mock_call):
2804+ packages = ['foo', 'bar']
2805+ options = ['--foo', '--bar']
2806+
2807+ fetch.install(packages, options)
2808+
2809+ mock_call.assert_called_with(['yum', '--assumeyes',
2810+ '--foo', '--bar', 'install',
2811+ 'foo', 'bar'],
2812+ env=getenv())
2813+
2814+ @patch('subprocess.call')
2815+ @patch('charmhelpers.fetch.centos.log')
2816+ def test_installs_yum_packages_without_options(self, log, mock_call):
2817+ packages = ['foo', 'bar']
2818+ fetch.install(packages)
2819+
2820+ mock_call.assert_called_with(['yum', '--assumeyes',
2821+ 'install', 'foo', 'bar'],
2822+ env=getenv())
2823+
2824+ @patch('subprocess.call')
2825+ @patch('charmhelpers.fetch.centos.log')
2826+ def test_installs_yum_packages_as_string(self, log, mock_call):
2827+ packages = 'foo bar'
2828+ fetch.install(packages)
2829+
2830+ mock_call.assert_called_with(['yum', '--assumeyes',
2831+ 'install', 'foo bar'],
2832+ env=getenv())
2833+
2834+ @patch('subprocess.check_call')
2835+ @patch('charmhelpers.fetch.centos.log')
2836+ def test_installs_yum_packages_with_possible_errors(self, log, mock_call):
2837+ packages = ['foo', 'bar']
2838+ options = ['--foo', '--bar']
2839+
2840+ fetch.install(packages, options, fatal=True)
2841+
2842+ mock_call.assert_called_with(['yum', '--assumeyes',
2843+ '--foo', '--bar',
2844+ 'install', 'foo', 'bar'],
2845+ env=getenv())
2846+
2847+ @patch('subprocess.check_call')
2848+ @patch('charmhelpers.fetch.centos.log')
2849+ def test_purges_yum_packages_as_string_fatal(self, log, mock_call):
2850+ packages = 'irrelevant names'
2851+ mock_call.side_effect = OSError('fail')
2852+
2853+ self.assertRaises(OSError, fetch.purge, packages, fatal=True)
2854+ self.assertTrue(log.called)
2855+
2856+ @patch('subprocess.check_call')
2857+ @patch('charmhelpers.fetch.centos.log')
2858+ def test_purges_yum_packages_fatal(self, log, mock_call):
2859+ packages = ['irrelevant', 'names']
2860+ mock_call.side_effect = OSError('fail')
2861+
2862+ self.assertRaises(OSError, fetch.purge, packages, fatal=True)
2863+ self.assertTrue(log.called)
2864+
2865+ @patch('subprocess.call')
2866+ @patch('charmhelpers.fetch.centos.log')
2867+ def test_purges_yum_packages_as_string_nofatal(self, log, mock_call):
2868+ packages = 'foo bar'
2869+ fetch.purge(packages)
2870+
2871+ self.assertTrue(log.called)
2872+ mock_call.assert_called_with(['yum', '--assumeyes',
2873+ 'remove', 'foo bar'],
2874+ env=getenv())
2875+
2876+ @patch('subprocess.call')
2877+ @patch('charmhelpers.fetch.centos.log')
2878+ def test_purges_yum_packages_nofatal(self, log, mock_call):
2879+ packages = ['foo', 'bar']
2880+ fetch.purge(packages)
2881+
2882+ self.assertTrue(log.called)
2883+ mock_call.assert_called_with(['yum', '--assumeyes',
2884+ 'remove', 'foo', 'bar'],
2885+ env=getenv())
2886+
2887+ @patch('subprocess.check_call')
2888+ @patch('charmhelpers.fetch.centos.log')
2889+ def test_yum_update_fatal(self, log, check_call):
2890+ fetch.update(fatal=True)
2891+ check_call.assert_called_with(['yum', '--assumeyes', 'update'],
2892+ env=getenv())
2893+ self.assertTrue(log.called)
2894+
2895+ @patch('subprocess.check_output')
2896+ @patch('charmhelpers.fetch.centos.log')
2897+ def test_yum_search(self, log, check_output):
2898+ package = ['irrelevant']
2899+
2900+ from charmhelpers.fetch.centos import yum_search
2901+ yum_search(package)
2902+ check_output.assert_called_with(['yum', 'search', 'irrelevant'])
2903+ self.assertTrue(log.called)
2904+
2905+ @patch('subprocess.check_call')
2906+ @patch('time.sleep')
2907+ def test_run_yum_command_retries_if_fatal(self, check_call, sleep):
2908+ """The _run_yum_command function retries the command if it can't get
2909+ the YUM lock."""
2910+ self.called = False
2911+
2912+ def side_effect(*args, **kwargs):
2913+ """
2914+ First, raise an exception (can't acquire lock), then return 0
2915+ (the lock is grabbed).
2916+ """
2917+ if not self.called:
2918+ self.called = True
2919+ raise subprocess.CalledProcessError(
2920+ returncode=1, cmd="some command")
2921+ else:
2922+ return 0
2923+
2924+ check_call.side_effect = side_effect
2925+ check_call.return_value = 0
2926+ from charmhelpers.fetch.centos import _run_yum_command
2927+ _run_yum_command(["some", "command"], fatal=True)
2928+ self.assertTrue(sleep.called)
2929
2930=== added file 'tests/fetch/test_fetch_ubuntu.py'
2931--- tests/fetch/test_fetch_ubuntu.py 1970-01-01 00:00:00 +0000
2932+++ tests/fetch/test_fetch_ubuntu.py 2017-04-10 18:12:16 +0000
2933@@ -0,0 +1,712 @@
2934+import six
2935+import subprocess
2936+import io
2937+import os
2938+import tempfile
2939+
2940+from tests.helpers import patch_open
2941+from testtools import TestCase
2942+from mock import (
2943+ patch,
2944+ MagicMock,
2945+ call,
2946+ sentinel,
2947+ ANY,
2948+)
2949+from charmhelpers.fetch import ubuntu as fetch
2950+
2951+if six.PY3:
2952+ builtin_open = 'builtins.open'
2953+else:
2954+ builtin_open = '__builtin__.open'
2955+
2956+# mocked return of openstack.lsb_release()
2957+FAKE_RELEASE = {
2958+ 'DISTRIB_CODENAME': 'precise',
2959+ 'DISTRIB_RELEASE': '12.04',
2960+ 'DISTRIB_ID': 'Ubuntu',
2961+ 'DISTRIB_DESCRIPTION': '"Ubuntu 12.04"'
2962+}
2963+
2964+url = 'deb ' + fetch.CLOUD_ARCHIVE_URL
2965+UCA_SOURCES = [
2966+ ('cloud:precise-folsom/proposed', url + ' precise-proposed/folsom main'),
2967+ ('cloud:precise-folsom', url + ' precise-updates/folsom main'),
2968+ ('cloud:precise-folsom/updates', url + ' precise-updates/folsom main'),
2969+ ('cloud:precise-grizzly/proposed', url + ' precise-proposed/grizzly main'),
2970+ ('cloud:precise-grizzly', url + ' precise-updates/grizzly main'),
2971+ ('cloud:precise-grizzly/updates', url + ' precise-updates/grizzly main'),
2972+ ('cloud:precise-havana/proposed', url + ' precise-proposed/havana main'),
2973+ ('cloud:precise-havana', url + ' precise-updates/havana main'),
2974+ ('cloud:precise-havana/updates', url + ' precise-updates/havana main'),
2975+ ('cloud:precise-icehouse/proposed',
2976+ url + ' precise-proposed/icehouse main'),
2977+ ('cloud:precise-icehouse', url + ' precise-updates/icehouse main'),
2978+ ('cloud:precise-icehouse/updates', url + ' precise-updates/icehouse main'),
2979+]
2980+
2981+PGP_KEY_ASCII_ARMOR = """-----BEGIN PGP PUBLIC KEY BLOCK-----
2982+Version: SKS 1.1.5
2983+Comment: Hostname: keyserver.ubuntu.com
2984+
2985+mI0EUCEyTAEEAMuUxyfiegCCwn4J/c0nw5PUTSJdn5FqiUTq6iMfij65xf1vl0g/Mxqw0gfg
2986+AJIsCDvO9N9dloLAwF6FUBMg5My7WyhRPTAKF505TKJboyX3Pp4J1fU1LV8QFVOp87vUh1Rz
2987+B6GU7cSglhnbL85gmbJTllkzkb3h4Yw7W+edjcQ/ABEBAAG0K0xhdW5jaHBhZCBQUEEgZm9y
2988+IFVidW50dSBDbG91ZCBBcmNoaXZlIFRlYW2IuAQTAQIAIgUCUCEyTAIbAwYLCQgHAwIGFQgC
2989+CQoLBBYCAwECHgECF4AACgkQimhEop9oEE7kJAP/eTBgq3Mhbvo0d8elMOuqZx3nmU7gSyPh
2990+ep0zYIRZ5TJWl/7PRtvp0CJA6N6ZywYTQ/4ANHhpibcHZkh8K0AzUvsGXnJRSFoJeqyDbD91
2991+EhoO+4ZfHs2HvRBQEDZILMa2OyuB497E5Mmyua3HDEOrG2cVLllsUZzpTFCx8NgeMHk=
2992+=jLBm
2993+-----END PGP PUBLIC KEY BLOCK-----
2994+"""
2995+
2996+FAKE_APT_CACHE = {
2997+ # an installed package
2998+ 'vim': {
2999+ 'current_ver': '2:7.3.547-6ubuntu5'
3000+ },
3001+ # a uninstalled installation candidate
3002+ 'emacs': {
3003+ }
3004+}
3005+
3006+
3007+def fake_apt_cache(in_memory=True, progress=None):
3008+ def _get(package):
3009+ pkg = MagicMock()
3010+ if package not in FAKE_APT_CACHE:
3011+ raise KeyError
3012+ pkg.name = package
3013+ if 'current_ver' in FAKE_APT_CACHE[package]:
3014+ pkg.current_ver.ver_str = FAKE_APT_CACHE[package]['current_ver']
3015+ else:
3016+ pkg.current_ver = None
3017+ return pkg
3018+ cache = MagicMock()
3019+ cache.__getitem__.side_effect = _get
3020+ return cache
3021+
3022+
3023+def getenv(update=None):
3024+ # return a copy of os.environ with update applied.
3025+ # this was necessary because some modules modify os.environment directly
3026+ copy = os.environ.copy()
3027+ if update is not None:
3028+ copy.update(update)
3029+ return copy
3030+
3031+
3032+class FetchTest(TestCase):
3033+
3034+ @patch("charmhelpers.fetch.ubuntu.log")
3035+ @patch('apt_pkg.Cache')
3036+ def test_filter_packages_missing_ubuntu(self, cache, log):
3037+ cache.side_effect = fake_apt_cache
3038+ result = fetch.filter_installed_packages(['vim', 'emacs'])
3039+ self.assertEquals(result, ['emacs'])
3040+
3041+ @patch("charmhelpers.fetch.ubuntu.log")
3042+ @patch('apt_pkg.Cache')
3043+ def test_filter_packages_none_missing_ubuntu(self, cache, log):
3044+ cache.side_effect = fake_apt_cache
3045+ result = fetch.filter_installed_packages(['vim'])
3046+ self.assertEquals(result, [])
3047+
3048+ @patch('charmhelpers.fetch.ubuntu.log')
3049+ @patch('apt_pkg.Cache')
3050+ def test_filter_packages_not_available_ubuntu(self, cache, log):
3051+ cache.side_effect = fake_apt_cache
3052+ result = fetch.filter_installed_packages(['vim', 'joe'])
3053+ self.assertEquals(result, ['joe'])
3054+ log.assert_called_with('Package joe has no installation candidate.',
3055+ level='WARNING')
3056+
3057+ @patch.object(fetch, 'log', lambda *args, **kwargs: None)
3058+ def test_import_apt_key_radix(self):
3059+ """Ensure shell out apt-key during key import"""
3060+ with patch('subprocess.check_call') as _subp:
3061+ fetch.import_key('foo')
3062+ cmd = ['apt-key', 'adv', '--keyserver',
3063+ 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']
3064+ _subp.assert_called_with(cmd)
3065+
3066+ @patch.object(fetch, 'log', lambda *args, **kwargs: None)
3067+ def test_import_apt_key_ascii_armor(self):
3068+ with tempfile.NamedTemporaryFile() as tmp:
3069+ with patch.object(fetch, 'NamedTemporaryFile') as mock_tmpfile:
3070+ tmpfile = mock_tmpfile.return_value
3071+ tmpfile.__enter__.return_value = tmpfile
3072+ tmpfile.name = tmp.name
3073+ with patch('subprocess.check_call') as _subp:
3074+ fetch.import_key(PGP_KEY_ASCII_ARMOR)
3075+ cmd = ['apt-key', 'add', tmp.name]
3076+ _subp.assert_called_with(cmd)
3077+ with open(tmp.name, 'r') as f:
3078+ self.assertEqual(PGP_KEY_ASCII_ARMOR, f.read())
3079+
3080+ @patch.object(fetch, 'log', lambda *args, **kwargs: None)
3081+ def test_import_bad_apt_key(self):
3082+ """Ensure error when importing apt key fails"""
3083+ with patch('subprocess.check_call') as _subp:
3084+ cmd = ['apt-key', 'adv', '--keyserver',
3085+ 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']
3086+ _subp.side_effect = subprocess.CalledProcessError(1, cmd, '')
3087+ try:
3088+ fetch.import_key('foo')
3089+ assert False
3090+ except fetch.GPGKeyError as e:
3091+ self.assertEqual(str(e), "Error importing PGP key 'foo'")
3092+
3093+ @patch('charmhelpers.fetch.ubuntu.log')
3094+ def test_add_source_none_ubuntu(self, log):
3095+ fetch.add_source(source=None)
3096+ self.assertTrue(log.called)
3097+
3098+ @patch('subprocess.check_call')
3099+ def test_add_source_ppa(self, check_call):
3100+ source = "ppa:test-ppa"
3101+ fetch.add_source(source=source)
3102+ check_call.assert_called_with(
3103+ ['add-apt-repository', '--yes', source])
3104+
3105+ @patch("charmhelpers.fetch.ubuntu.log")
3106+ @patch('subprocess.check_call')
3107+ @patch('time.sleep')
3108+ def test_add_source_ppa_retries_30_times(self, sleep, check_call, log):
3109+ self.call_count = 0
3110+
3111+ def side_effect(*args, **kwargs):
3112+ """Raise an 3 times, then return 0 """
3113+ self.call_count += 1
3114+ if self.call_count <= fetch.CMD_RETRY_COUNT:
3115+ raise subprocess.CalledProcessError(
3116+ returncode=1, cmd="some add-apt-repository command")
3117+ else:
3118+ return 0
3119+ check_call.side_effect = side_effect
3120+
3121+ source = "ppa:test-ppa"
3122+ fetch.add_source(source=source)
3123+ check_call.assert_called_with(
3124+ ['add-apt-repository', '--yes', source])
3125+ sleep.assert_called_with(10)
3126+ self.assertTrue(fetch.CMD_RETRY_COUNT, sleep.call_count)
3127+
3128+ @patch('charmhelpers.fetch.ubuntu.log')
3129+ @patch('subprocess.check_call')
3130+ def test_add_source_http_ubuntu(self, check_call, log):
3131+ source = "http://archive.ubuntu.com/ubuntu raring-backports main"
3132+ fetch.add_source(source=source)
3133+ check_call.assert_called_with(
3134+ ['add-apt-repository', '--yes', source])
3135+
3136+ @patch('charmhelpers.fetch.ubuntu.log')
3137+ @patch('subprocess.check_call')
3138+ def test_add_source_https(self, check_call, log):
3139+ source = "https://example.com"
3140+ fetch.add_source(source=source)
3141+ check_call.assert_called_with(
3142+ ['add-apt-repository', '--yes', source])
3143+
3144+ @patch('charmhelpers.fetch.ubuntu.log')
3145+ @patch('subprocess.check_call')
3146+ def test_add_source_deb(self, check_call, log):
3147+ """add-apt-repository behaves differently when using the deb prefix.
3148+
3149+ $ add-apt-repository --yes \
3150+ "http://special.example.com/ubuntu precise-special main"
3151+ $ grep special /etc/apt/sources.list
3152+ deb http://special.example.com/ubuntu precise precise-special main
3153+ deb-src http://special.example.com/ubuntu precise precise-special main
3154+
3155+ $ add-apt-repository --yes \
3156+ "deb http://special.example.com/ubuntu precise-special main"
3157+ $ grep special /etc/apt/sources.list
3158+ deb http://special.example.com/ubuntu precise precise-special main
3159+ deb-src http://special.example.com/ubuntu precise precise-special main
3160+ deb http://special.example.com/ubuntu precise-special main
3161+ deb-src http://special.example.com/ubuntu precise-special main
3162+ """
3163+ source = "deb http://archive.ubuntu.com/ubuntu raring-backports main"
3164+ fetch.add_source(source=source)
3165+ check_call.assert_called_with(
3166+ ['add-apt-repository', '--yes', source])
3167+
3168+ @patch('charmhelpers.fetch.ubuntu.log')
3169+ @patch('subprocess.check_call')
3170+ def test_add_source_http_and_key_id(self, check_call, log):
3171+ source = "http://archive.ubuntu.com/ubuntu raring-backports main"
3172+ key_id = "akey"
3173+ check_call.return_value = 0 # Successful exit code
3174+ fetch.add_source(source=source, key=key_id)
3175+ check_call.assert_has_calls([
3176+ call(['add-apt-repository', '--yes', source]),
3177+ call(['apt-key', 'adv', '--keyserver',
3178+ 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key_id])
3179+ ])
3180+
3181+ @patch('charmhelpers.fetch.ubuntu.log')
3182+ @patch('subprocess.check_call')
3183+ def test_add_source_https_and_key_id(self, check_call, log):
3184+ source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
3185+ key_id = "GPGPGP"
3186+ check_call.return_value = 0 # Success from both calls
3187+ fetch.add_source(source=source, key=key_id)
3188+ check_call.assert_has_calls([
3189+ call(['add-apt-repository', '--yes', source]),
3190+ call(['apt-key', 'adv', '--keyserver',
3191+ 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key_id])
3192+ ])
3193+
3194+ @patch('charmhelpers.fetch.ubuntu.log')
3195+ @patch('subprocess.check_call')
3196+ def test_add_source_http_and_key(self, check_call, log):
3197+ source = "http://archive.ubuntu.com/ubuntu raring-backports main"
3198+ key = '''
3199+ -----BEGIN PGP PUBLIC KEY BLOCK-----
3200+ [...]
3201+ -----END PGP PUBLIC KEY BLOCK-----
3202+ '''
3203+ with patch('subprocess.check_call') as check_call:
3204+ check_call.return_value = 0
3205+ fetch.add_source(source=source, key=key)
3206+ check_call.assert_any_call(['add-apt-repository', '--yes', source])
3207+ check_call.assert_any_call(['apt-key', 'add', ANY])
3208+
3209+ def test_add_source_cloud_invalid_pocket(self):
3210+ source = "cloud:havana-updates"
3211+ self.assertRaises(fetch.SourceConfigError,
3212+ fetch.add_source, source)
3213+
3214+ @patch('charmhelpers.fetch.ubuntu.log')
3215+ @patch.object(fetch, 'filter_installed_packages')
3216+ @patch.object(fetch, 'apt_install')
3217+ @patch.object(fetch, 'lsb_release')
3218+ def test_add_source_cloud_pocket_style(self, lsb_release, apt_install,
3219+ filter_pkg, log):
3220+ source = "cloud:precise-updates/havana"
3221+ lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
3222+ result = ('# Ubuntu Cloud Archive\n'
3223+ 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
3224+ ' precise-updates/havana main\n')
3225+
3226+ with patch_open() as (mock_open, mock_file):
3227+ fetch.add_source(source=source)
3228+ mock_file.write.assert_called_with(result)
3229+ filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
3230+
3231+ @patch('charmhelpers.fetch.ubuntu.log')
3232+ @patch.object(fetch, 'filter_installed_packages')
3233+ @patch.object(fetch, 'apt_install')
3234+ @patch.object(fetch, 'lsb_release')
3235+ def test_add_source_cloud_os_style(self, lsb_release, apt_install,
3236+ filter_pkg, log):
3237+ source = "cloud:precise-havana"
3238+ lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
3239+ result = ('# Ubuntu Cloud Archive\n'
3240+ 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
3241+ ' precise-updates/havana main\n')
3242+ with patch_open() as (mock_open, mock_file):
3243+ fetch.add_source(source=source)
3244+ mock_file.write.assert_called_with(result)
3245+ filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
3246+
3247+ @patch('charmhelpers.fetch.ubuntu.log')
3248+ @patch.object(fetch, 'filter_installed_packages')
3249+ @patch.object(fetch, 'apt_install')
3250+ def test_add_source_cloud_distroless_style(self, apt_install,
3251+ filter_pkg, log):
3252+ source = "cloud:havana"
3253+ result = ('# Ubuntu Cloud Archive\n'
3254+ 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
3255+ ' precise-updates/havana main\n')
3256+ with patch_open() as (mock_open, mock_file):
3257+ fetch.add_source(source=source)
3258+ mock_file.write.assert_called_with(result)
3259+ filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
3260+
3261+ @patch('charmhelpers.fetch.ubuntu.log')
3262+ @patch.object(fetch, 'lsb_release')
3263+ @patch('platform.machine')
3264+ def test_add_source_proposed_x86_64(self, _machine, lsb_release, log):
3265+ source = "proposed"
3266+ result = ('# Proposed\n'
3267+ 'deb http://archive.ubuntu.com/ubuntu precise-proposed'
3268+ ' main universe multiverse restricted\n')
3269+ lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
3270+ _machine.return_value = 'x86_64'
3271+ with patch_open() as (mock_open, mock_file):
3272+ fetch.add_source(source=source)
3273+ mock_file.write.assert_called_with(result)
3274+
3275+ @patch('charmhelpers.fetch.ubuntu.log')
3276+ @patch.object(fetch, 'lsb_release')
3277+ @patch('platform.machine')
3278+ def test_add_source_proposed_ppc64le(self, _machine, lsb_release, log):
3279+ source = "proposed"
3280+ result = (
3281+ "# Proposed\n"
3282+ "deb http://ports.ubuntu.com/ubuntu-ports precise-proposed main "
3283+ "universe multiverse restricted\n")
3284+ lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
3285+ _machine.return_value = 'ppc64le'
3286+ with patch_open() as (mock_open, mock_file):
3287+ fetch.add_source(source=source)
3288+ mock_file.write.assert_called_with(result)
3289+
3290+ @patch('charmhelpers.fetch.ubuntu.log')
3291+ @patch('subprocess.check_call')
3292+ def test_add_source_http_and_key_id_ubuntu(self, check_call, log):
3293+ source = "http://archive.ubuntu.com/ubuntu raring-backports main"
3294+ key_id = "akey"
3295+ fetch.add_source(source=source, key=key_id)
3296+ check_call.assert_any_call(['add-apt-repository', '--yes', source]),
3297+ check_call.assert_any_call([
3298+ 'apt-key', 'adv', '--keyserver',
3299+ 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key_id])
3300+
3301+ @patch('charmhelpers.fetch.ubuntu.log')
3302+ @patch('subprocess.check_call')
3303+ def test_add_source_https_and_key_id_ubuntu(self, check_call, log):
3304+ source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
3305+ key_id = "GPGPGP"
3306+ fetch.add_source(source=source, key=key_id)
3307+ check_call.assert_any_call(['add-apt-repository', '--yes', source]),
3308+ check_call.assert_any_call([
3309+ 'apt-key', 'adv', '--keyserver',
3310+ 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key_id])
3311+
3312+ @patch('charmhelpers.fetch.ubuntu.log')
3313+ @patch('subprocess.check_call')
3314+ def test_add_source_http_and_key_ubuntu(self, check_call, log):
3315+ source = "http://archive.ubuntu.com/ubuntu raring-backports main"
3316+ key = '''
3317+ -----BEGIN PGP PUBLIC KEY BLOCK-----
3318+ [...]
3319+ -----END PGP PUBLIC KEY BLOCK-----
3320+ '''
3321+ fetch.add_source(source=source, key=key)
3322+ check_call.assert_any_call(['add-apt-repository', '--yes', source])
3323+ check_call.assert_any_call(['apt-key', 'add', ANY])
3324+
3325+ @patch('charmhelpers.fetch.ubuntu.log')
3326+ def test_configure_bad_install_source(self, log):
3327+ try:
3328+ fetch.add_source('foo', fail_invalid=True)
3329+ self.fail("Calling add_source('foo') should fail")
3330+ except fetch.SourceConfigError as e:
3331+ self.assertEqual(str(e), "Unknown source: 'foo'")
3332+
3333+ @patch('charmhelpers.fetch.ubuntu.lsb_release')
3334+ def test_configure_install_source_uca_staging(self, _lsb):
3335+ """Test configuring installation source from UCA staging sources"""
3336+ _lsb.return_value = FAKE_RELEASE
3337+ # staging pockets are configured as PPAs
3338+ with patch('subprocess.check_call') as _subp:
3339+ src = 'cloud:precise-folsom/staging'
3340+ fetch.add_source(src)
3341+ cmd = ['add-apt-repository', '-y',
3342+ 'ppa:ubuntu-cloud-archive/folsom-staging']
3343+ _subp.assert_called_with(cmd)
3344+
3345+ @patch(builtin_open)
3346+ @patch('charmhelpers.fetch.ubuntu.apt_install')
3347+ @patch('charmhelpers.fetch.ubuntu.lsb_release')
3348+ @patch('charmhelpers.fetch.ubuntu.filter_installed_packages')
3349+ def test_configure_install_source_uca_repos(
3350+ self, _fip, _lsb, _install, _open):
3351+ """Test configuring installation source from UCA sources"""
3352+ _lsb.return_value = FAKE_RELEASE
3353+ _file = MagicMock(spec=io.FileIO)
3354+ _open.return_value = _file
3355+ _fip.side_effect = lambda x: x
3356+ for src, url in UCA_SOURCES:
3357+ actual_url = "# Ubuntu Cloud Archive\n{}\n".format(url)
3358+ fetch.add_source(src)
3359+ _install.assert_called_with(['ubuntu-cloud-keyring'],
3360+ fatal=True)
3361+ _open.assert_called_with(
3362+ '/etc/apt/sources.list.d/cloud-archive.list',
3363+ 'w'
3364+ )
3365+ _file.__enter__().write.assert_called_with(actual_url)
3366+
3367+ def test_configure_install_source_bad_uca(self):
3368+ """Test configuring installation source from bad UCA source"""
3369+ try:
3370+ fetch.add_source('cloud:foo-bar', fail_invalid=True)
3371+ self.fail("add_source('cloud:foo-bar') should fail")
3372+ except fetch.SourceConfigError as e:
3373+ _e = ('Invalid Cloud Archive release specified: foo-bar'
3374+ ' on this Ubuntuversion')
3375+ self.assertTrue(str(e).startswith(_e))
3376+
3377+ @patch('charmhelpers.fetch.ubuntu.log')
3378+ def test_add_unparsable_source(self, log_):
3379+ source = "propsed" # Minor typo
3380+ fetch.add_source(source=source)
3381+ self.assertEqual(1, log_.call_count)
3382+
3383+ @patch('charmhelpers.fetch.ubuntu.log')
3384+ def test_add_distro_source(self, log):
3385+ source = "distro"
3386+ # distro is a noop but test validate no exception is thrown
3387+ fetch.add_source(source=source)
3388+
3389+
3390+class AptTests(TestCase):
3391+
3392+ @patch('subprocess.call')
3393+ @patch('charmhelpers.fetch.ubuntu.log')
3394+ def test_apt_upgrade_non_fatal(self, log, mock_call):
3395+ options = ['--foo', '--bar']
3396+ fetch.apt_upgrade(options)
3397+
3398+ mock_call.assert_called_with(
3399+ ['apt-get', '--assume-yes',
3400+ '--foo', '--bar', 'upgrade'],
3401+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3402+
3403+ @patch('subprocess.check_call')
3404+ @patch('charmhelpers.fetch.ubuntu.log')
3405+ def test_apt_upgrade_fatal(self, log, mock_call):
3406+ options = ['--foo', '--bar']
3407+ fetch.apt_upgrade(options, fatal=True)
3408+
3409+ mock_call.assert_called_with(
3410+ ['apt-get', '--assume-yes',
3411+ '--foo', '--bar', 'upgrade'],
3412+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3413+
3414+ @patch('subprocess.check_call')
3415+ @patch('charmhelpers.fetch.ubuntu.log')
3416+ def test_apt_dist_upgrade_fatal(self, log, mock_call):
3417+ options = ['--foo', '--bar']
3418+ fetch.apt_upgrade(options, fatal=True, dist=True)
3419+
3420+ mock_call.assert_called_with(
3421+ ['apt-get', '--assume-yes',
3422+ '--foo', '--bar', 'dist-upgrade'],
3423+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3424+
3425+ @patch('subprocess.call')
3426+ @patch('charmhelpers.fetch.ubuntu.log')
3427+ def test_installs_apt_packages(self, log, mock_call):
3428+ packages = ['foo', 'bar']
3429+ options = ['--foo', '--bar']
3430+
3431+ fetch.apt_install(packages, options)
3432+
3433+ mock_call.assert_called_with(
3434+ ['apt-get', '--assume-yes',
3435+ '--foo', '--bar', 'install', 'foo', 'bar'],
3436+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3437+
3438+ @patch('subprocess.call')
3439+ @patch('charmhelpers.fetch.ubuntu.log')
3440+ def test_installs_apt_packages_without_options(self, log, mock_call):
3441+ packages = ['foo', 'bar']
3442+
3443+ fetch.apt_install(packages)
3444+
3445+ mock_call.assert_called_with(
3446+ ['apt-get', '--assume-yes',
3447+ '--option=Dpkg::Options::=--force-confold',
3448+ 'install', 'foo', 'bar'],
3449+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3450+
3451+ @patch('subprocess.call')
3452+ @patch('charmhelpers.fetch.ubuntu.log')
3453+ def test_installs_apt_packages_as_string(self, log, mock_call):
3454+ packages = 'foo bar'
3455+ options = ['--foo', '--bar']
3456+
3457+ fetch.apt_install(packages, options)
3458+
3459+ mock_call.assert_called_with(
3460+ ['apt-get', '--assume-yes',
3461+ '--foo', '--bar', 'install', 'foo bar'],
3462+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3463+
3464+ @patch('subprocess.check_call')
3465+ @patch('charmhelpers.fetch.ubuntu.log')
3466+ def test_installs_apt_packages_with_possible_errors(self, log,
3467+ check_call):
3468+ packages = ['foo', 'bar']
3469+ options = ['--foo', '--bar']
3470+
3471+ fetch.apt_install(packages, options, fatal=True)
3472+
3473+ check_call.assert_called_with(
3474+ ['apt-get', '--assume-yes',
3475+ '--foo', '--bar', 'install', 'foo', 'bar'],
3476+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3477+
3478+ @patch('subprocess.check_call')
3479+ @patch('charmhelpers.fetch.ubuntu.log')
3480+ def test_purges_apt_packages_as_string_fatal(self, log, mock_call):
3481+ packages = 'irrelevant names'
3482+ mock_call.side_effect = OSError('fail')
3483+
3484+ self.assertRaises(OSError, fetch.apt_purge, packages, fatal=True)
3485+ self.assertTrue(log.called)
3486+
3487+ @patch('subprocess.check_call')
3488+ @patch('charmhelpers.fetch.ubuntu.log')
3489+ def test_purges_apt_packages_fatal(self, log, mock_call):
3490+ packages = ['irrelevant', 'names']
3491+ mock_call.side_effect = OSError('fail')
3492+
3493+ self.assertRaises(OSError, fetch.apt_purge, packages, fatal=True)
3494+ self.assertTrue(log.called)
3495+
3496+ @patch('subprocess.call')
3497+ @patch('charmhelpers.fetch.ubuntu.log')
3498+ def test_purges_apt_packages_as_string_nofatal(self, log, mock_call):
3499+ packages = 'foo bar'
3500+
3501+ fetch.apt_purge(packages)
3502+
3503+ self.assertTrue(log.called)
3504+ mock_call.assert_called_with(
3505+ ['apt-get', '--assume-yes', 'purge', 'foo bar'],
3506+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3507+
3508+ @patch('subprocess.call')
3509+ @patch('charmhelpers.fetch.ubuntu.log')
3510+ def test_purges_apt_packages_nofatal(self, log, mock_call):
3511+ packages = ['foo', 'bar']
3512+
3513+ fetch.apt_purge(packages)
3514+
3515+ self.assertTrue(log.called)
3516+ mock_call.assert_called_with(
3517+ ['apt-get', '--assume-yes', 'purge', 'foo', 'bar'],
3518+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3519+
3520+ @patch('subprocess.check_call')
3521+ @patch('charmhelpers.fetch.ubuntu.log')
3522+ def test_mark_apt_packages_as_string_fatal(self, log, mock_call):
3523+ packages = 'irrelevant names'
3524+ mock_call.side_effect = OSError('fail')
3525+
3526+ self.assertRaises(OSError, fetch.apt_mark, packages, sentinel.mark,
3527+ fatal=True)
3528+ self.assertTrue(log.called)
3529+
3530+ @patch('subprocess.check_call')
3531+ @patch('charmhelpers.fetch.ubuntu.log')
3532+ def test_mark_apt_packages_fatal(self, log, mock_call):
3533+ packages = ['irrelevant', 'names']
3534+ mock_call.side_effect = OSError('fail')
3535+
3536+ self.assertRaises(OSError, fetch.apt_mark, packages, sentinel.mark,
3537+ fatal=True)
3538+ self.assertTrue(log.called)
3539+
3540+ @patch('subprocess.call')
3541+ @patch('charmhelpers.fetch.ubuntu.log')
3542+ def test_mark_apt_packages_as_string_nofatal(self, log, mock_call):
3543+ packages = 'foo bar'
3544+
3545+ fetch.apt_mark(packages, sentinel.mark)
3546+
3547+ self.assertTrue(log.called)
3548+ mock_call.assert_called_with(
3549+ ['apt-mark', sentinel.mark, 'foo bar'],
3550+ universal_newlines=True)
3551+
3552+ @patch('subprocess.call')
3553+ @patch('charmhelpers.fetch.ubuntu.log')
3554+ def test_mark_apt_packages_nofatal(self, log, mock_call):
3555+ packages = ['foo', 'bar']
3556+
3557+ fetch.apt_mark(packages, sentinel.mark)
3558+
3559+ self.assertTrue(log.called)
3560+ mock_call.assert_called_with(
3561+ ['apt-mark', sentinel.mark, 'foo', 'bar'],
3562+ universal_newlines=True)
3563+
3564+ @patch('subprocess.check_call')
3565+ @patch('charmhelpers.fetch.ubuntu.log')
3566+ def test_mark_apt_packages_nofatal_abortonfatal(self, log, mock_call):
3567+ packages = ['foo', 'bar']
3568+
3569+ fetch.apt_mark(packages, sentinel.mark, fatal=True)
3570+
3571+ self.assertTrue(log.called)
3572+ mock_call.assert_called_with(
3573+ ['apt-mark', sentinel.mark, 'foo', 'bar'],
3574+ universal_newlines=True)
3575+
3576+ @patch('charmhelpers.fetch.ubuntu.apt_mark')
3577+ def test_apt_hold(self, apt_mark):
3578+ fetch.apt_hold(sentinel.packages)
3579+ apt_mark.assert_called_once_with(sentinel.packages, 'hold',
3580+ fatal=False)
3581+
3582+ @patch('charmhelpers.fetch.ubuntu.apt_mark')
3583+ def test_apt_hold_fatal(self, apt_mark):
3584+ fetch.apt_hold(sentinel.packages, fatal=sentinel.fatal)
3585+ apt_mark.assert_called_once_with(sentinel.packages, 'hold',
3586+ fatal=sentinel.fatal)
3587+
3588+ @patch('charmhelpers.fetch.ubuntu.apt_mark')
3589+ def test_apt_unhold(self, apt_mark):
3590+ fetch.apt_unhold(sentinel.packages)
3591+ apt_mark.assert_called_once_with(sentinel.packages, 'unhold',
3592+ fatal=False)
3593+
3594+ @patch('charmhelpers.fetch.ubuntu.apt_mark')
3595+ def test_apt_unhold_fatal(self, apt_mark):
3596+ fetch.apt_unhold(sentinel.packages, fatal=sentinel.fatal)
3597+ apt_mark.assert_called_once_with(sentinel.packages, 'unhold',
3598+ fatal=sentinel.fatal)
3599+
3600+ @patch('subprocess.check_call')
3601+ def test_apt_update_fatal(self, check_call):
3602+ fetch.apt_update(fatal=True)
3603+ check_call.assert_called_with(
3604+ ['apt-get', 'update'],
3605+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3606+
3607+ @patch('subprocess.call')
3608+ def test_apt_update_nonfatal(self, call):
3609+ fetch.apt_update()
3610+ call.assert_called_with(
3611+ ['apt-get', 'update'],
3612+ env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
3613+
3614+ @patch('subprocess.check_call')
3615+ @patch('time.sleep')
3616+ def test_run_apt_command_retries_if_fatal(self, check_call, sleep):
3617+ """The _run_apt_command function retries the command if it can't get
3618+ the APT lock."""
3619+ self.called = False
3620+
3621+ def side_effect(*args, **kwargs):
3622+ """
3623+ First, raise an exception (can't acquire lock), then return 0
3624+ (the lock is grabbed).
3625+ """
3626+ if not self.called:
3627+ self.called = True
3628+ raise subprocess.CalledProcessError(
3629+ returncode=100, cmd="some command")
3630+ else:
3631+ return 0
3632+
3633+ check_call.side_effect = side_effect
3634+ check_call.return_value = 0
3635+
3636+ from charmhelpers.fetch.ubuntu import _run_apt_command
3637+ _run_apt_command(["some", "command"], fatal=True)
3638+ self.assertTrue(sleep.called)
3639+
3640+ @patch('apt_pkg.Cache')
3641+ def test_get_upstream_version(self, cache):
3642+ cache.side_effect = fake_apt_cache
3643+ self.assertEqual(fetch.get_upstream_version('vim'), '7.3.547')
3644+ self.assertEqual(fetch.get_upstream_version('emacs'), None)
3645+ self.assertEqual(fetch.get_upstream_version('unknown'), None)

Subscribers

People subscribed via source and target branches