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
=== modified file 'Makefile'
--- Makefile 2017-01-25 17:11:37 +0000
+++ Makefile 2017-04-10 18:12:16 +0000
@@ -27,6 +27,7 @@
27 -python setup.py clean27 -python setup.py clean
28 rm -rf build/ MANIFEST28 rm -rf build/ MANIFEST
29 find . -name '*.pyc' -delete29 find . -name '*.pyc' -delete
30 find . -name '__pycache__' -delete
30 rm -rf dist/*31 rm -rf dist/*
31 rm -rf .venv32 rm -rf .venv
32 rm -rf .venv333 rm -rf .venv3
3334
=== modified file 'charmhelpers/__init__.py'
--- charmhelpers/__init__.py 2016-07-06 14:41:05 +0000
+++ charmhelpers/__init__.py 2017-04-10 18:12:16 +0000
@@ -14,6 +14,11 @@
1414
15# Bootstrap charm-helpers, installing its dependencies if necessary using15# Bootstrap charm-helpers, installing its dependencies if necessary using
16# only standard libraries.16# only standard libraries.
17from __future__ import print_function
18from __future__ import absolute_import
19
20import functools
21import inspect
17import subprocess22import subprocess
18import sys23import sys
1924
@@ -34,3 +39,59 @@
34 else:39 else:
35 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])40 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
36 import yaml # flake8: noqa41 import yaml # flake8: noqa
42
43
44# Holds a list of mapping of mangled function names that have been deprecated
45# using the @deprecate decorator below. This is so that the warning is only
46# printed once for each usage of the function.
47__deprecated_functions = {}
48
49
50def deprecate(warning, date=None, log=None):
51 """Add a deprecation warning the first time the function is used.
52 The date, which is a string in semi-ISO8660 format indicate the year-month
53 that the function is officially going to be removed.
54
55 usage:
56
57 @deprecate('use core/fetch/add_source() instead', '2017-04')
58 def contributed_add_source_thing(...):
59 ...
60
61 And it then prints to the log ONCE that the function is deprecated.
62 The reason for passing the logging function (log) is so that hookenv.log
63 can be used for a charm if needed.
64
65 :param warning: String to indicat where it has moved ot.
66 :param date: optional sting, in YYYY-MM format to indicate when the
67 function will definitely (probably) be removed.
68 :param log: The log function to call to log. If not, logs to stdout
69 """
70 def wrap(f):
71
72 @functools.wraps(f)
73 def wrapped_f(*args, **kwargs):
74 try:
75 module = inspect.getmodule(f)
76 file = inspect.getsourcefile(f)
77 lines = inspect.getsourcelines(f)
78 f_name = "{}-{}-{}..{}-{}".format(
79 module.__name__, file, lines[0], lines[-1], f.__name__)
80 except (IOError, TypeError):
81 # assume it was local, so just use the name of the function
82 f_name = f.__name__
83 if f_name not in __deprecated_functions:
84 __deprecated_functions[f_name] = True
85 s = "DEPRECATION WARNING: Function {} is being removed".format(
86 f.__name__)
87 if date:
88 s = "{} on/around {}".format(s, date)
89 if warning:
90 s = "{} : {}".format(s, warning)
91 if log:
92 log(s)
93 else:
94 print(s)
95 return f(*args, **kwargs)
96 return wrapped_f
97 return wrap
3798
=== modified file 'charmhelpers/contrib/openstack/utils.py'
--- charmhelpers/contrib/openstack/utils.py 2017-03-24 16:50:31 +0000
+++ charmhelpers/contrib/openstack/utils.py 2017-04-10 18:12:16 +0000
@@ -26,11 +26,12 @@
26import shutil26import shutil
2727
28import six28import six
29import tempfile
30import traceback29import traceback
31import uuid30import uuid
32import yaml31import yaml
3332
33from charmhelpers import deprecate
34
34from charmhelpers.contrib.network import ip35from charmhelpers.contrib.network import ip
3536
36from charmhelpers.core import unitdata37from charmhelpers.core import unitdata
@@ -41,7 +42,6 @@
41 config,42 config,
42 log as juju_log,43 log as juju_log,
43 charm_dir,44 charm_dir,
44 DEBUG,
45 INFO,45 INFO,
46 ERROR,46 ERROR,
47 related_units,47 related_units,
@@ -82,9 +82,12 @@
82 restart_on_change_helper,82 restart_on_change_helper,
83)83)
84from charmhelpers.fetch import (84from charmhelpers.fetch import (
85 apt_install,
86 apt_cache,85 apt_cache,
87 install_remote,86 install_remote,
87 import_key as fetch_import_key,
88 add_source as fetch_add_source,
89 SourceConfigError,
90 GPGKeyError,
88 get_upstream_version91 get_upstream_version
89)92)
90from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk93from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
@@ -436,13 +439,14 @@
436 # error_out(e)439 # error_out(e)
437440
438441
439os_rel = None442# Module local cache variable for the os_release.
443_os_rel = None
440444
441445
442def reset_os_release():446def reset_os_release():
443 '''Unset the cached os_release version'''447 '''Unset the cached os_release version'''
444 global os_rel448 global _os_rel
445 os_rel = None449 _os_rel = None
446450
447451
448def os_release(package, base='essex', reset_cache=False):452def os_release(package, base='essex', reset_cache=False):
@@ -456,144 +460,77 @@
456 the installation source, the earliest release supported by the charm should460 the installation source, the earliest release supported by the charm should
457 be returned.461 be returned.
458 '''462 '''
459 global os_rel463 global _os_rel
460 if reset_cache:464 if reset_cache:
461 reset_os_release()465 reset_os_release()
462 if os_rel:466 if _os_rel:
463 return os_rel467 return _os_rel
464 os_rel = (git_os_codename_install_source(config('openstack-origin-git')) or468 _os_rel = (
465 get_os_codename_package(package, fatal=False) or469 git_os_codename_install_source(config('openstack-origin-git')) or
466 get_os_codename_install_source(config('openstack-origin')) or470 get_os_codename_package(package, fatal=False) or
467 base)471 get_os_codename_install_source(config('openstack-origin')) or
468 return os_rel472 base)
469473 return _os_rel
470474
475
476@deprecate("moved to charmhelpers.fetch.import_key()", "2017-07", log=juju_log)
471def import_key(keyid):477def import_key(keyid):
472 key = keyid.strip()478 """Import a key, either ASCII armored, or a GPG key id.
473 if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and479
474 key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):480 @param keyid: the key in ASCII armor format, or a GPG key id.
475 juju_log("PGP key found (looks like ASCII Armor format)", level=DEBUG)481 @raises SystemExit() via sys.exit() on failure.
476 juju_log("Importing ASCII Armor PGP key", level=DEBUG)482 """
477 with tempfile.NamedTemporaryFile() as keyfile:483 try:
478 with open(keyfile.name, 'w') as fd:484 return fetch_import_key(keyid)
479 fd.write(key)485 except GPGKeyError as e:
480 fd.write("\n")486 error_out("Could not import key: {}".format(str(e)))
481487
482 cmd = ['apt-key', 'add', keyfile.name]488
483 try:489def get_source_and_pgp_key(source_and_key):
484 subprocess.check_call(cmd)490 """Look for a pgp key ID or ascii-armor key in the given input.
485 except subprocess.CalledProcessError:491
486 error_out("Error importing PGP key '%s'" % key)492 :param source_and_key: Sting, "source_spec|keyid" where '|keyid' is
487 else:493 optional.
488 juju_log("PGP key found (looks like Radix64 format)", level=DEBUG)494 :returns (source_spec, key_id OR None) as a tuple. Returns None for key_id
489 juju_log("Importing PGP key from keyserver", level=DEBUG)495 if there was no '|' in the source_and_key string.
490 cmd = ['apt-key', 'adv', '--keyserver',496 """
491 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]497 try:
492 try:498 source, key = source_and_key.split('|', 2)
493 subprocess.check_call(cmd)499 return source, key or None
494 except subprocess.CalledProcessError:500 except ValueError:
495 error_out("Error importing PGP key '%s'" % key)501 return source_and_key, None
496502
497503
498def get_source_and_pgp_key(input):504@deprecate("use charmhelpers.fetch.add_source() instead.",
499 """Look for a pgp key ID or ascii-armor key in the given input."""505 "2017-07", log=juju_log)
500 index = input.strip()506def configure_installation_source(source_plus_key):
501 index = input.rfind('|')507 """Configure an installation source.
502 if index < 0:508
503 return input, None509 The functionality is provided by charmhelpers.fetch.add_source()
504510 The difference between the two functions is that add_source() signature
505 key = input[index + 1:].strip('|')511 requires the key to be passed directly, whereas this function passes an
506 source = input[:index]512 optional key by appending '|<key>' to the end of the source specificiation
507 return source, key513 'source'.
508514
509515 Another difference from add_source() is that the function calls sys.exit(1)
510def configure_installation_source(rel):516 if the configuration fails, whereas add_source() raises
511 '''Configure apt installation source.'''517 SourceConfigurationError(). Another difference, is that add_source()
512 if rel == 'distro':518 silently fails (with a juju_log command) if there is no matching source to
513 return519 configure, whereas this function fails with a sys.exit(1)
514 elif rel == 'distro-proposed':520
515 ubuntu_rel = lsb_release()['DISTRIB_CODENAME']521 :param source: String_plus_key -- see above for details.
516 with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:522
517 f.write(DISTRO_PROPOSED % ubuntu_rel)523 Note that the behaviour on error is to log the error to the juju log and
518 elif rel[:4] == "ppa:":524 then call sys.exit(1).
519 src, key = get_source_and_pgp_key(rel)525 """
520 if key:526 # extract the key if there is one, denoted by a '|' in the rel
521 import_key(key)527 source, key = get_source_and_pgp_key(source_plus_key)
522528
523 subprocess.check_call(["add-apt-repository", "-y", src])529 # handle the ordinary sources via add_source
524 elif rel[:3] == "deb":530 try:
525 src, key = get_source_and_pgp_key(rel)531 fetch_add_source(source, key, fail_invalid=True)
526 if key:532 except SourceConfigError as se:
527 import_key(key)533 error_out(str(se))
528
529 with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
530 f.write(src)
531 elif rel[:6] == 'cloud:':
532 ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
533 rel = rel.split(':')[1]
534 u_rel = rel.split('-')[0]
535 ca_rel = rel.split('-')[1]
536
537 if u_rel != ubuntu_rel:
538 e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
539 'version (%s)' % (ca_rel, ubuntu_rel)
540 error_out(e)
541
542 if 'staging' in ca_rel:
543 # staging is just a regular PPA.
544 os_rel = ca_rel.split('/')[0]
545 ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
546 cmd = 'add-apt-repository -y %s' % ppa
547 subprocess.check_call(cmd.split(' '))
548 return
549
550 # map charm config options to actual archive pockets.
551 pockets = {
552 'folsom': 'precise-updates/folsom',
553 'folsom/updates': 'precise-updates/folsom',
554 'folsom/proposed': 'precise-proposed/folsom',
555 'grizzly': 'precise-updates/grizzly',
556 'grizzly/updates': 'precise-updates/grizzly',
557 'grizzly/proposed': 'precise-proposed/grizzly',
558 'havana': 'precise-updates/havana',
559 'havana/updates': 'precise-updates/havana',
560 'havana/proposed': 'precise-proposed/havana',
561 'icehouse': 'precise-updates/icehouse',
562 'icehouse/updates': 'precise-updates/icehouse',
563 'icehouse/proposed': 'precise-proposed/icehouse',
564 'juno': 'trusty-updates/juno',
565 'juno/updates': 'trusty-updates/juno',
566 'juno/proposed': 'trusty-proposed/juno',
567 'kilo': 'trusty-updates/kilo',
568 'kilo/updates': 'trusty-updates/kilo',
569 'kilo/proposed': 'trusty-proposed/kilo',
570 'liberty': 'trusty-updates/liberty',
571 'liberty/updates': 'trusty-updates/liberty',
572 'liberty/proposed': 'trusty-proposed/liberty',
573 'mitaka': 'trusty-updates/mitaka',
574 'mitaka/updates': 'trusty-updates/mitaka',
575 'mitaka/proposed': 'trusty-proposed/mitaka',
576 'newton': 'xenial-updates/newton',
577 'newton/updates': 'xenial-updates/newton',
578 'newton/proposed': 'xenial-proposed/newton',
579 'ocata': 'xenial-updates/ocata',
580 'ocata/updates': 'xenial-updates/ocata',
581 'ocata/proposed': 'xenial-proposed/ocata',
582 }
583
584 try:
585 pocket = pockets[ca_rel]
586 except KeyError:
587 e = 'Invalid Cloud Archive release specified: %s' % rel
588 error_out(e)
589
590 src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
591 apt_install('ubuntu-cloud-keyring', fatal=True)
592
593 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
594 f.write(src)
595 else:
596 error_out("Invalid openstack-release specified: %s" % rel)
597534
598535
599def config_value_changed(option):536def config_value_changed(option):
@@ -638,7 +575,6 @@
638575
639 :returns: bool: : Returns True if configured installation source offers576 :returns: bool: : Returns True if configured installation source offers
640 a newer version of package.577 a newer version of package.
641
642 """578 """
643579
644 import apt_pkg as apt580 import apt_pkg as apt
645581
=== modified file 'charmhelpers/fetch/__init__.py'
--- charmhelpers/fetch/__init__.py 2016-09-20 17:07:51 +0000
+++ charmhelpers/fetch/__init__.py 2017-04-10 18:12:16 +0000
@@ -48,6 +48,13 @@
48 pass48 pass
4949
5050
51class GPGKeyError(Exception):
52 """Exception occurs when a GPG key cannot be fetched or used. The message
53 indicates what the problem is.
54 """
55 pass
56
57
51class BaseFetchHandler(object):58class BaseFetchHandler(object):
5259
53 """Base class for FetchHandler implementations in fetch plugins"""60 """Base class for FetchHandler implementations in fetch plugins"""
@@ -77,21 +84,22 @@
77fetch = importlib.import_module(module)84fetch = importlib.import_module(module)
7885
79filter_installed_packages = fetch.filter_installed_packages86filter_installed_packages = fetch.filter_installed_packages
80install = fetch.install87install = fetch.apt_install
81upgrade = fetch.upgrade88upgrade = fetch.apt_upgrade
82update = fetch.update89update = _fetch_update = fetch.apt_update
83purge = fetch.purge90purge = fetch.apt_purge
84add_source = fetch.add_source91add_source = fetch.add_source
8592
86if __platform__ == "ubuntu":93if __platform__ == "ubuntu":
87 apt_cache = fetch.apt_cache94 apt_cache = fetch.apt_cache
88 apt_install = fetch.install95 apt_install = fetch.apt_install
89 apt_update = fetch.update96 apt_update = fetch.apt_update
90 apt_upgrade = fetch.upgrade97 apt_upgrade = fetch.apt_upgrade
91 apt_purge = fetch.purge98 apt_purge = fetch.apt_purge
92 apt_mark = fetch.apt_mark99 apt_mark = fetch.apt_mark
93 apt_hold = fetch.apt_hold100 apt_hold = fetch.apt_hold
94 apt_unhold = fetch.apt_unhold101 apt_unhold = fetch.apt_unhold
102 import_key = fetch.import_key
95 get_upstream_version = fetch.get_upstream_version103 get_upstream_version = fetch.get_upstream_version
96elif __platform__ == "centos":104elif __platform__ == "centos":
97 yum_search = fetch.yum_search105 yum_search = fetch.yum_search
@@ -135,7 +143,7 @@
135 for source, key in zip(sources, keys):143 for source, key in zip(sources, keys):
136 add_source(source, key)144 add_source(source, key)
137 if update:145 if update:
138 fetch.update(fatal=True)146 _fetch_update(fatal=True)
139147
140148
141def install_remote(source, *args, **kwargs):149def install_remote(source, *args, **kwargs):
142150
=== modified file 'charmhelpers/fetch/centos.py'
--- charmhelpers/fetch/centos.py 2016-08-11 15:47:44 +0000
+++ charmhelpers/fetch/centos.py 2017-04-10 18:12:16 +0000
@@ -132,7 +132,7 @@
132 key_file.write(key)132 key_file.write(key)
133 key_file.flush()133 key_file.flush()
134 key_file.seek(0)134 key_file.seek(0)
135 subprocess.check_call(['rpm', '--import', key_file])135 subprocess.check_call(['rpm', '--import', key_file.name])
136 else:136 else:
137 subprocess.check_call(['rpm', '--import', key])137 subprocess.check_call(['rpm', '--import', key])
138138
139139
=== modified file 'charmhelpers/fetch/ubuntu.py'
--- charmhelpers/fetch/ubuntu.py 2017-03-03 20:37:37 +0000
+++ charmhelpers/fetch/ubuntu.py 2017-04-10 18:12:16 +0000
@@ -12,29 +12,46 @@
12# See the License for the specific language governing permissions and12# See the License for the specific language governing permissions and
13# limitations under the License.13# limitations under the License.
1414
15from collections import OrderedDict
15import os16import os
17import platform
18import re
16import six19import six
17import time20import time
18import subprocess21import subprocess
19
20from tempfile import NamedTemporaryFile22from tempfile import NamedTemporaryFile
23
21from charmhelpers.core.host import (24from charmhelpers.core.host import (
22 lsb_release25 lsb_release
23)26)
24from charmhelpers.core.hookenv import log27from charmhelpers.core.hookenv import (
25from charmhelpers.fetch import SourceConfigError28 log,
29 DEBUG,
30)
31from charmhelpers.fetch import SourceConfigError, GPGKeyError
2632
33PROPOSED_POCKET = (
34 "# Proposed\n"
35 "deb http://archive.ubuntu.com/ubuntu {}-proposed main universe "
36 "multiverse restricted\n")
37PROPOSED_PORTS_POCKET = (
38 "# Proposed\n"
39 "deb http://ports.ubuntu.com/ubuntu-ports {}-proposed main universe "
40 "multiverse restricted\n")
41# Only supports 64bit and ppc64 at the moment.
42ARCH_TO_PROPOSED_POCKET = {
43 'x86_64': PROPOSED_POCKET,
44 'ppc64le': PROPOSED_PORTS_POCKET,
45}
46CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
47CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
27CLOUD_ARCHIVE = """# Ubuntu Cloud Archive48CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
28deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main49deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
29"""50"""
30
31PROPOSED_POCKET = """# Proposed
32deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
33"""
34
35CLOUD_ARCHIVE_POCKETS = {51CLOUD_ARCHIVE_POCKETS = {
36 # Folsom52 # Folsom
37 'folsom': 'precise-updates/folsom',53 'folsom': 'precise-updates/folsom',
54 'folsom/updates': 'precise-updates/folsom',
38 'precise-folsom': 'precise-updates/folsom',55 'precise-folsom': 'precise-updates/folsom',
39 'precise-folsom/updates': 'precise-updates/folsom',56 'precise-folsom/updates': 'precise-updates/folsom',
40 'precise-updates/folsom': 'precise-updates/folsom',57 'precise-updates/folsom': 'precise-updates/folsom',
@@ -43,6 +60,7 @@
43 'precise-proposed/folsom': 'precise-proposed/folsom',60 'precise-proposed/folsom': 'precise-proposed/folsom',
44 # Grizzly61 # Grizzly
45 'grizzly': 'precise-updates/grizzly',62 'grizzly': 'precise-updates/grizzly',
63 'grizzly/updates': 'precise-updates/grizzly',
46 'precise-grizzly': 'precise-updates/grizzly',64 'precise-grizzly': 'precise-updates/grizzly',
47 'precise-grizzly/updates': 'precise-updates/grizzly',65 'precise-grizzly/updates': 'precise-updates/grizzly',
48 'precise-updates/grizzly': 'precise-updates/grizzly',66 'precise-updates/grizzly': 'precise-updates/grizzly',
@@ -51,6 +69,7 @@
51 'precise-proposed/grizzly': 'precise-proposed/grizzly',69 'precise-proposed/grizzly': 'precise-proposed/grizzly',
52 # Havana70 # Havana
53 'havana': 'precise-updates/havana',71 'havana': 'precise-updates/havana',
72 'havana/updates': 'precise-updates/havana',
54 'precise-havana': 'precise-updates/havana',73 'precise-havana': 'precise-updates/havana',
55 'precise-havana/updates': 'precise-updates/havana',74 'precise-havana/updates': 'precise-updates/havana',
56 'precise-updates/havana': 'precise-updates/havana',75 'precise-updates/havana': 'precise-updates/havana',
@@ -59,6 +78,7 @@
59 'precise-proposed/havana': 'precise-proposed/havana',78 'precise-proposed/havana': 'precise-proposed/havana',
60 # Icehouse79 # Icehouse
61 'icehouse': 'precise-updates/icehouse',80 'icehouse': 'precise-updates/icehouse',
81 'icehouse/updates': 'precise-updates/icehouse',
62 'precise-icehouse': 'precise-updates/icehouse',82 'precise-icehouse': 'precise-updates/icehouse',
63 'precise-icehouse/updates': 'precise-updates/icehouse',83 'precise-icehouse/updates': 'precise-updates/icehouse',
64 'precise-updates/icehouse': 'precise-updates/icehouse',84 'precise-updates/icehouse': 'precise-updates/icehouse',
@@ -67,6 +87,7 @@
67 'precise-proposed/icehouse': 'precise-proposed/icehouse',87 'precise-proposed/icehouse': 'precise-proposed/icehouse',
68 # Juno88 # Juno
69 'juno': 'trusty-updates/juno',89 'juno': 'trusty-updates/juno',
90 'juno/updates': 'trusty-updates/juno',
70 'trusty-juno': 'trusty-updates/juno',91 'trusty-juno': 'trusty-updates/juno',
71 'trusty-juno/updates': 'trusty-updates/juno',92 'trusty-juno/updates': 'trusty-updates/juno',
72 'trusty-updates/juno': 'trusty-updates/juno',93 'trusty-updates/juno': 'trusty-updates/juno',
@@ -75,6 +96,7 @@
75 'trusty-proposed/juno': 'trusty-proposed/juno',96 'trusty-proposed/juno': 'trusty-proposed/juno',
76 # Kilo97 # Kilo
77 'kilo': 'trusty-updates/kilo',98 'kilo': 'trusty-updates/kilo',
99 'kilo/updates': 'trusty-updates/kilo',
78 'trusty-kilo': 'trusty-updates/kilo',100 'trusty-kilo': 'trusty-updates/kilo',
79 'trusty-kilo/updates': 'trusty-updates/kilo',101 'trusty-kilo/updates': 'trusty-updates/kilo',
80 'trusty-updates/kilo': 'trusty-updates/kilo',102 'trusty-updates/kilo': 'trusty-updates/kilo',
@@ -83,6 +105,7 @@
83 'trusty-proposed/kilo': 'trusty-proposed/kilo',105 'trusty-proposed/kilo': 'trusty-proposed/kilo',
84 # Liberty106 # Liberty
85 'liberty': 'trusty-updates/liberty',107 'liberty': 'trusty-updates/liberty',
108 'liberty/updates': 'trusty-updates/liberty',
86 'trusty-liberty': 'trusty-updates/liberty',109 'trusty-liberty': 'trusty-updates/liberty',
87 'trusty-liberty/updates': 'trusty-updates/liberty',110 'trusty-liberty/updates': 'trusty-updates/liberty',
88 'trusty-updates/liberty': 'trusty-updates/liberty',111 'trusty-updates/liberty': 'trusty-updates/liberty',
@@ -91,6 +114,7 @@
91 'trusty-proposed/liberty': 'trusty-proposed/liberty',114 'trusty-proposed/liberty': 'trusty-proposed/liberty',
92 # Mitaka115 # Mitaka
93 'mitaka': 'trusty-updates/mitaka',116 'mitaka': 'trusty-updates/mitaka',
117 'mitaka/updates': 'trusty-updates/mitaka',
94 'trusty-mitaka': 'trusty-updates/mitaka',118 'trusty-mitaka': 'trusty-updates/mitaka',
95 'trusty-mitaka/updates': 'trusty-updates/mitaka',119 'trusty-mitaka/updates': 'trusty-updates/mitaka',
96 'trusty-updates/mitaka': 'trusty-updates/mitaka',120 'trusty-updates/mitaka': 'trusty-updates/mitaka',
@@ -99,6 +123,7 @@
99 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',123 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
100 # Newton124 # Newton
101 'newton': 'xenial-updates/newton',125 'newton': 'xenial-updates/newton',
126 'newton/updates': 'xenial-updates/newton',
102 'xenial-newton': 'xenial-updates/newton',127 'xenial-newton': 'xenial-updates/newton',
103 'xenial-newton/updates': 'xenial-updates/newton',128 'xenial-newton/updates': 'xenial-updates/newton',
104 'xenial-updates/newton': 'xenial-updates/newton',129 'xenial-updates/newton': 'xenial-updates/newton',
@@ -107,6 +132,7 @@
107 'xenial-proposed/newton': 'xenial-proposed/newton',132 'xenial-proposed/newton': 'xenial-proposed/newton',
108 # Ocata133 # Ocata
109 'ocata': 'xenial-updates/ocata',134 'ocata': 'xenial-updates/ocata',
135 'ocata/updates': 'xenial-updates/ocata',
110 'xenial-ocata': 'xenial-updates/ocata',136 'xenial-ocata': 'xenial-updates/ocata',
111 'xenial-ocata/updates': 'xenial-updates/ocata',137 'xenial-ocata/updates': 'xenial-updates/ocata',
112 'xenial-updates/ocata': 'xenial-updates/ocata',138 'xenial-updates/ocata': 'xenial-updates/ocata',
@@ -115,6 +141,7 @@
115 'xenial-ocata/newton': 'xenial-proposed/ocata',141 'xenial-ocata/newton': 'xenial-proposed/ocata',
116}142}
117143
144
118APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.145APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
119CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.146CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
120CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times.147CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times.
@@ -145,7 +172,7 @@
145 return apt_pkg.Cache(progress)172 return apt_pkg.Cache(progress)
146173
147174
148def install(packages, options=None, fatal=False):175def apt_install(packages, options=None, fatal=False):
149 """Install one or more packages."""176 """Install one or more packages."""
150 if options is None:177 if options is None:
151 options = ['--option=Dpkg::Options::=--force-confold']178 options = ['--option=Dpkg::Options::=--force-confold']
@@ -162,7 +189,7 @@
162 _run_apt_command(cmd, fatal)189 _run_apt_command(cmd, fatal)
163190
164191
165def upgrade(options=None, fatal=False, dist=False):192def apt_upgrade(options=None, fatal=False, dist=False):
166 """Upgrade all packages."""193 """Upgrade all packages."""
167 if options is None:194 if options is None:
168 options = ['--option=Dpkg::Options::=--force-confold']195 options = ['--option=Dpkg::Options::=--force-confold']
@@ -177,13 +204,13 @@
177 _run_apt_command(cmd, fatal)204 _run_apt_command(cmd, fatal)
178205
179206
180def update(fatal=False):207def apt_update(fatal=False):
181 """Update local apt cache."""208 """Update local apt cache."""
182 cmd = ['apt-get', 'update']209 cmd = ['apt-get', 'update']
183 _run_apt_command(cmd, fatal)210 _run_apt_command(cmd, fatal)
184211
185212
186def purge(packages, fatal=False):213def apt_purge(packages, fatal=False):
187 """Purge one or more packages."""214 """Purge one or more packages."""
188 cmd = ['apt-get', '--assume-yes', 'purge']215 cmd = ['apt-get', '--assume-yes', 'purge']
189 if isinstance(packages, six.string_types):216 if isinstance(packages, six.string_types):
@@ -217,7 +244,45 @@
217 return apt_mark(packages, 'unhold', fatal=fatal)244 return apt_mark(packages, 'unhold', fatal=fatal)
218245
219246
220def add_source(source, key=None):247def import_key(keyid):
248 """Import a key in either ASCII Armor or Radix64 format.
249
250 `keyid` is either the keyid to fetch from a PGP server, or
251 the key in ASCII armor foramt.
252
253 :param keyid: String of key (or key id).
254 :raises: GPGKeyError if the key could not be imported
255 """
256 key = keyid.strip()
257 if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
258 key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
259 log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
260 log("Importing ASCII Armor PGP key", level=DEBUG)
261 with NamedTemporaryFile() as keyfile:
262 with open(keyfile.name, 'w') as fd:
263 fd.write(key)
264 fd.write("\n")
265 cmd = ['apt-key', 'add', keyfile.name]
266 try:
267 subprocess.check_call(cmd)
268 except subprocess.CalledProcessError:
269 error = "Error importing PGP key '{}'".format(key)
270 log(error)
271 raise GPGKeyError(error)
272 else:
273 log("PGP key found (looks like Radix64 format)", level=DEBUG)
274 log("Importing PGP key from keyserver", level=DEBUG)
275 cmd = ['apt-key', 'adv', '--keyserver',
276 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
277 try:
278 subprocess.check_call(cmd)
279 except subprocess.CalledProcessError:
280 error = "Error importing PGP key '{}'".format(key)
281 log(error)
282 raise GPGKeyError(error)
283
284
285def add_source(source, key=None, fail_invalid=False):
221 """Add a package source to this system.286 """Add a package source to this system.
222287
223 @param source: a URL or sources.list entry, as supported by288 @param source: a URL or sources.list entry, as supported by
@@ -233,6 +298,33 @@
233 such as 'cloud:icehouse'298 such as 'cloud:icehouse'
234 'distro' may be used as a noop299 'distro' may be used as a noop
235300
301 Full list of source specifications supported by the function are:
302
303 'distro': A NOP; i.e. it has no effect.
304 'proposed': the proposed deb spec [2] is wrtten to
305 /etc/apt/sources.list/proposed
306 'distro-proposed': adds <version>-proposed to the debs [2]
307 'ppa:<ppa-name>': add-apt-repository --yes <ppa_name>
308 'deb <deb-spec>': add-apt-repository --yes deb <deb-spec>
309 'http://....': add-apt-repository --yes http://...
310 'cloud-archive:<spec>': add-apt-repository -yes cloud-archive:<spec>
311 'cloud:<release>[-staging]': specify a Cloud Archive pocket <release> with
312 optional staging version. If staging is used then the staging PPA [2]
313 with be used. If staging is NOT used then the cloud archive [3] will be
314 added, and the 'ubuntu-cloud-keyring' package will be added for the
315 current distro.
316
317 Otherwise the source is not recognised and this is logged to the juju log.
318 However, no error is raised, unless sys_error_on_exit is True.
319
320 [1] deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
321 where {} is replaced with the derived pocket name.
322 [2] deb http://archive.ubuntu.com/ubuntu {}-proposed \
323 main universe multiverse restricted
324 where {} is replaced with the lsb_release codename (e.g. xenial)
325 [3] deb http://ubuntu-cloud.archive.canonical.com/ubuntu <pocket>
326 to /etc/apt/sources.list.d/cloud-archive-list
327
236 @param key: A key to be added to the system's APT keyring and used328 @param key: A key to be added to the system's APT keyring and used
237 to verify the signatures on packages. Ideally, this should be an329 to verify the signatures on packages. Ideally, this should be an
238 ASCII format GPG public key including the block headers. A GPG key330 ASCII format GPG public key including the block headers. A GPG key
@@ -240,51 +332,141 @@
240 available to retrieve the actual public key from a public keyserver332 available to retrieve the actual public key from a public keyserver
241 placing your Juju environment at risk. ppa and cloud archive keys333 placing your Juju environment at risk. ppa and cloud archive keys
242 are securely added automtically, so sould not be provided.334 are securely added automtically, so sould not be provided.
335
336 @param fail_invalid: (boolean) if True, then the function raises a
337 SourceConfigError is there is no matching installation source.
338
339 @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a
340 valid pocket in CLOUD_ARCHIVE_POCKETS
243 """341 """
342 _mapping = OrderedDict([
343 (r"^distro$", lambda: None), # This is a NOP
344 (r"^(?:proposed|distro-proposed)$", _add_proposed),
345 (r"^cloud-archive:(.*)$", _add_apt_repository),
346 (r"^((?:deb |http:|https:|ppa:).*)$", _add_apt_repository),
347 (r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging),
348 (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
349 (r"^cloud:(.*)$", _add_cloud_pocket),
350 ])
244 if source is None:351 if source is None:
245 log('Source is not present. Skipping')352 source = ''
246 return353 for r, fn in six.iteritems(_mapping):
247354 m = re.match(r, source)
248 if (source.startswith('ppa:') or355 if m:
249 source.startswith('http') or356 # call the assoicated function with the captured groups
250 source.startswith('deb ') or357 # raises SourceConfigError on error.
251 source.startswith('cloud-archive:')):358 fn(*m.groups())
252 cmd = ['add-apt-repository', '--yes', source]359 if key:
253 _run_with_retries(cmd)360 try:
254 elif source.startswith('cloud:'):361 import_key(key)
255 install(filter_installed_packages(['ubuntu-cloud-keyring']),362 except GPGKeyError as e:
363 raise SourceConfigError(str(e))
364 break
365 else:
366 # nothing matched. log an error and maybe sys.exit
367 err = "Unknown source: {!r}".format(source)
368 log(err)
369 if fail_invalid:
370 raise SourceConfigError(err)
371
372
373def _add_proposed():
374 """Add the PROPOSED_POCKET as /etc/apt/source.list.d/proposed.list
375
376 Uses lsb_release()['DISTRIB_CODENAME'] to determine the correct staza for
377 the deb line.
378
379 For intel architecutres PROPOSED_POCKET is used for the release, but for
380 other architectures PROPOSED_PORTS_POCKET is used for the release.
381 """
382 release = lsb_release()['DISTRIB_CODENAME']
383 arch = platform.machine()
384 if arch not in six.iterkeys(ARCH_TO_PROPOSED_POCKET):
385 raise SourceConfigError("Arch {} not supported for (distro-)proposed"
386 .format(arch))
387 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
388 apt.write(ARCH_TO_PROPOSED_POCKET[arch].format(release))
389
390
391def _add_apt_repository(spec):
392 """Add the spec using add_apt_repository
393
394 :param spec: the parameter to pass to add_apt_repository
395 """
396 _run_with_retries(['add-apt-repository', '--yes', spec])
397
398
399def _add_cloud_pocket(pocket):
400 """Add a cloud pocket as /etc/apt/sources.d/cloud-archive.list
401
402 Note that this overwrites the existing file if there is one.
403
404 This function also converts the simple pocket in to the actual pocket using
405 the CLOUD_ARCHIVE_POCKETS mapping.
406
407 :param pocket: string representing the pocket to add a deb spec for.
408 :raises: SourceConfigError if the cloud pocket doesn't exist or the
409 requested release doesn't match the current distro version.
410 """
411 apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
256 fatal=True)412 fatal=True)
257 pocket = source.split(':')[-1]413 if pocket not in CLOUD_ARCHIVE_POCKETS:
258 if pocket not in CLOUD_ARCHIVE_POCKETS:414 raise SourceConfigError(
259 raise SourceConfigError(415 'Unsupported cloud: source option %s' %
260 'Unsupported cloud: source option %s' %416 pocket)
261 pocket)417 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
262 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]418 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
263 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:419 apt.write(CLOUD_ARCHIVE.format(actual_pocket))
264 apt.write(CLOUD_ARCHIVE.format(actual_pocket))420
265 elif source == 'proposed':421
266 release = lsb_release()['DISTRIB_CODENAME']422def _add_cloud_staging(cloud_archive_release, openstack_release):
267 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:423 """Add the cloud staging repository which is in
268 apt.write(PROPOSED_POCKET.format(release))424 ppa:ubuntu-cloud-archive/<openstack_release>-staging
269 elif source == 'distro':425
270 pass426 This function checks that the cloud_archive_release matches the current
271 else:427 codename for the distro that charm is being installed on.
272 log("Unknown source: {!r}".format(source))428
273429 :param cloud_archive_release: string, codename for the release.
274 if key:430 :param openstack_release: String, codename for the openstack release.
275 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:431 :raises: SourceConfigError if the cloud_archive_release doesn't match the
276 with NamedTemporaryFile('w+') as key_file:432 current version of the os.
277 key_file.write(key)433 """
278 key_file.flush()434 _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
279 key_file.seek(0)435 ppa = 'ppa:ubuntu-cloud-archive/{}-staging'.format(openstack_release)
280 subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)436 cmd = 'add-apt-repository -y {}'.format(ppa)
281 else:437 _run_with_retries(cmd.split(' '))
282 # Note that hkp: is in no way a secure protocol. Using a438
283 # GPG key id is pointless from a security POV unless you439
284 # absolutely trust your network and DNS.440def _add_cloud_distro_check(cloud_archive_release, openstack_release):
285 subprocess.check_call(['apt-key', 'adv', '--keyserver',441 """Add the cloud pocket, but also check the cloud_archive_release against
286 'hkp://keyserver.ubuntu.com:80', '--recv',442 the current distro, and use the openstack_release as the full lookup.
287 key])443
444 This just calls _add_cloud_pocket() with the openstack_release as pocket
445 to get the correct cloud-archive.list for dpkg to work with.
446
447 :param cloud_archive_release:String, codename for the distro release.
448 :param openstack_release: String, spec for the release to look up in the
449 CLOUD_ARCHIVE_POCKETS
450 :raises: SourceConfigError if this is the wrong distro, or the pocket spec
451 doesn't exist.
452 """
453 _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
454 _add_cloud_pocket("{}-{}".format(cloud_archive_release, openstack_release))
455
456
457def _verify_is_ubuntu_rel(release, os_release):
458 """Verify that the release is in the same as the current ubuntu release.
459
460 :param release: String, lowercase for the release.
461 :param os_release: String, the os_release being asked for
462 :raises: SourceConfigError if the release is not the same as the ubuntu
463 release.
464 """
465 ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
466 if release != ubuntu_rel:
467 raise SourceConfigError(
468 'Invalid Cloud Archive release specified: {}-{} on this Ubuntu'
469 'version ({})'.format(release, os_release, ubuntu_rel))
288470
289471
290def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),472def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
@@ -300,9 +482,12 @@
300 :param: cmd_env: dict: Environment variables to add to the command run.482 :param: cmd_env: dict: Environment variables to add to the command run.
301 """483 """
302484
303 env = os.environ.copy()485 env = None
486 kwargs = {}
304 if cmd_env:487 if cmd_env:
488 env = os.environ.copy()
305 env.update(cmd_env)489 env.update(cmd_env)
490 kwargs['env'] = env
306491
307 if not retry_message:492 if not retry_message:
308 retry_message = "Failed executing '{}'".format(" ".join(cmd))493 retry_message = "Failed executing '{}'".format(" ".join(cmd))
@@ -314,7 +499,8 @@
314 retry_results = (None,) + retry_exitcodes499 retry_results = (None,) + retry_exitcodes
315 while result in retry_results:500 while result in retry_results:
316 try:501 try:
317 result = subprocess.check_call(cmd, env=env)502 # result = subprocess.check_call(cmd, env=env)
503 result = subprocess.check_call(cmd, **kwargs)
318 except subprocess.CalledProcessError as e:504 except subprocess.CalledProcessError as e:
319 retry_count = retry_count + 1505 retry_count = retry_count + 1
320 if retry_count > max_retries:506 if retry_count > max_retries:
@@ -327,6 +513,7 @@
327def _run_apt_command(cmd, fatal=False):513def _run_apt_command(cmd, fatal=False):
328 """Run an apt command with optional retries.514 """Run an apt command with optional retries.
329515
516 :param: cmd: str: The apt command to run.
330 :param: fatal: bool: Whether the command's output should be checked and517 :param: fatal: bool: Whether the command's output should be checked and
331 retried.518 retried.
332 """519 """
333520
=== modified file 'tests/contrib/openstack/test_openstack_utils.py'
--- tests/contrib/openstack/test_openstack_utils.py 2016-12-14 18:51:15 +0000
+++ tests/contrib/openstack/test_openstack_utils.py 2017-04-10 18:12:16 +0000
@@ -1,13 +1,13 @@
1import io1import io
2import os2import os
3import subprocess
4import tempfile
5import contextlib3import contextlib
6import unittest4import unittest
7from copy import copy5from copy import copy
6from tests.helpers import patch_open
8from testtools import TestCase7from testtools import TestCase
9from mock import MagicMock, patch, call8from mock import MagicMock, patch, call
109
10from charmhelpers.fetch import ubuntu as fetch
11import charmhelpers.contrib.openstack.utils as openstack11import charmhelpers.contrib.openstack.utils as openstack
1212
13import six13import six
@@ -123,23 +123,9 @@
123123
124# Mock python-dnspython resolver used by get_host_ip()124# Mock python-dnspython resolver used by get_host_ip()
125125
126PGP_KEY_ASCII_ARMOR = """-----BEGIN PGP PUBLIC KEY BLOCK-----
127Version: SKS 1.1.5
128Comment: Hostname: keyserver.ubuntu.com
129
130mI0EUCEyTAEEAMuUxyfiegCCwn4J/c0nw5PUTSJdn5FqiUTq6iMfij65xf1vl0g/Mxqw0gfg
131AJIsCDvO9N9dloLAwF6FUBMg5My7WyhRPTAKF505TKJboyX3Pp4J1fU1LV8QFVOp87vUh1Rz
132B6GU7cSglhnbL85gmbJTllkzkb3h4Yw7W+edjcQ/ABEBAAG0K0xhdW5jaHBhZCBQUEEgZm9y
133IFVidW50dSBDbG91ZCBBcmNoaXZlIFRlYW2IuAQTAQIAIgUCUCEyTAIbAwYLCQgHAwIGFQgC
134CQoLBBYCAwECHgECF4AACgkQimhEop9oEE7kJAP/eTBgq3Mhbvo0d8elMOuqZx3nmU7gSyPh
135ep0zYIRZ5TJWl/7PRtvp0CJA6N6ZywYTQ/4ANHhpibcHZkh8K0AzUvsGXnJRSFoJeqyDbD91
136EhoO+4ZfHs2HvRBQEDZILMa2OyuB497E5Mmyua3HDEOrG2cVLllsUZzpTFCx8NgeMHk=
137=jLBm
138-----END PGP PUBLIC KEY BLOCK-----
139"""
140
141126
142class FakeAnswer(object):127class FakeAnswer(object):
128
143 def __init__(self, ip):129 def __init__(self, ip):
144 self.ip = ip130 self.ip = ip
145131
@@ -148,6 +134,7 @@
148134
149135
150class FakeResolver(object):136class FakeResolver(object):
137
151 def __init__(self, ip):138 def __init__(self, ip):
152 self.ip = ip139 self.ip = ip
153140
@@ -159,16 +146,19 @@
159146
160147
161class FakeReverse(object):148class FakeReverse(object):
149
162 def from_address(self, address):150 def from_address(self, address):
163 return '156.94.189.91.in-addr.arpa'151 return '156.94.189.91.in-addr.arpa'
164152
165153
166class FakeDNSName(object):154class FakeDNSName(object):
155
167 def __init__(self, dnsname):156 def __init__(self, dnsname):
168 pass157 pass
169158
170159
171class FakeDNS(object):160class FakeDNS(object):
161
172 def __init__(self, ip):162 def __init__(self, ip):
173 self.resolver = FakeResolver(ip)163 self.resolver = FakeResolver(ip)
174 self.reversename = FakeReverse()164 self.reversename = FakeReverse()
@@ -177,6 +167,7 @@
177167
178168
179class OpenStackHelpersTestCase(TestCase):169class OpenStackHelpersTestCase(TestCase):
170
180 def _apt_cache(self):171 def _apt_cache(self):
181 # mocks out the apt cache172 # mocks out the apt cache
182 def cache_get(package):173 def cache_get(package):
@@ -197,7 +188,7 @@
197188
198 @patch('charmhelpers.contrib.openstack.utils.lsb_release')189 @patch('charmhelpers.contrib.openstack.utils.lsb_release')
199 def test_os_codename_from_install_source(self, mocked_lsb):190 def test_os_codename_from_install_source(self, mocked_lsb):
200 '''Test mapping install source to OpenStack release name'''191 """Test mapping install source to OpenStack release name"""
201 mocked_lsb.return_value = FAKE_RELEASE192 mocked_lsb.return_value = FAKE_RELEASE
202193
203 # the openstack release shipped with respective ubuntu/lsb release.194 # the openstack release shipped with respective ubuntu/lsb release.
@@ -236,7 +227,7 @@
236227
237 @patch('charmhelpers.contrib.openstack.utils.lsb_release')228 @patch('charmhelpers.contrib.openstack.utils.lsb_release')
238 def test_os_codename_from_bad_install_source(self, mocked_lsb):229 def test_os_codename_from_bad_install_source(self, mocked_lsb):
239 '''Test mapping install source to OpenStack release name'''230 """Test mapping install source to OpenStack release name"""
240 _fake_release = copy(FAKE_RELEASE)231 _fake_release = copy(FAKE_RELEASE)
241 _fake_release['DISTRIB_CODENAME'] = 'natty'232 _fake_release['DISTRIB_CODENAME'] = 'natty'
242233
@@ -249,32 +240,32 @@
249 mocked_err.assert_called_with(_er)240 mocked_err.assert_called_with(_er)
250241
251 def test_os_codename_from_version(self):242 def test_os_codename_from_version(self):
252 '''Test mapping OpenStack numerical versions to code name'''243 """Test mapping OpenStack numerical versions to code name"""
253 self.assertEquals(openstack.get_os_codename_version('2013.1'),244 self.assertEquals(openstack.get_os_codename_version('2013.1'),
254 'grizzly')245 'grizzly')
255246
256 @patch('charmhelpers.contrib.openstack.utils.error_out')247 @patch('charmhelpers.contrib.openstack.utils.error_out')
257 def test_os_codename_from_bad_version(self, mocked_error):248 def test_os_codename_from_bad_version(self, mocked_error):
258 '''Test mapping a bad OpenStack numerical versions to code name'''249 """Test mapping a bad OpenStack numerical versions to code name"""
259 openstack.get_os_codename_version('2014.5.5')250 openstack.get_os_codename_version('2014.5.5')
260 expected_err = ('Could not determine OpenStack codename for '251 expected_err = ('Could not determine OpenStack codename for '
261 'version 2014.5.5')252 'version 2014.5.5')
262 mocked_error.assert_called_with(expected_err)253 mocked_error.assert_called_with(expected_err)
263254
264 def test_os_version_from_codename(self):255 def test_os_version_from_codename(self):
265 '''Test mapping a OpenStack codename to numerical version'''256 """Test mapping a OpenStack codename to numerical version"""
266 self.assertEquals(openstack.get_os_version_codename('folsom'),257 self.assertEquals(openstack.get_os_version_codename('folsom'),
267 '2012.2')258 '2012.2')
268259
269 @patch('charmhelpers.contrib.openstack.utils.error_out')260 @patch('charmhelpers.contrib.openstack.utils.error_out')
270 def test_os_version_from_bad_codename(self, mocked_error):261 def test_os_version_from_bad_codename(self, mocked_error):
271 '''Test mapping a bad OpenStack codename to numerical version'''262 """Test mapping a bad OpenStack codename to numerical version"""
272 openstack.get_os_version_codename('foo')263 openstack.get_os_version_codename('foo')
273 expected_err = 'Could not derive OpenStack version for codename: foo'264 expected_err = 'Could not derive OpenStack version for codename: foo'
274 mocked_error.assert_called_with(expected_err)265 mocked_error.assert_called_with(expected_err)
275266
276 def test_os_version_swift_from_codename(self):267 def test_os_version_swift_from_codename(self):
277 '''Test mapping a swift codename to numerical version'''268 """Test mapping a swift codename to numerical version"""
278 self.assertEquals(openstack.get_os_version_codename_swift('liberty'),269 self.assertEquals(openstack.get_os_version_codename_swift('liberty'),
279 '2.5.0')270 '2.5.0')
280271
@@ -283,7 +274,7 @@
283274
284 @patch('charmhelpers.contrib.openstack.utils.error_out')275 @patch('charmhelpers.contrib.openstack.utils.error_out')
285 def test_os_version_swift_from_bad_codename(self, mocked_error):276 def test_os_version_swift_from_bad_codename(self, mocked_error):
286 '''Test mapping a bad swift codename to numerical version'''277 """Test mapping a bad swift codename to numerical version"""
287 openstack.get_os_version_codename_swift('foo')278 openstack.get_os_version_codename_swift('foo')
288 expected_err = 'Could not derive swift version for codename: foo'279 expected_err = 'Could not derive swift version for codename: foo'
289 mocked_error.assert_called_with(expected_err)280 mocked_error.assert_called_with(expected_err)
@@ -302,7 +293,7 @@
302 self.assertEquals(openstack.get_swift_codename('1.2.3'), None)293 self.assertEquals(openstack.get_swift_codename('1.2.3'), None)
303294
304 def test_os_codename_from_package(self):295 def test_os_codename_from_package(self):
305 '''Test deriving OpenStack codename from an installed package'''296 """Test deriving OpenStack codename from an installed package"""
306 with patch('apt_pkg.Cache') as cache:297 with patch('apt_pkg.Cache') as cache:
307 cache.return_value = self._apt_cache()298 cache.return_value = self._apt_cache()
308 for pkg, vers in six.iteritems(FAKE_REPO):299 for pkg, vers in six.iteritems(FAKE_REPO):
@@ -316,7 +307,7 @@
316307
317 @patch('charmhelpers.contrib.openstack.utils.error_out')308 @patch('charmhelpers.contrib.openstack.utils.error_out')
318 def test_os_codename_from_bad_package_version(self, mocked_error):309 def test_os_codename_from_bad_package_version(self, mocked_error):
319 '''Test deriving OpenStack codename for a poorly versioned package'''310 """Test deriving OpenStack codename for a poorly versioned package"""
320 with patch('apt_pkg.Cache') as cache:311 with patch('apt_pkg.Cache') as cache:
321 cache.return_value = self._apt_cache()312 cache.return_value = self._apt_cache()
322 openstack.get_os_codename_package('bad-version')313 openstack.get_os_codename_package('bad-version')
@@ -325,7 +316,7 @@
325316
326 @patch('charmhelpers.contrib.openstack.utils.error_out')317 @patch('charmhelpers.contrib.openstack.utils.error_out')
327 def test_os_codename_from_bad_package(self, mocked_error):318 def test_os_codename_from_bad_package(self, mocked_error):
328 '''Test deriving OpenStack codename from an unavailable package'''319 """Test deriving OpenStack codename from an unavailable package"""
329 with patch('apt_pkg.Cache') as cache:320 with patch('apt_pkg.Cache') as cache:
330 cache.return_value = self._apt_cache()321 cache.return_value = self._apt_cache()
331 try:322 try:
@@ -339,7 +330,7 @@
339 mocked_error.assert_called_with(e)330 mocked_error.assert_called_with(e)
340331
341 def test_os_codename_from_bad_package_nonfatal(self):332 def test_os_codename_from_bad_package_nonfatal(self):
342 '''Test OpenStack codename from an unavailable package is non-fatal'''333 """Test OpenStack codename from an unavailable package is non-fatal"""
343 with patch('apt_pkg.Cache') as cache:334 with patch('apt_pkg.Cache') as cache:
344 cache.return_value = self._apt_cache()335 cache.return_value = self._apt_cache()
345 self.assertEquals(336 self.assertEquals(
@@ -349,7 +340,7 @@
349340
350 @patch('charmhelpers.contrib.openstack.utils.error_out')341 @patch('charmhelpers.contrib.openstack.utils.error_out')
351 def test_os_codename_from_uninstalled_package(self, mock_error):342 def test_os_codename_from_uninstalled_package(self, mock_error):
352 '''Test OpenStack codename from an available but uninstalled pkg'''343 """Test OpenStack codename from an available but uninstalled pkg"""
353 with patch('apt_pkg.Cache') as cache:344 with patch('apt_pkg.Cache') as cache:
354 cache.return_value = self._apt_cache()345 cache.return_value = self._apt_cache()
355 try:346 try:
@@ -361,7 +352,7 @@
361 mock_error.assert_called_with(e)352 mock_error.assert_called_with(e)
362353
363 def test_os_codename_from_uninstalled_package_nonfatal(self):354 def test_os_codename_from_uninstalled_package_nonfatal(self):
364 '''Test OpenStack codename from avail uninstalled pkg is non fatal'''355 """Test OpenStack codename from avail uninstalled pkg is non fatal"""
365 with patch('apt_pkg.Cache') as cache:356 with patch('apt_pkg.Cache') as cache:
366 cache.return_value = self._apt_cache()357 cache.return_value = self._apt_cache()
367 self.assertEquals(358 self.assertEquals(
@@ -371,7 +362,7 @@
371362
372 @patch('charmhelpers.contrib.openstack.utils.error_out')363 @patch('charmhelpers.contrib.openstack.utils.error_out')
373 def test_os_version_from_package(self, mocked_error):364 def test_os_version_from_package(self, mocked_error):
374 '''Test deriving OpenStack version from an installed package'''365 """Test deriving OpenStack version from an installed package"""
375 with patch('apt_pkg.Cache') as cache:366 with patch('apt_pkg.Cache') as cache:
376 cache.return_value = self._apt_cache()367 cache.return_value = self._apt_cache()
377 for pkg, vers in six.iteritems(FAKE_REPO):368 for pkg, vers in six.iteritems(FAKE_REPO):
@@ -384,7 +375,7 @@
384375
385 @patch('charmhelpers.contrib.openstack.utils.error_out')376 @patch('charmhelpers.contrib.openstack.utils.error_out')
386 def test_os_version_from_bad_package(self, mocked_error):377 def test_os_version_from_bad_package(self, mocked_error):
387 '''Test deriving OpenStack version from an uninstalled package'''378 """Test deriving OpenStack version from an uninstalled package"""
388 with patch('apt_pkg.Cache') as cache:379 with patch('apt_pkg.Cache') as cache:
389 cache.return_value = self._apt_cache()380 cache.return_value = self._apt_cache()
390 try:381 try:
@@ -398,7 +389,7 @@
398 mocked_error.assert_called_with(e)389 mocked_error.assert_called_with(e)
399390
400 def test_os_version_from_bad_package_nonfatal(self):391 def test_os_version_from_bad_package_nonfatal(self):
401 '''Test OpenStack version from an uninstalled package is non-fatal'''392 """Test OpenStack version from an uninstalled package is non-fatal"""
402 with patch('apt_pkg.Cache') as cache:393 with patch('apt_pkg.Cache') as cache:
403 cache.return_value = self._apt_cache()394 cache.return_value = self._apt_cache()
404 self.assertEquals(395 self.assertEquals(
@@ -410,7 +401,7 @@
410 @patch.object(openstack, 'git_os_codename_install_source')401 @patch.object(openstack, 'git_os_codename_install_source')
411 @patch('charmhelpers.contrib.openstack.utils.config')402 @patch('charmhelpers.contrib.openstack.utils.config')
412 def test_os_release_uncached(self, config, git_cn, get_cn):403 def test_os_release_uncached(self, config, git_cn, get_cn):
413 openstack.os_rel = None404 openstack._os_rel = None
414 get_cn.return_value = 'folsom'405 get_cn.return_value = 'folsom'
415 git_cn.return_value = None406 git_cn.return_value = None
416407
@@ -420,69 +411,142 @@
420 self.assertEquals('folsom', openstack.os_release('nova-common'))411 self.assertEquals('folsom', openstack.os_release('nova-common'))
421412
422 def test_os_release_cached(self):413 def test_os_release_cached(self):
423 openstack.os_rel = 'foo'414 openstack._os_rel = 'foo'
424 self.assertEquals('foo', openstack.os_release('nova-common'))415 self.assertEquals('foo', openstack.os_release('nova-common'))
425416
426 @patch.object(openstack, 'juju_log')417 @patch.object(openstack, 'juju_log')
427 @patch('sys.exit')418 @patch('sys.exit')
428 def test_error_out(self, mocked_exit, juju_log):419 def test_error_out(self, mocked_exit, juju_log):
429 '''Test erroring out'''420 """Test erroring out"""
430 openstack.error_out('Everything broke.')421 openstack.error_out('Everything broke.')
431 _log = 'FATAL ERROR: Everything broke.'422 _log = 'FATAL ERROR: Everything broke.'
432 juju_log.assert_called_with(_log, level='ERROR')423 juju_log.assert_called_with(_log, level='ERROR')
433 mocked_exit.assert_called_with(1)424 mocked_exit.assert_called_with(1)
434425
426 def test_get_source_and_pgp_key(self):
427 tests = {
428 "source|key": ('source', 'key'),
429 "source|": ('source', None),
430 "|key": ('', 'key'),
431 "source": ('source', None),
432 }
433 for k, v in six.iteritems(tests):
434 self.assertEqual(openstack.get_source_and_pgp_key(k), v)
435
436 # These should still work, even though the bulk of the functionality has
437 # moved to charmhelpers.fetch.add_source()
435 def test_configure_install_source_distro(self):438 def test_configure_install_source_distro(self):
436 '''Test configuring installation from distro'''439 """Test configuring installation from distro"""
437 self.assertIsNone(openstack.configure_installation_source('distro'))440 self.assertIsNone(openstack.configure_installation_source('distro'))
438441
439 def test_configure_install_source_ppa(self):442 def test_configure_install_source_ppa(self):
440 '''Test configuring installation source from PPA'''443 """Test configuring installation source from PPA"""
441 with patch('subprocess.check_call') as mock:444 with patch('subprocess.check_call') as mock:
442 src = 'ppa:gandelman-a/openstack'445 src = 'ppa:gandelman-a/openstack'
443 openstack.configure_installation_source(src)446 openstack.configure_installation_source(src)
444 ex_cmd = ['add-apt-repository', '-y', 'ppa:gandelman-a/openstack']447 ex_cmd = [
448 'add-apt-repository', '--yes', 'ppa:gandelman-a/openstack']
445 mock.assert_called_with(ex_cmd)449 mock.assert_called_with(ex_cmd)
446450
447 @patch(builtin_open)451 @patch('subprocess.check_call')
448 @patch('charmhelpers.contrib.openstack.utils.juju_log')452 @patch.object(fetch, 'import_key')
449 @patch('charmhelpers.contrib.openstack.utils.import_key')453 def test_configure_install_source_deb_url(self, _import, _spcc):
450 def test_configure_install_source_deb_url(self, _import, _log, _open):454 """Test configuring installation source from deb repo url"""
451 '''Test configuring installation source from deb repo url'''
452 _file = MagicMock(spec=io.FileIO)
453 _open.return_value = _file
454 src = ('deb http://ubuntu-cloud.archive.canonical.com/ubuntu '455 src = ('deb http://ubuntu-cloud.archive.canonical.com/ubuntu '
455 'precise-havana main|KEYID')456 'precise-havana main|KEYID')
456 openstack.configure_installation_source(src)457 openstack.configure_installation_source(src)
457 _import.assert_called_with('KEYID')458 _import.assert_called_with('KEYID')
458 _file.__enter__().write.assert_called_with(src.split('|')[0])459 _spcc.assert_called_once_with(
459 src = ('deb http://ubuntu-cloud.archive.canonical.com/ubuntu '460 ['add-apt-repository', '--yes',
460 'precise-havana main')461 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu '
461 openstack.configure_installation_source(src)462 'precise-havana main'])
462 _file.__enter__().write.assert_called_with(src)
463463
464 @patch('charmhelpers.contrib.openstack.utils.lsb_release')464 @patch.object(fetch, 'lsb_release')
465 @patch(builtin_open)465 @patch(builtin_open)
466 @patch('charmhelpers.contrib.openstack.utils.juju_log')466 @patch('subprocess.check_call')
467 def test_configure_install_source_distro_proposed(self, _log, _open, _lsb):467 def test_configure_install_source_distro_proposed(
468 '''Test configuring installation source from deb repo url'''468 self, _spcc, _open, _lsb):
469 """Test configuring installation source from deb repo url"""
469 _lsb.return_value = FAKE_RELEASE470 _lsb.return_value = FAKE_RELEASE
470 _file = MagicMock(spec=io.FileIO)471 _file = MagicMock(spec=io.FileIO)
471 _open.return_value = _file472 _open.return_value = _file
472 openstack.configure_installation_source('distro-proposed')473 openstack.configure_installation_source('distro-proposed')
474 _file.__enter__().write.assert_called_once_with(
475 '# Proposed\ndeb http://archive.ubuntu.com/ubuntu '
476 'precise-proposed main universe multiverse restricted\n')
473 src = ('deb http://archive.ubuntu.com/ubuntu/ precise-proposed '477 src = ('deb http://archive.ubuntu.com/ubuntu/ precise-proposed '
474 'restricted main multiverse universe')478 'restricted main multiverse universe')
475 openstack.configure_installation_source(src)479 openstack.configure_installation_source(src)
476 _file.__enter__().write.assert_called_with(src)480 _spcc.assert_called_once_with(
481 ['add-apt-repository', '--yes',
482 'deb http://archive.ubuntu.com/ubuntu/ precise-proposed '
483 'restricted main multiverse universe'])
484
485 @patch('charmhelpers.fetch.filter_installed_packages')
486 @patch('charmhelpers.fetch.apt_install')
487 @patch.object(openstack, 'error_out')
488 @patch.object(openstack, 'juju_log')
489 def test_add_source_cloud_invalid_pocket(self, _log, _out,
490 apt_install, filter_pkg):
491 openstack.configure_installation_source("cloud:havana-updates")
492 _e = ('Invalid Cloud Archive release specified: '
493 'havana-updates on this Ubuntuversion')
494 _s = _out.call_args[0][0]
495 self.assertTrue(_s.startswith(_e))
496
497 @patch.object(fetch, 'filter_installed_packages')
498 @patch.object(fetch, 'apt_install')
499 @patch.object(fetch, 'lsb_release')
500 def test_add_source_cloud_pocket_style(self, lsb_release,
501 apt_install, filter_pkg):
502 source = "cloud:precise-updates/havana"
503 lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
504 result = (
505 "# Ubuntu Cloud Archive\n"
506 "deb http://ubuntu-cloud.archive.canonical.com/ubuntu "
507 "precise-updates/havana main\n")
508 with patch_open() as (mock_open, mock_file):
509 openstack.configure_installation_source(source)
510 mock_file.write.assert_called_with(result)
511 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
512
513 @patch.object(fetch, 'filter_installed_packages')
514 @patch.object(fetch, 'apt_install')
515 @patch.object(fetch, 'lsb_release')
516 def test_add_source_cloud_os_style(self, lsb_release,
517 apt_install, filter_pkg):
518 source = "cloud:precise-havana"
519 lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
520 result = (
521 "# Ubuntu Cloud Archive\n"
522 "deb http://ubuntu-cloud.archive.canonical.com/ubuntu "
523 "precise-updates/havana main\n")
524 with patch_open() as (mock_open, mock_file):
525 openstack.configure_installation_source(source)
526 mock_file.write.assert_called_with(result)
527 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
528
529 @patch.object(fetch, 'filter_installed_packages')
530 @patch.object(fetch, 'apt_install')
531 def test_add_source_cloud_distroless_style(self, apt_install, filter_pkg):
532 source = "cloud:havana"
533 result = (
534 "# Ubuntu Cloud Archive\n"
535 "deb http://ubuntu-cloud.archive.canonical.com/ubuntu "
536 "precise-updates/havana main\n")
537 with patch_open() as (mock_open, mock_file):
538 openstack.configure_installation_source(source)
539 mock_file.write.assert_called_with(result)
540 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
477541
478 @patch('charmhelpers.contrib.openstack.utils.error_out')542 @patch('charmhelpers.contrib.openstack.utils.error_out')
479 def test_configure_bad_install_source(self, _error):543 def test_configure_bad_install_source(self, _error):
480 openstack.configure_installation_source('foo')544 openstack.configure_installation_source('foo')
481 _error.assert_called_with('Invalid openstack-release specified: foo')545 _error.assert_called_with("Unknown source: 'foo'")
482546
483 @patch('charmhelpers.contrib.openstack.utils.lsb_release')547 @patch.object(fetch, 'lsb_release')
484 def test_configure_install_source_uca_staging(self, _lsb):548 def test_configure_install_source_uca_staging(self, _lsb):
485 '''Test configuring installation source from UCA staging sources'''549 """Test configuring installation source from UCA staging sources"""
486 _lsb.return_value = FAKE_RELEASE550 _lsb.return_value = FAKE_RELEASE
487 # staging pockets are configured as PPAs551 # staging pockets are configured as PPAs
488 with patch('subprocess.check_call') as _subp:552 with patch('subprocess.check_call') as _subp:
@@ -493,76 +557,63 @@
493 _subp.assert_called_with(cmd)557 _subp.assert_called_with(cmd)
494558
495 @patch(builtin_open)559 @patch(builtin_open)
496 @patch('charmhelpers.contrib.openstack.utils.apt_install')560 @patch.object(fetch, 'apt_install')
497 @patch('charmhelpers.contrib.openstack.utils.lsb_release')561 @patch.object(fetch, 'lsb_release')
498 def test_configure_install_source_uca_repos(self, _lsb, _install, _open):562 @patch.object(fetch, 'filter_installed_packages')
499 '''Test configuring installation source from UCA sources'''563 def test_configure_install_source_uca_repos(
564 self, _fip, _lsb, _install, _open):
565 """Test configuring installation source from UCA sources"""
500 _lsb.return_value = FAKE_RELEASE566 _lsb.return_value = FAKE_RELEASE
501 _file = MagicMock(spec=io.FileIO)567 _file = MagicMock(spec=io.FileIO)
502 _open.return_value = _file568 _open.return_value = _file
569 _fip.side_effect = lambda x: x
503 for src, url in UCA_SOURCES:570 for src, url in UCA_SOURCES:
571 actual_url = "# Ubuntu Cloud Archive\n{}\n".format(url)
504 openstack.configure_installation_source(src)572 openstack.configure_installation_source(src)
505 _install.assert_called_with('ubuntu-cloud-keyring',573 _install.assert_called_with(['ubuntu-cloud-keyring'],
506 fatal=True)574 fatal=True)
507 _open.assert_called_with(575 _open.assert_called_with(
508 '/etc/apt/sources.list.d/cloud-archive.list',576 '/etc/apt/sources.list.d/cloud-archive.list',
509 'w'577 'w'
510 )578 )
511 _file.__enter__().write.assert_called_with(url)579 _file.__enter__().write.assert_called_with(actual_url)
512580
513 @patch('charmhelpers.contrib.openstack.utils.error_out')581 @patch('charmhelpers.contrib.openstack.utils.error_out')
514 def test_configure_install_source_bad_uca(self, mocked_error):582 def test_configure_install_source_bad_uca(self, mocked_error):
515 '''Test configuring installation source from bad UCA source'''583 """Test configuring installation source from bad UCA source"""
516 try:584 try:
517 openstack.configure_installation_source('cloud:foo-bar')585 openstack.configure_installation_source('cloud:foo-bar')
518 except:586 except:
519 # ignore exceptions that raise when error_out is mocked587 # ignore exceptions that raise when error_out is mocked
520 # and doesn't sys.exit(1)588 # and doesn't sys.exit(1)
521 pass589 pass
522 _e = 'Invalid Cloud Archive release specified: foo-bar'590 _e = ('Invalid Cloud Archive release specified: foo-bar'
523 mocked_error.assert_called_with(_e)591 ' on this Ubuntuversion')
524592 _s = mocked_error.call_args[0][0]
525 @patch.object(openstack, 'juju_log', lambda *args, **kwargs: None)593 self.assertTrue(_s.startswith(_e))
526 def test_import_apt_key_radix(self):594
527 '''Ensure shell out apt-key during key import'''595 @patch.object(openstack, 'fetch_import_key')
528 with patch('subprocess.check_call') as _subp:596 def test_import_key_calls_fetch_import_key(self, fetch_import_key):
529 openstack.import_key('foo')597 openstack.import_key('random-string')
530 cmd = ['apt-key', 'adv', '--keyserver',598 fetch_import_key.assert_called_once_with('random-string')
531 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']599
532 _subp.assert_called_with(cmd)600 @patch.object(openstack, 'fetch_import_key')
533601 @patch.object(openstack, 'sys')
534 @patch.object(openstack, 'juju_log', lambda *args, **kwargs: None)602 def test_import_key_calls_sys_exit_on_error(self, mock_sys,
535 def test_import_apt_key_ascii_armor(self):603 fetch_import_key):
536 with tempfile.NamedTemporaryFile() as tmp:604
537 with patch.object(openstack, 'tempfile') as \605 def raiser(_):
538 mock_tmpfile:606 raise openstack.GPGKeyError("an error occurred")
539 tmpfile = mock_tmpfile.NamedTemporaryFile.return_value607 fetch_import_key.side_effect = raiser
540 tmpfile.__enter__.return_value = tmpfile608 openstack.import_key('random failure')
541 tmpfile.name = tmp.name609 mock_sys.exit.assert_called_once_with(1)
542 with patch('subprocess.check_call') as _subp:
543 openstack.import_key(PGP_KEY_ASCII_ARMOR)
544 cmd = ['apt-key', 'add', tmp.name]
545 _subp.assert_called_with(cmd)
546
547 @patch.object(openstack, 'juju_log', lambda *args, **kwargs: None)
548 @patch('charmhelpers.contrib.openstack.utils.error_out')
549 def test_import_bad_apt_key(self, mocked_error):
550 '''Ensure error when importing apt key fails'''
551 with patch('subprocess.check_call') as _subp:
552 cmd = ['apt-key', 'adv', '--keyserver',
553 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']
554 _subp.side_effect = subprocess.CalledProcessError(1, cmd, '')
555 openstack.import_key('foo')
556 cmd = ['apt-key', 'adv', '--keyserver',
557 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']
558 mocked_error.assert_called_with("Error importing PGP key 'foo'")
559610
560 @patch('os.mkdir')611 @patch('os.mkdir')
561 @patch('os.path.exists')612 @patch('os.path.exists')
562 @patch('charmhelpers.contrib.openstack.utils.charm_dir')613 @patch('charmhelpers.contrib.openstack.utils.charm_dir')
563 @patch(builtin_open)614 @patch(builtin_open)
564 def test_save_scriptrc(self, _open, _charm_dir, _exists, _mkdir):615 def test_save_scriptrc(self, _open, _charm_dir, _exists, _mkdir):
565 '''Test generation of scriptrc from environment'''616 """Test generation of scriptrc from environment"""
566 scriptrc = ['#!/bin/bash\n',617 scriptrc = ['#!/bin/bash\n',
567 'export setting1=foo\n',618 'export setting1=foo\n',
568 'export setting2=bar\n']619 'export setting2=bar\n']
@@ -618,14 +669,14 @@
618 @patch.object(openstack, 'is_block_device')669 @patch.object(openstack, 'is_block_device')
619 @patch.object(openstack, 'error_out')670 @patch.object(openstack, 'error_out')
620 def test_ensure_block_device_bad_config(self, err, is_bd):671 def test_ensure_block_device_bad_config(self, err, is_bd):
621 '''Test it doesn't prepare storage with bad config'''672 """Test it doesn't prepare storage with bad config"""
622 openstack.ensure_block_device(block_device='none')673 openstack.ensure_block_device(block_device='none')
623 self.assertTrue(err.called)674 self.assertTrue(err.called)
624675
625 @patch.object(openstack, 'is_block_device')676 @patch.object(openstack, 'is_block_device')
626 @patch.object(openstack, 'ensure_loopback_device')677 @patch.object(openstack, 'ensure_loopback_device')
627 def test_ensure_block_device_loopback(self, ensure_loopback, is_bd):678 def test_ensure_block_device_loopback(self, ensure_loopback, is_bd):
628 '''Test it ensures loopback device when checking block device'''679 """Test it ensures loopback device when checking block device"""
629 defsize = openstack.DEFAULT_LOOPBACK_SIZE680 defsize = openstack.DEFAULT_LOOPBACK_SIZE
630 is_bd.return_value = True681 is_bd.return_value = True
631682
@@ -641,7 +692,7 @@
641692
642 @patch.object(openstack, 'is_block_device')693 @patch.object(openstack, 'is_block_device')
643 def test_ensure_standard_block_device(self, is_bd):694 def test_ensure_standard_block_device(self, is_bd):
644 '''Test it looks for storage at both relative and full device path'''695 """Test it looks for storage at both relative and full device path"""
645 for dev in ['vdb', '/dev/vdb']:696 for dev in ['vdb', '/dev/vdb']:
646 openstack.ensure_block_device(dev)697 openstack.ensure_block_device(dev)
647 is_bd.assert_called_with('/dev/vdb')698 is_bd.assert_called_with('/dev/vdb')
@@ -649,7 +700,7 @@
649 @patch.object(openstack, 'is_block_device')700 @patch.object(openstack, 'is_block_device')
650 @patch.object(openstack, 'error_out')701 @patch.object(openstack, 'error_out')
651 def test_ensure_nonexistent_block_device(self, error_out, is_bd):702 def test_ensure_nonexistent_block_device(self, error_out, is_bd):
652 '''Test it will not ensure a non-existant block device'''703 """Test it will not ensure a non-existant block device"""
653 is_bd.return_value = False704 is_bd.return_value = False
654 openstack.ensure_block_device(block_device='foo')705 openstack.ensure_block_device(block_device='foo')
655 self.assertTrue(error_out.called)706 self.assertTrue(error_out.called)
@@ -660,7 +711,7 @@
660 @patch.object(openstack, 'zap_disk')711 @patch.object(openstack, 'zap_disk')
661 @patch.object(openstack, 'is_lvm_physical_volume')712 @patch.object(openstack, 'is_lvm_physical_volume')
662 def test_clean_storage_unmount(self, is_pv, zap_disk, mounts, umount, log):713 def test_clean_storage_unmount(self, is_pv, zap_disk, mounts, umount, log):
663 '''Test it unmounts block device when cleaning storage'''714 """Test it unmounts block device when cleaning storage"""
664 is_pv.return_value = False715 is_pv.return_value = False
665 zap_disk.return_value = True716 zap_disk.return_value = True
666 mounts.return_value = MOUNTS717 mounts.return_value = MOUNTS
@@ -673,7 +724,7 @@
673 @patch.object(openstack, 'mounts')724 @patch.object(openstack, 'mounts')
674 @patch.object(openstack, 'is_lvm_physical_volume')725 @patch.object(openstack, 'is_lvm_physical_volume')
675 def test_clean_storage_lvm_wipe(self, is_pv, mounts, rm_lv, rm_vg, log):726 def test_clean_storage_lvm_wipe(self, is_pv, mounts, rm_lv, rm_vg, log):
676 '''Test it removes traces of LVM when cleaning storage'''727 """Test it removes traces of LVM when cleaning storage"""
677 mounts.return_value = []728 mounts.return_value = []
678 is_pv.return_value = True729 is_pv.return_value = True
679 openstack.clean_storage('/dev/vdb')730 openstack.clean_storage('/dev/vdb')
@@ -684,7 +735,7 @@
684 @patch.object(openstack, 'is_lvm_physical_volume')735 @patch.object(openstack, 'is_lvm_physical_volume')
685 @patch.object(openstack, 'mounts')736 @patch.object(openstack, 'mounts')
686 def test_clean_storage_zap_disk(self, mounts, is_pv, zap_disk):737 def test_clean_storage_zap_disk(self, mounts, is_pv, zap_disk):
687 '''It removes traces of LVM when cleaning storage'''738 """It removes traces of LVM when cleaning storage"""
688 mounts.return_value = []739 mounts.return_value = []
689 is_pv.return_value = False740 is_pv.return_value = False
690 openstack.clean_storage('/dev/vdb')741 openstack.clean_storage('/dev/vdb')
691742
=== modified file 'tests/contrib/openstack/test_os_utils.py'
--- tests/contrib/openstack/test_os_utils.py 2017-03-24 16:50:31 +0000
+++ tests/contrib/openstack/test_os_utils.py 2017-04-10 18:12:16 +0000
@@ -126,7 +126,7 @@
126 mock_git_os_codename_install_source,126 mock_git_os_codename_install_source,
127 mock_config):127 mock_config):
128 # Wipe the modules cached os_rel128 # Wipe the modules cached os_rel
129 utils.os_rel = None129 utils._os_rel = None
130 mock_get_os_codename_install_source.return_value = None130 mock_get_os_codename_install_source.return_value = None
131 mock_get_os_codename_package.return_value = None131 mock_get_os_codename_package.return_value = None
132 mock_git_os_codename_install_source.return_value = None132 mock_git_os_codename_install_source.return_value = None
133133
=== modified file 'tests/fetch/test_fetch.py'
--- tests/fetch/test_fetch.py 2017-03-03 20:18:47 +0000
+++ tests/fetch/test_fetch.py 2017-04-10 18:12:16 +0000
@@ -1,25 +1,22 @@
1import subprocess
2import six1import six
3import os2import os
4import yaml3import yaml
5import imp
6import tempfile
74
8from charmhelpers import osplatform
9from tests.helpers import patch_open
10from testtools import TestCase5from testtools import TestCase
11from mock import (6from mock import (
12 patch,7 patch,
13 MagicMock,8 MagicMock,
14 call,9 call,
15 sentinel,
16)10)
11
17from charmhelpers import fetch12from charmhelpers import fetch
18from six.moves import StringIO13
19if six.PY3:14if six.PY3:
20 from urllib.parse import urlparse15 from urllib.parse import urlparse
16 builtin_open = 'builtins.open'
21else:17else:
22 from urlparse import urlparse18 from urlparse import urlparse
19 builtin_open = '__builtin__.open'
2320
2421
25FAKE_APT_CACHE = {22FAKE_APT_CACHE = {
@@ -60,479 +57,7 @@
6057
61class FetchTest(TestCase):58class FetchTest(TestCase):
6259
63 @patch("charmhelpers.fetch.ubuntu.log")60 @patch('charmhelpers.fetch.log')
64 @patch.object(osplatform, 'get_platform')
65 @patch('apt_pkg.Cache')
66 def test_filter_packages_missing_ubuntu(self, cache, platform, log):
67 platform.return_value = 'ubuntu'
68 imp.reload(fetch)
69
70 cache.side_effect = fake_apt_cache
71 result = fetch.filter_installed_packages(['vim', 'emacs'])
72 self.assertEquals(result, ['emacs'])
73
74 @patch("charmhelpers.fetch.centos.log")
75 @patch('yum.YumBase.doPackageLists')
76 @patch.object(osplatform, 'get_platform')
77 def test_filter_packages_missing_centos(self, platform, yumBase, log):
78 platform.return_value = 'centos'
79 imp.reload(fetch)
80
81 class MockPackage:
82 def __init__(self, name):
83 self.base_package_name = name
84
85 yum_dict = {
86 'installed': {
87 MockPackage('vim')
88 },
89 'available': {
90 MockPackage('vim')
91 }
92 }
93 import yum
94 yum.YumBase.return_value.doPackageLists.return_value = yum_dict
95 result = fetch.filter_installed_packages(['vim', 'emacs'])
96 self.assertEquals(result, ['emacs'])
97
98 @patch("charmhelpers.fetch.ubuntu.log")
99 @patch.object(osplatform, 'get_platform')
100 @patch('apt_pkg.Cache')
101 def test_filter_packages_none_missing_ubuntu(self, cache, platform, log):
102 platform.return_value = 'ubuntu'
103 imp.reload(fetch)
104
105 cache.side_effect = fake_apt_cache
106 result = fetch.filter_installed_packages(['vim'])
107 self.assertEquals(result, [])
108
109 @patch("charmhelpers.fetch.centos.log")
110 @patch.object(osplatform, 'get_platform')
111 def test_filter_packages_none_missing_centos(self, platform, log):
112 platform.return_value = 'centos'
113 imp.reload(fetch)
114
115 class MockPackage:
116 def __init__(self, name):
117 self.base_package_name = name
118
119 yum_dict = {
120 'installed': {
121 MockPackage('vim')
122 },
123 'available': {
124 MockPackage('vim')
125 }
126 }
127 import yum
128 yum.yumBase.return_value.doPackageLists.return_value = yum_dict
129 result = fetch.filter_installed_packages(['vim'])
130 self.assertEquals(result, [])
131
132 @patch.object(osplatform, 'get_platform')
133 @patch('charmhelpers.fetch.ubuntu.log')
134 @patch('apt_pkg.Cache')
135 def test_filter_packages_not_available_ubuntu(self, cache, log, platform):
136 platform.return_value = 'ubuntu'
137 imp.reload(fetch)
138
139 cache.side_effect = fake_apt_cache
140 result = fetch.filter_installed_packages(['vim', 'joe'])
141 self.assertEquals(result, ['joe'])
142 log.assert_called_with('Package joe has no installation candidate.',
143 level='WARNING')
144
145 @patch.object(osplatform, 'get_platform')
146 @patch('charmhelpers.fetch.centos.log')
147 @patch('yum.YumBase.doPackageLists')
148 def test_filter_packages_not_available_centos(self, yumBase,
149 log, platform):
150 platform.return_value = 'centos'
151 imp.reload(fetch)
152
153 class MockPackage:
154 def __init__(self, name):
155 self.base_package_name = name
156
157 yum_dict = {
158 'installed': {
159 MockPackage('vim')
160 }
161 }
162 import yum
163 yum.YumBase.return_value.doPackageLists.return_value = yum_dict
164
165 result = fetch.filter_installed_packages(['vim', 'joe'])
166 self.assertEquals(result, ['joe'])
167
168 @patch.object(osplatform, 'get_platform')
169 @patch('charmhelpers.fetch.ubuntu.log')
170 def test_add_source_none_ubuntu(self, log, platform):
171 platform.return_value = 'ubuntu'
172 imp.reload(fetch)
173
174 fetch.add_source(source=None)
175 self.assertTrue(log.called)
176
177 @patch.object(osplatform, 'get_platform')
178 @patch('charmhelpers.fetch.centos.log')
179 def test_add_source_none_centos(self, log, platform):
180 platform.return_value = 'centos'
181 imp.reload(fetch)
182
183 fetch.add_source(source=None)
184 self.assertTrue(log.called)
185
186 @patch.object(osplatform, 'get_platform')
187 @patch('subprocess.check_call')
188 def test_add_source_ppa(self, check_call, platform):
189 platform.return_value = 'ubuntu'
190 imp.reload(fetch)
191
192 source = "ppa:test-ppa"
193 fetch.add_source(source=source)
194 check_call.assert_called_with(
195 ['add-apt-repository', '--yes', source], env=getenv())
196
197 @patch("charmhelpers.fetch.ubuntu.log")
198 @patch.object(osplatform, 'get_platform')
199 @patch('subprocess.check_call')
200 @patch('time.sleep')
201 def test_add_source_ppa_retries_30_times(self, sleep, check_call,
202 platform, log):
203 platform.return_value = 'ubuntu'
204 imp.reload(fetch)
205
206 self.call_count = 0
207
208 def side_effect(*args, **kwargs):
209 """Raise an 3 times, then return 0 """
210 self.call_count += 1
211 if self.call_count <= fetch.ubuntu.CMD_RETRY_COUNT:
212 raise subprocess.CalledProcessError(
213 returncode=1, cmd="some add-apt-repository command")
214 else:
215 return 0
216 check_call.side_effect = side_effect
217
218 source = "ppa:test-ppa"
219 fetch.add_source(source=source)
220 check_call.assert_called_with(
221 ['add-apt-repository', '--yes', source], env=getenv())
222 sleep.assert_called_with(10)
223 self.assertTrue(fetch.ubuntu.CMD_RETRY_COUNT, sleep.call_count)
224
225 @patch('charmhelpers.fetch.ubuntu.log')
226 @patch.object(osplatform, 'get_platform')
227 @patch('subprocess.check_call')
228 def test_add_source_http_ubuntu(self, check_call, platform, log):
229 platform.return_value = 'ubuntu'
230 imp.reload(fetch)
231
232 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
233 fetch.add_source(source=source)
234 check_call.assert_called_with(
235 ['add-apt-repository', '--yes', source], env=getenv())
236
237 @patch('charmhelpers.fetch.centos.log')
238 @patch.object(osplatform, 'get_platform')
239 @patch('os.listdir')
240 def test_add_source_http_centos(self, listdir, platform, log):
241 platform.return_value = 'centos'
242 imp.reload(fetch)
243
244 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
245 with patch_open() as (mock_open, mock_file):
246 fetch.add_source(source=source)
247 listdir.assert_called_with('/etc/yum.repos.d/')
248 mock_file.write.assert_has_calls([
249 call("[archive.ubuntu.com_ubuntu raring-backports main]\n"),
250 call("name=archive.ubuntu.com/ubuntu raring-backports main\n"),
251 call("baseurl=http://archive.ubuntu.com/ubuntu raring"
252 "-backports main\n\n")])
253
254 @patch('charmhelpers.fetch.ubuntu.log')
255 @patch.object(osplatform, 'get_platform')
256 @patch('subprocess.check_call')
257 def test_add_source_https(self, check_call, platform, log):
258 platform.return_value = 'ubuntu'
259 imp.reload(fetch)
260
261 source = "https://example.com"
262 fetch.add_source(source=source)
263 check_call.assert_called_with(
264 ['add-apt-repository', '--yes', source], env=getenv())
265
266 @patch('charmhelpers.fetch.ubuntu.log')
267 @patch.object(osplatform, 'get_platform')
268 @patch('subprocess.check_call')
269 def test_add_source_deb(self, check_call, platform, log):
270 """add-apt-repository behaves differently when using the deb prefix.
271
272 $ add-apt-repository --yes "http://special.example.com/ubuntu
273 precise-special main"
274 $ grep special /etc/apt/sources.list
275 deb http://special.example.com/ubuntu precise precise-special main
276 deb-src http://special.example.com/ubuntu precise precise-special main
277
278 $ add-apt-repository --yes "deb http://special.example.com/ubuntu
279 precise-special main"
280 $ grep special /etc/apt/sources.list
281 deb http://special.example.com/ubuntu precise precise-special main
282 deb-src http://special.example.com/ubuntu precise precise-special main
283 deb http://special.example.com/ubuntu precise-special main
284 deb-src http://special.example.com/ubuntu precise-special main
285 """
286 platform.return_value = 'ubuntu'
287 imp.reload(fetch)
288
289 source = "deb http://archive.ubuntu.com/ubuntu raring-backports main"
290 fetch.add_source(source=source)
291 check_call.assert_called_with(
292 ['add-apt-repository', '--yes', source], env=getenv())
293
294 @patch.object(osplatform, 'get_platform')
295 @patch.object(fetch.ubuntu, 'filter_installed_packages')
296 @patch.object(fetch.ubuntu, 'install')
297 def test_add_source_cloud_invalid_pocket(self, install,
298 filter_pkg, platform):
299 platform.return_value = 'ubuntu'
300 imp.reload(fetch)
301
302 source = "cloud:havana-updates"
303 self.assertRaises(fetch.ubuntu.SourceConfigError,
304 fetch.add_source, source)
305 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
306
307 @patch('charmhelpers.fetch.ubuntu.log')
308 @patch.object(osplatform, 'get_platform')
309 @patch.object(fetch.ubuntu, 'filter_installed_packages')
310 @patch.object(fetch.ubuntu, 'install')
311 def test_add_source_cloud_pocket_style(self, install, filter_pkg,
312 platform, log):
313 platform.return_value = 'ubuntu'
314 imp.reload(fetch)
315
316 source = "cloud:precise-updates/havana"
317 result = ('# Ubuntu Cloud Archive\n'
318 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
319 ' precise-updates/havana main\n')
320
321 with patch_open() as (mock_open, mock_file):
322 fetch.add_source(source=source)
323 mock_file.write.assert_called_with(result)
324 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
325
326 @patch('charmhelpers.fetch.ubuntu.log')
327 @patch.object(osplatform, 'get_platform')
328 @patch.object(fetch.ubuntu, 'filter_installed_packages')
329 @patch.object(fetch.ubuntu, 'install')
330 def test_add_source_cloud_os_style(self, install, filter_pkg,
331 platform, log):
332 platform.return_value = 'ubuntu'
333 imp.reload(fetch)
334
335 source = "cloud:precise-havana"
336 result = ('# Ubuntu Cloud Archive\n'
337 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
338 ' precise-updates/havana main\n')
339 with patch_open() as (mock_open, mock_file):
340 fetch.add_source(source=source)
341 mock_file.write.assert_called_with(result)
342 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
343
344 @patch('charmhelpers.fetch.ubuntu.log')
345 @patch.object(osplatform, 'get_platform')
346 @patch.object(fetch.ubuntu, 'filter_installed_packages')
347 @patch.object(fetch.ubuntu, 'install')
348 def test_add_source_cloud_distroless_style(self, install, filter_pkg,
349 platform, log):
350 platform.return_value = 'ubuntu'
351 imp.reload(fetch)
352
353 source = "cloud:havana"
354 result = ('# Ubuntu Cloud Archive\n'
355 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
356 ' precise-updates/havana main\n')
357 with patch_open() as (mock_open, mock_file):
358 fetch.add_source(source=source)
359 mock_file.write.assert_called_with(result)
360 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
361
362 @patch('charmhelpers.fetch.ubuntu.log')
363 @patch.object(osplatform, 'get_platform')
364 @patch.object(fetch.ubuntu, 'lsb_release')
365 def test_add_source_proposed(self, lsb_release, platform, log):
366 platform.return_value = 'ubuntu'
367 imp.reload(fetch)
368
369 source = "proposed"
370 result = ('# Proposed\n'
371 'deb http://archive.ubuntu.com/ubuntu precise-proposed'
372 ' main universe multiverse restricted\n')
373 lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
374 with patch_open() as (mock_open, mock_file):
375 fetch.add_source(source=source)
376 mock_file.write.assert_called_with(result)
377
378 @patch('charmhelpers.fetch.ubuntu.log')
379 @patch.object(osplatform, 'get_platform')
380 @patch('subprocess.check_call')
381 def test_add_source_http_and_key_id_ubuntu(self, check_call,
382 platform, log):
383 platform.return_value = 'ubuntu'
384 imp.reload(fetch)
385
386 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
387 key_id = "akey"
388 check_call.return_value = 0 # Successful exit code
389 fetch.add_source(source=source, key=key_id)
390 check_call.assert_has_calls([
391 call(['add-apt-repository', '--yes', source], env=getenv()),
392 call(['apt-key', 'adv', '--keyserver',
393 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])
394 ])
395
396 @patch('charmhelpers.fetch.centos.log')
397 @patch('os.listdir')
398 @patch.object(osplatform, 'get_platform')
399 @patch('subprocess.check_call')
400 def test_add_source_http_and_key_id_centos(self, check_call,
401 platform, listdir, log):
402 platform.return_value = 'centos'
403 imp.reload(fetch)
404
405 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
406 key_id = "akey"
407 with patch_open() as (mock_open, mock_file):
408 fetch.add_source(source=source, key=key_id)
409 listdir.assert_called_with('/etc/yum.repos.d/')
410 mock_file.write.assert_has_calls([
411 call("[archive.ubuntu.com_ubuntu raring-backports main]\n"),
412 call("name=archive.ubuntu.com/ubuntu raring-backports main\n"),
413 call("baseurl=http://archive.ubuntu.com/ubuntu raring"
414 "-backports main\n\n")])
415 check_call.assert_called_with(['rpm', '--import', key_id])
416
417 @patch('charmhelpers.fetch.ubuntu.log')
418 @patch.object(osplatform, 'get_platform')
419 @patch('subprocess.check_call')
420 def test_add_source_https_and_key_id_ubuntu(self, check_call,
421 platform, log):
422 check_call.return_value = 0 # Success from both calls
423 platform.return_value = 'ubuntu'
424 imp.reload(fetch)
425
426 source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
427 key_id = "GPGPGP"
428 fetch.add_source(source=source, key=key_id)
429 check_call.assert_has_calls([
430 call(['add-apt-repository', '--yes', source], env=getenv()),
431 call(['apt-key', 'adv', '--keyserver',
432 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])
433 ])
434
435 @patch('charmhelpers.fetch.centos.log')
436 @patch('os.listdir')
437 @patch.object(osplatform, 'get_platform')
438 @patch('subprocess.check_call')
439 def test_add_source_https_and_key_id_centos(self, check_call,
440 platform, listdir, log):
441 platform.return_value = 'centos'
442 imp.reload(fetch)
443
444 source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
445 key_id = "GPGPGP"
446 with patch_open() as (mock_open, mock_file):
447 fetch.add_source(source=source, key=key_id)
448 listdir.assert_called_with('/etc/yum.repos.d/')
449 mock_file.write.assert_has_calls([
450 call("[_USER:PASS@private-ppa.launchpad"
451 ".net_project_awesome]\n"),
452 call("name=/USER:PASS@private-ppa.launchpad.net"
453 "/project/awesome\n"),
454 call("baseurl=https://USER:PASS@private-ppa.launchpad.net"
455 "/project/awesome\n\n")])
456 check_call.assert_called_with(['rpm', '--import', key_id])
457
458 @patch('charmhelpers.fetch.ubuntu.log')
459 @patch.object(osplatform, 'get_platform')
460 @patch('subprocess.check_call')
461 def test_add_source_http_and_key_ubuntu(self, check_call, platform, log):
462 platform.return_value = 'ubuntu'
463 imp.reload(fetch)
464
465 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
466 key = '''
467 -----BEGIN PGP PUBLIC KEY BLOCK-----
468 [...]
469 -----END PGP PUBLIC KEY BLOCK-----
470 '''
471
472 received_args = []
473 received_key = StringIO()
474
475 def _check_call(arg, stdin=None, env=None):
476 '''side_effect to store the stdin passed to check_call process.'''
477 if stdin is not None:
478 received_args.extend(arg)
479 received_key.write(stdin.read())
480 return 0 # Successful return code checked by add-apt-repository
481
482 with patch('subprocess.check_call',
483 side_effect=_check_call) as check_call:
484 fetch.add_source(source=source, key=key)
485 check_call.assert_any_call(
486 ['add-apt-repository', '--yes', source], env=getenv())
487 self.assertEqual(['apt-key', 'add', '-'], received_args)
488 self.assertEqual(key, received_key.getvalue())
489
490 @patch('charmhelpers.fetch.centos.log')
491 @patch.object(tempfile, 'NamedTemporaryFile')
492 @patch('os.listdir')
493 @patch.object(osplatform, 'get_platform')
494 @patch('subprocess.check_call')
495 def test_add_source_http_and_key_centos(self, check_call, platform,
496 listdir, temp_file, log):
497 platform.return_value = 'centos'
498 imp.reload(fetch)
499
500 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
501 key = '''
502 -----BEGIN PGP PUBLIC KEY BLOCK-----
503 [...]
504 -----END PGP PUBLIC KEY BLOCK-----
505 '''
506 temp_file.return_value.__enter__.return_value = key
507 temp_file.return_value.__exit__.return_value = key
508
509 with patch_open() as (mock_open, mock_file):
510 fetch.add_source(source=source, key=key)
511 listdir.assert_called_with('/etc/yum.repos.d/')
512 self.assertTrue(log.called)
513 check_call.assert_called_with(['rpm', '--import', key])
514
515 @patch.object(osplatform, 'get_platform')
516 @patch('charmhelpers.fetch.ubuntu.log')
517 def test_add_unparsable_source(self, log_, platform):
518 platform.return_value = 'ubuntu'
519 imp.reload(fetch)
520
521 source = "propsed" # Minor typo
522 fetch.add_source(source=source)
523 self.assertEqual(1, log_.call_count)
524
525 @patch('charmhelpers.fetch.ubuntu.log')
526 @patch.object(osplatform, 'get_platform')
527 def test_add_distro_source(self, platform, log):
528 platform.return_value = 'ubuntu'
529 imp.reload(fetch)
530
531 source = "distro"
532 # distro is a noop but test validate no exception is thrown
533 fetch.add_source(source=source)
534
535 @patch('charmhelpers.fetch.ubuntu.log')
536 @patch.object(fetch, 'config')61 @patch.object(fetch, 'config')
537 @patch.object(fetch, 'add_source')62 @patch.object(fetch, 'add_source')
538 def test_configure_sources_single_source(self, add_source, config, log):63 def test_configure_sources_single_source(self, add_source, config, log):
@@ -587,12 +112,11 @@
587 ]112 ]
588 self.assertRaises(fetch.SourceConfigError, fetch.configure_sources)113 self.assertRaises(fetch.SourceConfigError, fetch.configure_sources)
589114
590 @patch.object(osplatform, 'get_platform')115 @patch.object(fetch, '_fetch_update')
591 @patch('charmhelpers.fetch.ubuntu.update')
592 @patch.object(fetch, 'config')116 @patch.object(fetch, 'config')
593 @patch.object(fetch, 'add_source')117 @patch.object(fetch, 'add_source')
594 def test_configure_sources_update_called_ubuntu(self, add_source, config,118 def test_configure_sources_update_called_ubuntu(self, add_source, config,
595 update, platform):119 update):
596 config.side_effect = ['source', 'key']120 config.side_effect = ['source', 'key']
597 fetch.configure_sources(update=True)121 fetch.configure_sources(update=True)
598 add_source.assert_called_with('source', 'key')122 add_source.assert_called_with('source', 'key')
@@ -703,7 +227,8 @@
703 self.assertEqual(2, log_.call_count)227 self.assertEqual(2, log_.call_count)
704228
705 @patch('charmhelpers.fetch.log')229 @patch('charmhelpers.fetch.log')
706 def test_plugins_are_valid(self, log_):230 @patch.object(fetch.importlib, 'import_module')
231 def test_plugins_are_valid(self, import_module, log_):
707 plugins = fetch.plugins()232 plugins = fetch.plugins()
708 self.assertEqual(len(fetch.FETCH_HANDLERS), len(plugins))233 self.assertEqual(len(fetch.FETCH_HANDLERS), len(plugins))
709234
@@ -742,572 +267,3 @@
742 expected_url = "http://example.com/foo"267 expected_url = "http://example.com/foo"
743 u = self.fh.base_url(sample_url)268 u = self.fh.base_url(sample_url)
744 self.assertEqual(u, expected_url)269 self.assertEqual(u, expected_url)
745
746
747class AptTests(TestCase):
748
749 @patch.object(osplatform, 'get_platform')
750 @patch('subprocess.call')
751 @patch('charmhelpers.fetch.ubuntu.log')
752 def test_apt_upgrade_non_fatal(self, log, mock_call, platform):
753 platform.return_value = 'ubuntu'
754 imp.reload(fetch)
755
756 options = ['--foo', '--bar']
757 fetch.apt_upgrade(options)
758
759 mock_call.assert_called_with(
760 ['apt-get', '--assume-yes',
761 '--foo', '--bar', 'upgrade'],
762 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
763
764 @patch.object(osplatform, 'get_platform')
765 @patch('subprocess.check_call')
766 @patch('charmhelpers.fetch.ubuntu.log')
767 def test_apt_upgrade_fatal(self, log, mock_call, platform):
768 platform.return_value = 'ubuntu'
769 imp.reload(fetch)
770
771 options = ['--foo', '--bar']
772 fetch.apt_upgrade(options, fatal=True)
773
774 mock_call.assert_called_with(
775 ['apt-get', '--assume-yes',
776 '--foo', '--bar', 'upgrade'],
777 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
778
779 @patch.object(osplatform, 'get_platform')
780 @patch('subprocess.check_call')
781 @patch('charmhelpers.fetch.ubuntu.log')
782 def test_apt_dist_upgrade_fatal(self, log, mock_call, platform):
783 platform.return_value = 'ubuntu'
784 imp.reload(fetch)
785
786 options = ['--foo', '--bar']
787 fetch.apt_upgrade(options, fatal=True, dist=True)
788
789 mock_call.assert_called_with(
790 ['apt-get', '--assume-yes',
791 '--foo', '--bar', 'dist-upgrade'],
792 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
793
794 @patch.object(osplatform, 'get_platform')
795 @patch('subprocess.call')
796 @patch('charmhelpers.fetch.ubuntu.log')
797 def test_installs_apt_packages(self, log, mock_call, platform):
798 platform.return_value = 'ubuntu'
799 imp.reload(fetch)
800
801 packages = ['foo', 'bar']
802 options = ['--foo', '--bar']
803
804 fetch.apt_install(packages, options)
805
806 mock_call.assert_called_with(
807 ['apt-get', '--assume-yes',
808 '--foo', '--bar', 'install', 'foo', 'bar'],
809 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
810
811 @patch.object(osplatform, 'get_platform')
812 @patch('subprocess.call')
813 @patch('charmhelpers.fetch.ubuntu.log')
814 def test_installs_apt_packages_without_options(self, log, mock_call,
815 platform):
816 platform.return_value = 'ubuntu'
817 imp.reload(fetch)
818
819 packages = ['foo', 'bar']
820
821 fetch.apt_install(packages)
822
823 mock_call.assert_called_with(
824 ['apt-get', '--assume-yes',
825 '--option=Dpkg::Options::=--force-confold',
826 'install', 'foo', 'bar'],
827 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
828
829 @patch.object(osplatform, 'get_platform')
830 @patch('subprocess.call')
831 @patch('charmhelpers.fetch.ubuntu.log')
832 def test_installs_apt_packages_as_string(self, log, mock_call, platform):
833 platform.return_value = 'ubuntu'
834 imp.reload(fetch)
835
836 packages = 'foo bar'
837 options = ['--foo', '--bar']
838
839 fetch.apt_install(packages, options)
840
841 mock_call.assert_called_with(
842 ['apt-get', '--assume-yes',
843 '--foo', '--bar', 'install', 'foo bar'],
844 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
845
846 @patch.object(osplatform, 'get_platform')
847 @patch('subprocess.check_call')
848 @patch('charmhelpers.fetch.ubuntu.log')
849 def test_installs_apt_packages_with_possible_errors(self, log,
850 check_call, platform):
851 platform.return_value = 'ubuntu'
852 imp.reload(fetch)
853
854 packages = ['foo', 'bar']
855 options = ['--foo', '--bar']
856
857 fetch.apt_install(packages, options, fatal=True)
858
859 check_call.assert_called_with(
860 ['apt-get', '--assume-yes',
861 '--foo', '--bar', 'install', 'foo', 'bar'],
862 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
863
864 @patch.object(osplatform, 'get_platform')
865 @patch('subprocess.check_call')
866 @patch('charmhelpers.fetch.ubuntu.log')
867 def test_purges_apt_packages_as_string_fatal(self, log, mock_call,
868 platform):
869 platform.return_value = 'ubuntu'
870 imp.reload(fetch)
871
872 packages = 'irrelevant names'
873 mock_call.side_effect = OSError('fail')
874
875 self.assertRaises(OSError, fetch.apt_purge, packages, fatal=True)
876 self.assertTrue(log.called)
877
878 @patch.object(osplatform, 'get_platform')
879 @patch('subprocess.check_call')
880 @patch('charmhelpers.fetch.ubuntu.log')
881 def test_purges_apt_packages_fatal(self, log, mock_call, platform):
882 platform.return_value = 'ubuntu'
883 imp.reload(fetch)
884
885 packages = ['irrelevant', 'names']
886 mock_call.side_effect = OSError('fail')
887
888 self.assertRaises(OSError, fetch.apt_purge, packages, fatal=True)
889 self.assertTrue(log.called)
890
891 @patch.object(osplatform, 'get_platform')
892 @patch('subprocess.call')
893 @patch('charmhelpers.fetch.ubuntu.log')
894 def test_purges_apt_packages_as_string_nofatal(self, log, mock_call,
895 platform):
896 platform.return_value = 'ubuntu'
897 imp.reload(fetch)
898
899 packages = 'foo bar'
900
901 fetch.apt_purge(packages)
902
903 self.assertTrue(log.called)
904 mock_call.assert_called_with(
905 ['apt-get', '--assume-yes', 'purge', 'foo bar'],
906 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
907
908 @patch.object(osplatform, 'get_platform')
909 @patch('subprocess.call')
910 @patch('charmhelpers.fetch.ubuntu.log')
911 def test_purges_apt_packages_nofatal(self, log, mock_call, platform):
912 platform.return_value = 'ubuntu'
913 imp.reload(fetch)
914
915 packages = ['foo', 'bar']
916
917 fetch.apt_purge(packages)
918
919 self.assertTrue(log.called)
920 mock_call.assert_called_with(
921 ['apt-get', '--assume-yes', 'purge', 'foo', 'bar'],
922 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
923
924 @patch.object(osplatform, 'get_platform')
925 @patch('subprocess.check_call')
926 @patch('charmhelpers.fetch.ubuntu.log')
927 def test_mark_apt_packages_as_string_fatal(self, log, mock_call,
928 platform):
929 packages = 'irrelevant names'
930 mock_call.side_effect = OSError('fail')
931
932 self.assertRaises(OSError, fetch.apt_mark, packages, sentinel.mark,
933 fatal=True)
934 self.assertTrue(log.called)
935
936 @patch.object(osplatform, 'get_platform')
937 @patch('subprocess.check_call')
938 @patch('charmhelpers.fetch.ubuntu.log')
939 def test_mark_apt_packages_fatal(self, log, mock_call, platform):
940 platform.return_value = 'ubuntu'
941 imp.reload(fetch)
942
943 packages = ['irrelevant', 'names']
944 mock_call.side_effect = OSError('fail')
945
946 self.assertRaises(OSError, fetch.apt_mark, packages, sentinel.mark,
947 fatal=True)
948 self.assertTrue(log.called)
949
950 @patch.object(osplatform, 'get_platform')
951 @patch('subprocess.call')
952 @patch('charmhelpers.fetch.ubuntu.log')
953 def test_mark_apt_packages_as_string_nofatal(self, log, mock_call,
954 platform):
955 platform.return_value = 'ubuntu'
956 imp.reload(fetch)
957
958 packages = 'foo bar'
959
960 fetch.apt_mark(packages, sentinel.mark)
961
962 self.assertTrue(log.called)
963 mock_call.assert_called_with(
964 ['apt-mark', sentinel.mark, 'foo bar'],
965 universal_newlines=True)
966
967 @patch.object(osplatform, 'get_platform')
968 @patch('subprocess.call')
969 @patch('charmhelpers.fetch.ubuntu.log')
970 def test_mark_apt_packages_nofatal(self, log, mock_call,
971 platform):
972 platform.return_value = 'ubuntu'
973 imp.reload(fetch)
974
975 packages = ['foo', 'bar']
976
977 fetch.apt_mark(packages, sentinel.mark)
978
979 self.assertTrue(log.called)
980 mock_call.assert_called_with(
981 ['apt-mark', sentinel.mark, 'foo', 'bar'],
982 universal_newlines=True)
983
984 @patch.object(osplatform, 'get_platform')
985 @patch('subprocess.check_call')
986 @patch('charmhelpers.fetch.ubuntu.log')
987 def test_mark_apt_packages_nofatal_abortonfatal(self, log, mock_call,
988 platform):
989 platform.return_value = 'ubuntu'
990 imp.reload(fetch)
991
992 packages = ['foo', 'bar']
993
994 fetch.apt_mark(packages, sentinel.mark, fatal=True)
995
996 self.assertTrue(log.called)
997 mock_call.assert_called_with(
998 ['apt-mark', sentinel.mark, 'foo', 'bar'],
999 universal_newlines=True)
1000
1001 @patch.object(osplatform, 'get_platform')
1002 @patch('charmhelpers.fetch.ubuntu.apt_mark')
1003 def test_apt_hold(self, apt_mark, platform):
1004 platform.return_value = 'ubuntu'
1005 imp.reload(fetch)
1006
1007 fetch.apt_hold(sentinel.packages)
1008 apt_mark.assert_called_once_with(sentinel.packages, 'hold',
1009 fatal=False)
1010
1011 @patch.object(osplatform, 'get_platform')
1012 @patch('charmhelpers.fetch.ubuntu.apt_mark')
1013 def test_apt_hold_fatal(self, apt_mark, platform):
1014 platform.return_value = 'ubuntu'
1015 imp.reload(fetch)
1016
1017 fetch.apt_hold(sentinel.packages, fatal=sentinel.fatal)
1018 apt_mark.assert_called_once_with(sentinel.packages, 'hold',
1019 fatal=sentinel.fatal)
1020
1021 @patch.object(osplatform, 'get_platform')
1022 @patch('charmhelpers.fetch.ubuntu.apt_mark')
1023 def test_apt_unhold(self, apt_mark, platform):
1024 platform.return_value = 'ubuntu'
1025 imp.reload(fetch)
1026
1027 fetch.apt_unhold(sentinel.packages)
1028 apt_mark.assert_called_once_with(sentinel.packages, 'unhold',
1029 fatal=False)
1030
1031 @patch.object(osplatform, 'get_platform')
1032 @patch('charmhelpers.fetch.ubuntu.apt_mark')
1033 def test_apt_unhold_fatal(self, apt_mark, platform):
1034 platform.return_value = 'ubuntu'
1035 imp.reload(fetch)
1036
1037 fetch.apt_unhold(sentinel.packages, fatal=sentinel.fatal)
1038 apt_mark.assert_called_once_with(sentinel.packages, 'unhold',
1039 fatal=sentinel.fatal)
1040
1041 @patch.object(osplatform, 'get_platform')
1042 @patch('subprocess.check_call')
1043 def test_apt_update_fatal(self, check_call, platform):
1044 platform.return_value = 'ubuntu'
1045 imp.reload(fetch)
1046
1047 fetch.apt_update(fatal=True)
1048 check_call.assert_called_with(
1049 ['apt-get', 'update'],
1050 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
1051
1052 @patch.object(osplatform, 'get_platform')
1053 @patch('subprocess.call')
1054 def test_apt_update_nonfatal(self, call, platform):
1055 platform.return_value = 'ubuntu'
1056 imp.reload(fetch)
1057
1058 fetch.apt_update()
1059 call.assert_called_with(
1060 ['apt-get', 'update'],
1061 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
1062
1063 @patch.object(osplatform, 'get_platform')
1064 @patch('subprocess.check_call')
1065 @patch('time.sleep')
1066 def test_run_apt_command_retries_if_fatal(self, check_call,
1067 sleep, platform):
1068 """The _run_apt_command function retries the command if it can't get
1069 the APT lock."""
1070 platform.return_value = 'ubuntu'
1071 imp.reload(fetch)
1072
1073 self.called = False
1074
1075 def side_effect(*args, **kwargs):
1076 """
1077 First, raise an exception (can't acquire lock), then return 0
1078 (the lock is grabbed).
1079 """
1080 if not self.called:
1081 self.called = True
1082 raise subprocess.CalledProcessError(
1083 returncode=100, cmd="some command")
1084 else:
1085 return 0
1086
1087 check_call.side_effect = side_effect
1088 check_call.return_value = 0
1089
1090 from charmhelpers.fetch.ubuntu import _run_apt_command
1091 _run_apt_command(["some", "command"], fatal=True)
1092 self.assertTrue(sleep.called)
1093
1094
1095class YumTests(TestCase):
1096
1097 @patch.object(osplatform, 'get_platform')
1098 @patch('subprocess.call')
1099 @patch('charmhelpers.fetch.centos.log')
1100 def test_yum_upgrade_non_fatal(self, log, mock_call, platform):
1101 platform.return_value = 'centos'
1102 imp.reload(fetch)
1103
1104 options = ['--foo', '--bar']
1105 fetch.upgrade(options)
1106
1107 mock_call.assert_called_with(['yum', '--assumeyes',
1108 '--foo', '--bar', 'upgrade'],
1109 env=getenv())
1110
1111 @patch.object(osplatform, 'get_platform')
1112 @patch('subprocess.call')
1113 @patch('charmhelpers.fetch.centos.log')
1114 def test_yum_upgrade_fatal(self, log, mock_call, platform):
1115 platform.return_value = 'centos'
1116 imp.reload(fetch)
1117
1118 options = ['--foo', '--bar']
1119 fetch.upgrade(options, fatal=True)
1120
1121 mock_call.assert_called_with(['yum', '--assumeyes',
1122 '--foo', '--bar', 'upgrade'],
1123 env=getenv())
1124
1125 @patch.object(osplatform, 'get_platform')
1126 @patch('subprocess.call')
1127 @patch('charmhelpers.fetch.centos.log')
1128 def test_installs_yum_packages(self, log, mock_call, platform):
1129 platform.return_value = 'centos'
1130 imp.reload(fetch)
1131
1132 packages = ['foo', 'bar']
1133 options = ['--foo', '--bar']
1134
1135 fetch.install(packages, options)
1136
1137 mock_call.assert_called_with(['yum', '--assumeyes',
1138 '--foo', '--bar', 'install',
1139 'foo', 'bar'],
1140 env=getenv())
1141
1142 @patch.object(osplatform, 'get_platform')
1143 @patch('subprocess.call')
1144 @patch('charmhelpers.fetch.centos.log')
1145 def test_installs_yum_packages_without_options(self, log, mock_call,
1146 platform):
1147 platform.return_value = 'centos'
1148 imp.reload(fetch)
1149
1150 packages = ['foo', 'bar']
1151 fetch.install(packages)
1152
1153 mock_call.assert_called_with(['yum', '--assumeyes',
1154 'install', 'foo', 'bar'],
1155 env=getenv())
1156
1157 @patch.object(osplatform, 'get_platform')
1158 @patch('subprocess.call')
1159 @patch('charmhelpers.fetch.centos.log')
1160 def test_installs_yum_packages_as_string(self, log, mock_call,
1161 platform):
1162 platform.return_value = 'centos'
1163 imp.reload(fetch)
1164
1165 packages = 'foo bar'
1166 fetch.install(packages)
1167
1168 mock_call.assert_called_with(['yum', '--assumeyes',
1169 'install', 'foo bar'],
1170 env=getenv())
1171
1172 @patch.object(osplatform, 'get_platform')
1173 @patch('subprocess.call')
1174 @patch('charmhelpers.fetch.centos.log')
1175 def test_installs_yum_packages_with_possible_errors(self, log, mock_call,
1176 platform):
1177 platform.return_value = 'centos'
1178 imp.reload(fetch)
1179
1180 packages = ['foo', 'bar']
1181 options = ['--foo', '--bar']
1182
1183 fetch.install(packages, options, fatal=True)
1184
1185 mock_call.assert_called_with(['yum', '--assumeyes',
1186 '--foo', '--bar',
1187 'install', 'foo', 'bar'],
1188 env=getenv())
1189
1190 @patch.object(osplatform, 'get_platform')
1191 @patch('subprocess.check_call')
1192 @patch('charmhelpers.fetch.centos.log')
1193 def test_purges_yum_packages_as_string_fatal(self, log, mock_call,
1194 platform):
1195 platform.return_value = 'centos'
1196 imp.reload(fetch)
1197
1198 packages = 'irrelevant names'
1199 mock_call.side_effect = OSError('fail')
1200
1201 self.assertRaises(OSError, fetch.purge, packages, fatal=True)
1202 self.assertTrue(log.called)
1203
1204 @patch.object(osplatform, 'get_platform')
1205 @patch('subprocess.check_call')
1206 @patch('charmhelpers.fetch.centos.log')
1207 def test_purges_yum_packages_fatal(self, log, mock_call, platform):
1208 platform.return_value = 'centos'
1209 imp.reload(fetch)
1210
1211 packages = ['irrelevant', 'names']
1212 mock_call.side_effect = OSError('fail')
1213
1214 self.assertRaises(OSError, fetch.purge, packages, fatal=True)
1215 self.assertTrue(log.called)
1216
1217 @patch.object(osplatform, 'get_platform')
1218 @patch('subprocess.call')
1219 @patch('charmhelpers.fetch.centos.log')
1220 def test_purges_yum_packages_as_string_nofatal(self, log, mock_call,
1221 platform):
1222 platform.return_value = 'centos'
1223 imp.reload(fetch)
1224
1225 packages = 'foo bar'
1226 fetch.purge(packages)
1227
1228 self.assertTrue(log.called)
1229 mock_call.assert_called_with(['yum', '--assumeyes',
1230 'remove', 'foo bar'],
1231 env=getenv())
1232
1233 @patch.object(osplatform, 'get_platform')
1234 @patch('subprocess.call')
1235 @patch('charmhelpers.fetch.centos.log')
1236 def test_purges_yum_packages_nofatal(self, log, mock_call,
1237 platform):
1238 platform.return_value = 'centos'
1239 imp.reload(fetch)
1240
1241 packages = ['foo', 'bar']
1242 fetch.purge(packages)
1243
1244 self.assertTrue(log.called)
1245 mock_call.assert_called_with(['yum', '--assumeyes',
1246 'remove', 'foo', 'bar'],
1247 env=getenv())
1248
1249 @patch.object(osplatform, 'get_platform')
1250 @patch('subprocess.check_call')
1251 @patch('charmhelpers.fetch.centos.log')
1252 def test_yum_update_fatal(self, log, check_call, platform):
1253 platform.return_value = 'centos'
1254 imp.reload(fetch)
1255
1256 fetch.update(fatal=True)
1257 check_call.assert_called_with(['yum', '--assumeyes', 'update'],
1258 env=getenv())
1259 self.assertTrue(log.called)
1260
1261 @patch.object(osplatform, 'get_platform')
1262 @patch('subprocess.check_output')
1263 @patch('charmhelpers.fetch.centos.log')
1264 def test_yum_search(self, log, check_output, platform):
1265 platform.return_value = 'centos'
1266 imp.reload(fetch)
1267
1268 package = ['irrelevant']
1269
1270 from charmhelpers.fetch.centos import yum_search
1271 yum_search(package)
1272 check_output.assert_called_with(['yum', 'search', 'irrelevant'])
1273 self.assertTrue(log.called)
1274
1275 @patch.object(osplatform, 'get_platform')
1276 @patch('subprocess.check_call')
1277 @patch('time.sleep')
1278 def test_run_yum_command_retries_if_fatal(self, check_call,
1279 sleep, platform):
1280 """The _run_yum_command function retries the command if it can't get
1281 the YUM lock."""
1282 platform.return_value = 'centos'
1283 imp.reload(fetch)
1284
1285 self.called = False
1286
1287 def side_effect(*args, **kwargs):
1288 """
1289 First, raise an exception (can't acquire lock), then return 0
1290 (the lock is grabbed).
1291 """
1292 if not self.called:
1293 self.called = True
1294 raise subprocess.CalledProcessError(
1295 returncode=1, cmd="some command")
1296 else:
1297 return 0
1298
1299 check_call.side_effect = side_effect
1300 check_call.return_value = 0
1301 from charmhelpers.fetch.centos import _run_yum_command
1302 _run_yum_command(["some", "command"], fatal=True)
1303 self.assertTrue(sleep.called)
1304
1305 @patch.object(osplatform, 'get_platform')
1306 @patch('apt_pkg.Cache')
1307 def test_get_upstream_version(self, cache, platform):
1308 platform.return_value = 'ubuntu'
1309 imp.reload(fetch)
1310 cache.side_effect = fake_apt_cache
1311 self.assertEqual(fetch.get_upstream_version('vim'), '7.3.547')
1312 self.assertEqual(fetch.get_upstream_version('emacs'), None)
1313 self.assertEqual(fetch.get_upstream_version('unknown'), None)
1314270
=== added file 'tests/fetch/test_fetch_centos.py'
--- tests/fetch/test_fetch_centos.py 1970-01-01 00:00:00 +0000
+++ tests/fetch/test_fetch_centos.py 2017-04-10 18:12:16 +0000
@@ -0,0 +1,315 @@
1import subprocess
2import os
3
4from tests.helpers import patch_open
5from testtools import TestCase
6from mock import (
7 patch,
8 MagicMock,
9 call,
10)
11from charmhelpers.fetch import centos as fetch
12
13
14def getenv(update=None):
15 # return a copy of os.environ with update applied.
16 # this was necessary because some modules modify os.environment directly
17 copy = os.environ.copy()
18 if update is not None:
19 copy.update(update)
20 return copy
21
22
23class FetchTest(TestCase):
24
25 @patch("charmhelpers.fetch.log")
26 @patch('yum.YumBase.doPackageLists')
27 def test_filter_packages_missing_centos(self, yumBase, log):
28
29 class MockPackage:
30 def __init__(self, name):
31 self.base_package_name = name
32
33 yum_dict = {
34 'installed': {
35 MockPackage('vim')
36 },
37 'available': {
38 MockPackage('vim')
39 }
40 }
41 import yum
42 yum.YumBase.return_value.doPackageLists.return_value = yum_dict
43 result = fetch.filter_installed_packages(['vim', 'emacs'])
44 self.assertEquals(result, ['emacs'])
45
46 @patch("charmhelpers.fetch.log")
47 def test_filter_packages_none_missing_centos(self, log):
48
49 class MockPackage:
50 def __init__(self, name):
51 self.base_package_name = name
52
53 yum_dict = {
54 'installed': {
55 MockPackage('vim')
56 },
57 'available': {
58 MockPackage('vim')
59 }
60 }
61 import yum
62 yum.yumBase.return_value.doPackageLists.return_value = yum_dict
63 result = fetch.filter_installed_packages(['vim'])
64 self.assertEquals(result, [])
65
66 @patch('charmhelpers.fetch.centos.log')
67 @patch('yum.YumBase.doPackageLists')
68 def test_filter_packages_not_available_centos(self, yumBase, log):
69
70 class MockPackage:
71 def __init__(self, name):
72 self.base_package_name = name
73
74 yum_dict = {
75 'installed': {
76 MockPackage('vim')
77 }
78 }
79 import yum
80 yum.YumBase.return_value.doPackageLists.return_value = yum_dict
81
82 result = fetch.filter_installed_packages(['vim', 'joe'])
83 self.assertEquals(result, ['joe'])
84
85 @patch('charmhelpers.fetch.centos.log')
86 def test_add_source_none_centos(self, log):
87 fetch.add_source(source=None)
88 self.assertTrue(log.called)
89
90 @patch('charmhelpers.fetch.centos.log')
91 @patch('os.listdir')
92 def test_add_source_http_centos(self, listdir, log):
93 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
94 with patch_open() as (mock_open, mock_file):
95 fetch.add_source(source=source)
96 listdir.assert_called_with('/etc/yum.repos.d/')
97 mock_file.write.assert_has_calls([
98 call("[archive.ubuntu.com_ubuntu raring-backports main]\n"),
99 call("name=archive.ubuntu.com/ubuntu raring-backports main\n"),
100 call("baseurl=http://archive.ubuntu.com/ubuntu raring"
101 "-backports main\n\n")])
102
103 @patch('charmhelpers.fetch.centos.log')
104 @patch('os.listdir')
105 @patch('subprocess.check_call')
106 def test_add_source_http_and_key_id_centos(self, check_call,
107 listdir, log):
108 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
109 key_id = "akey"
110 with patch_open() as (mock_open, mock_file):
111 fetch.add_source(source=source, key=key_id)
112 listdir.assert_called_with('/etc/yum.repos.d/')
113 mock_file.write.assert_has_calls([
114 call("[archive.ubuntu.com_ubuntu raring-backports main]\n"),
115 call("name=archive.ubuntu.com/ubuntu raring-backports main\n"),
116 call("baseurl=http://archive.ubuntu.com/ubuntu raring"
117 "-backports main\n\n")])
118 check_call.assert_called_with(['rpm', '--import', key_id])
119
120 @patch('charmhelpers.fetch.centos.log')
121 @patch('os.listdir')
122 @patch('subprocess.check_call')
123 def test_add_source_https_and_key_id_centos(self, check_call,
124 listdir, log):
125 source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
126 key_id = "GPGPGP"
127 with patch_open() as (mock_open, mock_file):
128 fetch.add_source(source=source, key=key_id)
129 listdir.assert_called_with('/etc/yum.repos.d/')
130 mock_file.write.assert_has_calls([
131 call("[_USER:PASS@private-ppa.launchpad"
132 ".net_project_awesome]\n"),
133 call("name=/USER:PASS@private-ppa.launchpad.net"
134 "/project/awesome\n"),
135 call("baseurl=https://USER:PASS@private-ppa.launchpad.net"
136 "/project/awesome\n\n")])
137 check_call.assert_called_with(['rpm', '--import', key_id])
138
139 @patch('charmhelpers.fetch.centos.log')
140 @patch.object(fetch, 'NamedTemporaryFile')
141 @patch('os.listdir')
142 @patch('subprocess.check_call')
143 def test_add_source_http_and_key_centos(self, check_call,
144 listdir, temp_file, log):
145 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
146 key = '''
147 -----BEGIN PGP PUBLIC KEY BLOCK-----
148 [...]
149 -----END PGP PUBLIC KEY BLOCK-----
150 '''
151 file_mock = MagicMock()
152 file_mock.name = 'temporary_file'
153 temp_file.return_value.__enter__.return_value = file_mock
154 listdir.return_value = []
155
156 with patch_open() as (mock_open, mock_file):
157 fetch.add_source(source=source, key=key)
158 listdir.assert_called_with('/etc/yum.repos.d/')
159 self.assertTrue(log.called)
160 check_call.assert_called_with(['rpm', '--import', file_mock.name])
161 file_mock.write.assert_called_once_with(key)
162 file_mock.flush.assert_called_once_with()
163 file_mock.seek.assert_called_once_with(0)
164
165
166class YumTests(TestCase):
167
168 @patch('subprocess.call')
169 @patch('charmhelpers.fetch.centos.log')
170 def test_yum_upgrade_non_fatal(self, log, mock_call):
171 options = ['--foo', '--bar']
172 fetch.upgrade(options)
173
174 mock_call.assert_called_with(['yum', '--assumeyes',
175 '--foo', '--bar', 'upgrade'],
176 env=getenv())
177
178 @patch('subprocess.check_call')
179 @patch('charmhelpers.fetch.centos.log')
180 def test_yum_upgrade_fatal(self, log, mock_call):
181 options = ['--foo', '--bar']
182 fetch.upgrade(options, fatal=True)
183
184 mock_call.assert_called_with(['yum', '--assumeyes',
185 '--foo', '--bar', 'upgrade'],
186 env=getenv())
187
188 @patch('subprocess.call')
189 @patch('charmhelpers.fetch.centos.log')
190 def test_installs_yum_packages(self, log, mock_call):
191 packages = ['foo', 'bar']
192 options = ['--foo', '--bar']
193
194 fetch.install(packages, options)
195
196 mock_call.assert_called_with(['yum', '--assumeyes',
197 '--foo', '--bar', 'install',
198 'foo', 'bar'],
199 env=getenv())
200
201 @patch('subprocess.call')
202 @patch('charmhelpers.fetch.centos.log')
203 def test_installs_yum_packages_without_options(self, log, mock_call):
204 packages = ['foo', 'bar']
205 fetch.install(packages)
206
207 mock_call.assert_called_with(['yum', '--assumeyes',
208 'install', 'foo', 'bar'],
209 env=getenv())
210
211 @patch('subprocess.call')
212 @patch('charmhelpers.fetch.centos.log')
213 def test_installs_yum_packages_as_string(self, log, mock_call):
214 packages = 'foo bar'
215 fetch.install(packages)
216
217 mock_call.assert_called_with(['yum', '--assumeyes',
218 'install', 'foo bar'],
219 env=getenv())
220
221 @patch('subprocess.check_call')
222 @patch('charmhelpers.fetch.centos.log')
223 def test_installs_yum_packages_with_possible_errors(self, log, mock_call):
224 packages = ['foo', 'bar']
225 options = ['--foo', '--bar']
226
227 fetch.install(packages, options, fatal=True)
228
229 mock_call.assert_called_with(['yum', '--assumeyes',
230 '--foo', '--bar',
231 'install', 'foo', 'bar'],
232 env=getenv())
233
234 @patch('subprocess.check_call')
235 @patch('charmhelpers.fetch.centos.log')
236 def test_purges_yum_packages_as_string_fatal(self, log, mock_call):
237 packages = 'irrelevant names'
238 mock_call.side_effect = OSError('fail')
239
240 self.assertRaises(OSError, fetch.purge, packages, fatal=True)
241 self.assertTrue(log.called)
242
243 @patch('subprocess.check_call')
244 @patch('charmhelpers.fetch.centos.log')
245 def test_purges_yum_packages_fatal(self, log, mock_call):
246 packages = ['irrelevant', 'names']
247 mock_call.side_effect = OSError('fail')
248
249 self.assertRaises(OSError, fetch.purge, packages, fatal=True)
250 self.assertTrue(log.called)
251
252 @patch('subprocess.call')
253 @patch('charmhelpers.fetch.centos.log')
254 def test_purges_yum_packages_as_string_nofatal(self, log, mock_call):
255 packages = 'foo bar'
256 fetch.purge(packages)
257
258 self.assertTrue(log.called)
259 mock_call.assert_called_with(['yum', '--assumeyes',
260 'remove', 'foo bar'],
261 env=getenv())
262
263 @patch('subprocess.call')
264 @patch('charmhelpers.fetch.centos.log')
265 def test_purges_yum_packages_nofatal(self, log, mock_call):
266 packages = ['foo', 'bar']
267 fetch.purge(packages)
268
269 self.assertTrue(log.called)
270 mock_call.assert_called_with(['yum', '--assumeyes',
271 'remove', 'foo', 'bar'],
272 env=getenv())
273
274 @patch('subprocess.check_call')
275 @patch('charmhelpers.fetch.centos.log')
276 def test_yum_update_fatal(self, log, check_call):
277 fetch.update(fatal=True)
278 check_call.assert_called_with(['yum', '--assumeyes', 'update'],
279 env=getenv())
280 self.assertTrue(log.called)
281
282 @patch('subprocess.check_output')
283 @patch('charmhelpers.fetch.centos.log')
284 def test_yum_search(self, log, check_output):
285 package = ['irrelevant']
286
287 from charmhelpers.fetch.centos import yum_search
288 yum_search(package)
289 check_output.assert_called_with(['yum', 'search', 'irrelevant'])
290 self.assertTrue(log.called)
291
292 @patch('subprocess.check_call')
293 @patch('time.sleep')
294 def test_run_yum_command_retries_if_fatal(self, check_call, sleep):
295 """The _run_yum_command function retries the command if it can't get
296 the YUM lock."""
297 self.called = False
298
299 def side_effect(*args, **kwargs):
300 """
301 First, raise an exception (can't acquire lock), then return 0
302 (the lock is grabbed).
303 """
304 if not self.called:
305 self.called = True
306 raise subprocess.CalledProcessError(
307 returncode=1, cmd="some command")
308 else:
309 return 0
310
311 check_call.side_effect = side_effect
312 check_call.return_value = 0
313 from charmhelpers.fetch.centos import _run_yum_command
314 _run_yum_command(["some", "command"], fatal=True)
315 self.assertTrue(sleep.called)
0316
=== added file 'tests/fetch/test_fetch_ubuntu.py'
--- tests/fetch/test_fetch_ubuntu.py 1970-01-01 00:00:00 +0000
+++ tests/fetch/test_fetch_ubuntu.py 2017-04-10 18:12:16 +0000
@@ -0,0 +1,712 @@
1import six
2import subprocess
3import io
4import os
5import tempfile
6
7from tests.helpers import patch_open
8from testtools import TestCase
9from mock import (
10 patch,
11 MagicMock,
12 call,
13 sentinel,
14 ANY,
15)
16from charmhelpers.fetch import ubuntu as fetch
17
18if six.PY3:
19 builtin_open = 'builtins.open'
20else:
21 builtin_open = '__builtin__.open'
22
23# mocked return of openstack.lsb_release()
24FAKE_RELEASE = {
25 'DISTRIB_CODENAME': 'precise',
26 'DISTRIB_RELEASE': '12.04',
27 'DISTRIB_ID': 'Ubuntu',
28 'DISTRIB_DESCRIPTION': '"Ubuntu 12.04"'
29}
30
31url = 'deb ' + fetch.CLOUD_ARCHIVE_URL
32UCA_SOURCES = [
33 ('cloud:precise-folsom/proposed', url + ' precise-proposed/folsom main'),
34 ('cloud:precise-folsom', url + ' precise-updates/folsom main'),
35 ('cloud:precise-folsom/updates', url + ' precise-updates/folsom main'),
36 ('cloud:precise-grizzly/proposed', url + ' precise-proposed/grizzly main'),
37 ('cloud:precise-grizzly', url + ' precise-updates/grizzly main'),
38 ('cloud:precise-grizzly/updates', url + ' precise-updates/grizzly main'),
39 ('cloud:precise-havana/proposed', url + ' precise-proposed/havana main'),
40 ('cloud:precise-havana', url + ' precise-updates/havana main'),
41 ('cloud:precise-havana/updates', url + ' precise-updates/havana main'),
42 ('cloud:precise-icehouse/proposed',
43 url + ' precise-proposed/icehouse main'),
44 ('cloud:precise-icehouse', url + ' precise-updates/icehouse main'),
45 ('cloud:precise-icehouse/updates', url + ' precise-updates/icehouse main'),
46]
47
48PGP_KEY_ASCII_ARMOR = """-----BEGIN PGP PUBLIC KEY BLOCK-----
49Version: SKS 1.1.5
50Comment: Hostname: keyserver.ubuntu.com
51
52mI0EUCEyTAEEAMuUxyfiegCCwn4J/c0nw5PUTSJdn5FqiUTq6iMfij65xf1vl0g/Mxqw0gfg
53AJIsCDvO9N9dloLAwF6FUBMg5My7WyhRPTAKF505TKJboyX3Pp4J1fU1LV8QFVOp87vUh1Rz
54B6GU7cSglhnbL85gmbJTllkzkb3h4Yw7W+edjcQ/ABEBAAG0K0xhdW5jaHBhZCBQUEEgZm9y
55IFVidW50dSBDbG91ZCBBcmNoaXZlIFRlYW2IuAQTAQIAIgUCUCEyTAIbAwYLCQgHAwIGFQgC
56CQoLBBYCAwECHgECF4AACgkQimhEop9oEE7kJAP/eTBgq3Mhbvo0d8elMOuqZx3nmU7gSyPh
57ep0zYIRZ5TJWl/7PRtvp0CJA6N6ZywYTQ/4ANHhpibcHZkh8K0AzUvsGXnJRSFoJeqyDbD91
58EhoO+4ZfHs2HvRBQEDZILMa2OyuB497E5Mmyua3HDEOrG2cVLllsUZzpTFCx8NgeMHk=
59=jLBm
60-----END PGP PUBLIC KEY BLOCK-----
61"""
62
63FAKE_APT_CACHE = {
64 # an installed package
65 'vim': {
66 'current_ver': '2:7.3.547-6ubuntu5'
67 },
68 # a uninstalled installation candidate
69 'emacs': {
70 }
71}
72
73
74def fake_apt_cache(in_memory=True, progress=None):
75 def _get(package):
76 pkg = MagicMock()
77 if package not in FAKE_APT_CACHE:
78 raise KeyError
79 pkg.name = package
80 if 'current_ver' in FAKE_APT_CACHE[package]:
81 pkg.current_ver.ver_str = FAKE_APT_CACHE[package]['current_ver']
82 else:
83 pkg.current_ver = None
84 return pkg
85 cache = MagicMock()
86 cache.__getitem__.side_effect = _get
87 return cache
88
89
90def getenv(update=None):
91 # return a copy of os.environ with update applied.
92 # this was necessary because some modules modify os.environment directly
93 copy = os.environ.copy()
94 if update is not None:
95 copy.update(update)
96 return copy
97
98
99class FetchTest(TestCase):
100
101 @patch("charmhelpers.fetch.ubuntu.log")
102 @patch('apt_pkg.Cache')
103 def test_filter_packages_missing_ubuntu(self, cache, log):
104 cache.side_effect = fake_apt_cache
105 result = fetch.filter_installed_packages(['vim', 'emacs'])
106 self.assertEquals(result, ['emacs'])
107
108 @patch("charmhelpers.fetch.ubuntu.log")
109 @patch('apt_pkg.Cache')
110 def test_filter_packages_none_missing_ubuntu(self, cache, log):
111 cache.side_effect = fake_apt_cache
112 result = fetch.filter_installed_packages(['vim'])
113 self.assertEquals(result, [])
114
115 @patch('charmhelpers.fetch.ubuntu.log')
116 @patch('apt_pkg.Cache')
117 def test_filter_packages_not_available_ubuntu(self, cache, log):
118 cache.side_effect = fake_apt_cache
119 result = fetch.filter_installed_packages(['vim', 'joe'])
120 self.assertEquals(result, ['joe'])
121 log.assert_called_with('Package joe has no installation candidate.',
122 level='WARNING')
123
124 @patch.object(fetch, 'log', lambda *args, **kwargs: None)
125 def test_import_apt_key_radix(self):
126 """Ensure shell out apt-key during key import"""
127 with patch('subprocess.check_call') as _subp:
128 fetch.import_key('foo')
129 cmd = ['apt-key', 'adv', '--keyserver',
130 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']
131 _subp.assert_called_with(cmd)
132
133 @patch.object(fetch, 'log', lambda *args, **kwargs: None)
134 def test_import_apt_key_ascii_armor(self):
135 with tempfile.NamedTemporaryFile() as tmp:
136 with patch.object(fetch, 'NamedTemporaryFile') as mock_tmpfile:
137 tmpfile = mock_tmpfile.return_value
138 tmpfile.__enter__.return_value = tmpfile
139 tmpfile.name = tmp.name
140 with patch('subprocess.check_call') as _subp:
141 fetch.import_key(PGP_KEY_ASCII_ARMOR)
142 cmd = ['apt-key', 'add', tmp.name]
143 _subp.assert_called_with(cmd)
144 with open(tmp.name, 'r') as f:
145 self.assertEqual(PGP_KEY_ASCII_ARMOR, f.read())
146
147 @patch.object(fetch, 'log', lambda *args, **kwargs: None)
148 def test_import_bad_apt_key(self):
149 """Ensure error when importing apt key fails"""
150 with patch('subprocess.check_call') as _subp:
151 cmd = ['apt-key', 'adv', '--keyserver',
152 'hkp://keyserver.ubuntu.com:80', '--recv-keys', 'foo']
153 _subp.side_effect = subprocess.CalledProcessError(1, cmd, '')
154 try:
155 fetch.import_key('foo')
156 assert False
157 except fetch.GPGKeyError as e:
158 self.assertEqual(str(e), "Error importing PGP key 'foo'")
159
160 @patch('charmhelpers.fetch.ubuntu.log')
161 def test_add_source_none_ubuntu(self, log):
162 fetch.add_source(source=None)
163 self.assertTrue(log.called)
164
165 @patch('subprocess.check_call')
166 def test_add_source_ppa(self, check_call):
167 source = "ppa:test-ppa"
168 fetch.add_source(source=source)
169 check_call.assert_called_with(
170 ['add-apt-repository', '--yes', source])
171
172 @patch("charmhelpers.fetch.ubuntu.log")
173 @patch('subprocess.check_call')
174 @patch('time.sleep')
175 def test_add_source_ppa_retries_30_times(self, sleep, check_call, log):
176 self.call_count = 0
177
178 def side_effect(*args, **kwargs):
179 """Raise an 3 times, then return 0 """
180 self.call_count += 1
181 if self.call_count <= fetch.CMD_RETRY_COUNT:
182 raise subprocess.CalledProcessError(
183 returncode=1, cmd="some add-apt-repository command")
184 else:
185 return 0
186 check_call.side_effect = side_effect
187
188 source = "ppa:test-ppa"
189 fetch.add_source(source=source)
190 check_call.assert_called_with(
191 ['add-apt-repository', '--yes', source])
192 sleep.assert_called_with(10)
193 self.assertTrue(fetch.CMD_RETRY_COUNT, sleep.call_count)
194
195 @patch('charmhelpers.fetch.ubuntu.log')
196 @patch('subprocess.check_call')
197 def test_add_source_http_ubuntu(self, check_call, log):
198 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
199 fetch.add_source(source=source)
200 check_call.assert_called_with(
201 ['add-apt-repository', '--yes', source])
202
203 @patch('charmhelpers.fetch.ubuntu.log')
204 @patch('subprocess.check_call')
205 def test_add_source_https(self, check_call, log):
206 source = "https://example.com"
207 fetch.add_source(source=source)
208 check_call.assert_called_with(
209 ['add-apt-repository', '--yes', source])
210
211 @patch('charmhelpers.fetch.ubuntu.log')
212 @patch('subprocess.check_call')
213 def test_add_source_deb(self, check_call, log):
214 """add-apt-repository behaves differently when using the deb prefix.
215
216 $ add-apt-repository --yes \
217 "http://special.example.com/ubuntu precise-special main"
218 $ grep special /etc/apt/sources.list
219 deb http://special.example.com/ubuntu precise precise-special main
220 deb-src http://special.example.com/ubuntu precise precise-special main
221
222 $ add-apt-repository --yes \
223 "deb http://special.example.com/ubuntu precise-special main"
224 $ grep special /etc/apt/sources.list
225 deb http://special.example.com/ubuntu precise precise-special main
226 deb-src http://special.example.com/ubuntu precise precise-special main
227 deb http://special.example.com/ubuntu precise-special main
228 deb-src http://special.example.com/ubuntu precise-special main
229 """
230 source = "deb http://archive.ubuntu.com/ubuntu raring-backports main"
231 fetch.add_source(source=source)
232 check_call.assert_called_with(
233 ['add-apt-repository', '--yes', source])
234
235 @patch('charmhelpers.fetch.ubuntu.log')
236 @patch('subprocess.check_call')
237 def test_add_source_http_and_key_id(self, check_call, log):
238 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
239 key_id = "akey"
240 check_call.return_value = 0 # Successful exit code
241 fetch.add_source(source=source, key=key_id)
242 check_call.assert_has_calls([
243 call(['add-apt-repository', '--yes', source]),
244 call(['apt-key', 'adv', '--keyserver',
245 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key_id])
246 ])
247
248 @patch('charmhelpers.fetch.ubuntu.log')
249 @patch('subprocess.check_call')
250 def test_add_source_https_and_key_id(self, check_call, log):
251 source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
252 key_id = "GPGPGP"
253 check_call.return_value = 0 # Success from both calls
254 fetch.add_source(source=source, key=key_id)
255 check_call.assert_has_calls([
256 call(['add-apt-repository', '--yes', source]),
257 call(['apt-key', 'adv', '--keyserver',
258 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key_id])
259 ])
260
261 @patch('charmhelpers.fetch.ubuntu.log')
262 @patch('subprocess.check_call')
263 def test_add_source_http_and_key(self, check_call, log):
264 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
265 key = '''
266 -----BEGIN PGP PUBLIC KEY BLOCK-----
267 [...]
268 -----END PGP PUBLIC KEY BLOCK-----
269 '''
270 with patch('subprocess.check_call') as check_call:
271 check_call.return_value = 0
272 fetch.add_source(source=source, key=key)
273 check_call.assert_any_call(['add-apt-repository', '--yes', source])
274 check_call.assert_any_call(['apt-key', 'add', ANY])
275
276 def test_add_source_cloud_invalid_pocket(self):
277 source = "cloud:havana-updates"
278 self.assertRaises(fetch.SourceConfigError,
279 fetch.add_source, source)
280
281 @patch('charmhelpers.fetch.ubuntu.log')
282 @patch.object(fetch, 'filter_installed_packages')
283 @patch.object(fetch, 'apt_install')
284 @patch.object(fetch, 'lsb_release')
285 def test_add_source_cloud_pocket_style(self, lsb_release, apt_install,
286 filter_pkg, log):
287 source = "cloud:precise-updates/havana"
288 lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
289 result = ('# Ubuntu Cloud Archive\n'
290 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
291 ' precise-updates/havana main\n')
292
293 with patch_open() as (mock_open, mock_file):
294 fetch.add_source(source=source)
295 mock_file.write.assert_called_with(result)
296 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
297
298 @patch('charmhelpers.fetch.ubuntu.log')
299 @patch.object(fetch, 'filter_installed_packages')
300 @patch.object(fetch, 'apt_install')
301 @patch.object(fetch, 'lsb_release')
302 def test_add_source_cloud_os_style(self, lsb_release, apt_install,
303 filter_pkg, log):
304 source = "cloud:precise-havana"
305 lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
306 result = ('# Ubuntu Cloud Archive\n'
307 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
308 ' precise-updates/havana main\n')
309 with patch_open() as (mock_open, mock_file):
310 fetch.add_source(source=source)
311 mock_file.write.assert_called_with(result)
312 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
313
314 @patch('charmhelpers.fetch.ubuntu.log')
315 @patch.object(fetch, 'filter_installed_packages')
316 @patch.object(fetch, 'apt_install')
317 def test_add_source_cloud_distroless_style(self, apt_install,
318 filter_pkg, log):
319 source = "cloud:havana"
320 result = ('# Ubuntu Cloud Archive\n'
321 'deb http://ubuntu-cloud.archive.canonical.com/ubuntu'
322 ' precise-updates/havana main\n')
323 with patch_open() as (mock_open, mock_file):
324 fetch.add_source(source=source)
325 mock_file.write.assert_called_with(result)
326 filter_pkg.assert_called_with(['ubuntu-cloud-keyring'])
327
328 @patch('charmhelpers.fetch.ubuntu.log')
329 @patch.object(fetch, 'lsb_release')
330 @patch('platform.machine')
331 def test_add_source_proposed_x86_64(self, _machine, lsb_release, log):
332 source = "proposed"
333 result = ('# Proposed\n'
334 'deb http://archive.ubuntu.com/ubuntu precise-proposed'
335 ' main universe multiverse restricted\n')
336 lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
337 _machine.return_value = 'x86_64'
338 with patch_open() as (mock_open, mock_file):
339 fetch.add_source(source=source)
340 mock_file.write.assert_called_with(result)
341
342 @patch('charmhelpers.fetch.ubuntu.log')
343 @patch.object(fetch, 'lsb_release')
344 @patch('platform.machine')
345 def test_add_source_proposed_ppc64le(self, _machine, lsb_release, log):
346 source = "proposed"
347 result = (
348 "# Proposed\n"
349 "deb http://ports.ubuntu.com/ubuntu-ports precise-proposed main "
350 "universe multiverse restricted\n")
351 lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
352 _machine.return_value = 'ppc64le'
353 with patch_open() as (mock_open, mock_file):
354 fetch.add_source(source=source)
355 mock_file.write.assert_called_with(result)
356
357 @patch('charmhelpers.fetch.ubuntu.log')
358 @patch('subprocess.check_call')
359 def test_add_source_http_and_key_id_ubuntu(self, check_call, log):
360 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
361 key_id = "akey"
362 fetch.add_source(source=source, key=key_id)
363 check_call.assert_any_call(['add-apt-repository', '--yes', source]),
364 check_call.assert_any_call([
365 'apt-key', 'adv', '--keyserver',
366 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key_id])
367
368 @patch('charmhelpers.fetch.ubuntu.log')
369 @patch('subprocess.check_call')
370 def test_add_source_https_and_key_id_ubuntu(self, check_call, log):
371 source = "https://USER:PASS@private-ppa.launchpad.net/project/awesome"
372 key_id = "GPGPGP"
373 fetch.add_source(source=source, key=key_id)
374 check_call.assert_any_call(['add-apt-repository', '--yes', source]),
375 check_call.assert_any_call([
376 'apt-key', 'adv', '--keyserver',
377 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key_id])
378
379 @patch('charmhelpers.fetch.ubuntu.log')
380 @patch('subprocess.check_call')
381 def test_add_source_http_and_key_ubuntu(self, check_call, log):
382 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
383 key = '''
384 -----BEGIN PGP PUBLIC KEY BLOCK-----
385 [...]
386 -----END PGP PUBLIC KEY BLOCK-----
387 '''
388 fetch.add_source(source=source, key=key)
389 check_call.assert_any_call(['add-apt-repository', '--yes', source])
390 check_call.assert_any_call(['apt-key', 'add', ANY])
391
392 @patch('charmhelpers.fetch.ubuntu.log')
393 def test_configure_bad_install_source(self, log):
394 try:
395 fetch.add_source('foo', fail_invalid=True)
396 self.fail("Calling add_source('foo') should fail")
397 except fetch.SourceConfigError as e:
398 self.assertEqual(str(e), "Unknown source: 'foo'")
399
400 @patch('charmhelpers.fetch.ubuntu.lsb_release')
401 def test_configure_install_source_uca_staging(self, _lsb):
402 """Test configuring installation source from UCA staging sources"""
403 _lsb.return_value = FAKE_RELEASE
404 # staging pockets are configured as PPAs
405 with patch('subprocess.check_call') as _subp:
406 src = 'cloud:precise-folsom/staging'
407 fetch.add_source(src)
408 cmd = ['add-apt-repository', '-y',
409 'ppa:ubuntu-cloud-archive/folsom-staging']
410 _subp.assert_called_with(cmd)
411
412 @patch(builtin_open)
413 @patch('charmhelpers.fetch.ubuntu.apt_install')
414 @patch('charmhelpers.fetch.ubuntu.lsb_release')
415 @patch('charmhelpers.fetch.ubuntu.filter_installed_packages')
416 def test_configure_install_source_uca_repos(
417 self, _fip, _lsb, _install, _open):
418 """Test configuring installation source from UCA sources"""
419 _lsb.return_value = FAKE_RELEASE
420 _file = MagicMock(spec=io.FileIO)
421 _open.return_value = _file
422 _fip.side_effect = lambda x: x
423 for src, url in UCA_SOURCES:
424 actual_url = "# Ubuntu Cloud Archive\n{}\n".format(url)
425 fetch.add_source(src)
426 _install.assert_called_with(['ubuntu-cloud-keyring'],
427 fatal=True)
428 _open.assert_called_with(
429 '/etc/apt/sources.list.d/cloud-archive.list',
430 'w'
431 )
432 _file.__enter__().write.assert_called_with(actual_url)
433
434 def test_configure_install_source_bad_uca(self):
435 """Test configuring installation source from bad UCA source"""
436 try:
437 fetch.add_source('cloud:foo-bar', fail_invalid=True)
438 self.fail("add_source('cloud:foo-bar') should fail")
439 except fetch.SourceConfigError as e:
440 _e = ('Invalid Cloud Archive release specified: foo-bar'
441 ' on this Ubuntuversion')
442 self.assertTrue(str(e).startswith(_e))
443
444 @patch('charmhelpers.fetch.ubuntu.log')
445 def test_add_unparsable_source(self, log_):
446 source = "propsed" # Minor typo
447 fetch.add_source(source=source)
448 self.assertEqual(1, log_.call_count)
449
450 @patch('charmhelpers.fetch.ubuntu.log')
451 def test_add_distro_source(self, log):
452 source = "distro"
453 # distro is a noop but test validate no exception is thrown
454 fetch.add_source(source=source)
455
456
457class AptTests(TestCase):
458
459 @patch('subprocess.call')
460 @patch('charmhelpers.fetch.ubuntu.log')
461 def test_apt_upgrade_non_fatal(self, log, mock_call):
462 options = ['--foo', '--bar']
463 fetch.apt_upgrade(options)
464
465 mock_call.assert_called_with(
466 ['apt-get', '--assume-yes',
467 '--foo', '--bar', 'upgrade'],
468 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
469
470 @patch('subprocess.check_call')
471 @patch('charmhelpers.fetch.ubuntu.log')
472 def test_apt_upgrade_fatal(self, log, mock_call):
473 options = ['--foo', '--bar']
474 fetch.apt_upgrade(options, fatal=True)
475
476 mock_call.assert_called_with(
477 ['apt-get', '--assume-yes',
478 '--foo', '--bar', 'upgrade'],
479 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
480
481 @patch('subprocess.check_call')
482 @patch('charmhelpers.fetch.ubuntu.log')
483 def test_apt_dist_upgrade_fatal(self, log, mock_call):
484 options = ['--foo', '--bar']
485 fetch.apt_upgrade(options, fatal=True, dist=True)
486
487 mock_call.assert_called_with(
488 ['apt-get', '--assume-yes',
489 '--foo', '--bar', 'dist-upgrade'],
490 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
491
492 @patch('subprocess.call')
493 @patch('charmhelpers.fetch.ubuntu.log')
494 def test_installs_apt_packages(self, log, mock_call):
495 packages = ['foo', 'bar']
496 options = ['--foo', '--bar']
497
498 fetch.apt_install(packages, options)
499
500 mock_call.assert_called_with(
501 ['apt-get', '--assume-yes',
502 '--foo', '--bar', 'install', 'foo', 'bar'],
503 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
504
505 @patch('subprocess.call')
506 @patch('charmhelpers.fetch.ubuntu.log')
507 def test_installs_apt_packages_without_options(self, log, mock_call):
508 packages = ['foo', 'bar']
509
510 fetch.apt_install(packages)
511
512 mock_call.assert_called_with(
513 ['apt-get', '--assume-yes',
514 '--option=Dpkg::Options::=--force-confold',
515 'install', 'foo', 'bar'],
516 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
517
518 @patch('subprocess.call')
519 @patch('charmhelpers.fetch.ubuntu.log')
520 def test_installs_apt_packages_as_string(self, log, mock_call):
521 packages = 'foo bar'
522 options = ['--foo', '--bar']
523
524 fetch.apt_install(packages, options)
525
526 mock_call.assert_called_with(
527 ['apt-get', '--assume-yes',
528 '--foo', '--bar', 'install', 'foo bar'],
529 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
530
531 @patch('subprocess.check_call')
532 @patch('charmhelpers.fetch.ubuntu.log')
533 def test_installs_apt_packages_with_possible_errors(self, log,
534 check_call):
535 packages = ['foo', 'bar']
536 options = ['--foo', '--bar']
537
538 fetch.apt_install(packages, options, fatal=True)
539
540 check_call.assert_called_with(
541 ['apt-get', '--assume-yes',
542 '--foo', '--bar', 'install', 'foo', 'bar'],
543 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
544
545 @patch('subprocess.check_call')
546 @patch('charmhelpers.fetch.ubuntu.log')
547 def test_purges_apt_packages_as_string_fatal(self, log, mock_call):
548 packages = 'irrelevant names'
549 mock_call.side_effect = OSError('fail')
550
551 self.assertRaises(OSError, fetch.apt_purge, packages, fatal=True)
552 self.assertTrue(log.called)
553
554 @patch('subprocess.check_call')
555 @patch('charmhelpers.fetch.ubuntu.log')
556 def test_purges_apt_packages_fatal(self, log, mock_call):
557 packages = ['irrelevant', 'names']
558 mock_call.side_effect = OSError('fail')
559
560 self.assertRaises(OSError, fetch.apt_purge, packages, fatal=True)
561 self.assertTrue(log.called)
562
563 @patch('subprocess.call')
564 @patch('charmhelpers.fetch.ubuntu.log')
565 def test_purges_apt_packages_as_string_nofatal(self, log, mock_call):
566 packages = 'foo bar'
567
568 fetch.apt_purge(packages)
569
570 self.assertTrue(log.called)
571 mock_call.assert_called_with(
572 ['apt-get', '--assume-yes', 'purge', 'foo bar'],
573 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
574
575 @patch('subprocess.call')
576 @patch('charmhelpers.fetch.ubuntu.log')
577 def test_purges_apt_packages_nofatal(self, log, mock_call):
578 packages = ['foo', 'bar']
579
580 fetch.apt_purge(packages)
581
582 self.assertTrue(log.called)
583 mock_call.assert_called_with(
584 ['apt-get', '--assume-yes', 'purge', 'foo', 'bar'],
585 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
586
587 @patch('subprocess.check_call')
588 @patch('charmhelpers.fetch.ubuntu.log')
589 def test_mark_apt_packages_as_string_fatal(self, log, mock_call):
590 packages = 'irrelevant names'
591 mock_call.side_effect = OSError('fail')
592
593 self.assertRaises(OSError, fetch.apt_mark, packages, sentinel.mark,
594 fatal=True)
595 self.assertTrue(log.called)
596
597 @patch('subprocess.check_call')
598 @patch('charmhelpers.fetch.ubuntu.log')
599 def test_mark_apt_packages_fatal(self, log, mock_call):
600 packages = ['irrelevant', 'names']
601 mock_call.side_effect = OSError('fail')
602
603 self.assertRaises(OSError, fetch.apt_mark, packages, sentinel.mark,
604 fatal=True)
605 self.assertTrue(log.called)
606
607 @patch('subprocess.call')
608 @patch('charmhelpers.fetch.ubuntu.log')
609 def test_mark_apt_packages_as_string_nofatal(self, log, mock_call):
610 packages = 'foo bar'
611
612 fetch.apt_mark(packages, sentinel.mark)
613
614 self.assertTrue(log.called)
615 mock_call.assert_called_with(
616 ['apt-mark', sentinel.mark, 'foo bar'],
617 universal_newlines=True)
618
619 @patch('subprocess.call')
620 @patch('charmhelpers.fetch.ubuntu.log')
621 def test_mark_apt_packages_nofatal(self, log, mock_call):
622 packages = ['foo', 'bar']
623
624 fetch.apt_mark(packages, sentinel.mark)
625
626 self.assertTrue(log.called)
627 mock_call.assert_called_with(
628 ['apt-mark', sentinel.mark, 'foo', 'bar'],
629 universal_newlines=True)
630
631 @patch('subprocess.check_call')
632 @patch('charmhelpers.fetch.ubuntu.log')
633 def test_mark_apt_packages_nofatal_abortonfatal(self, log, mock_call):
634 packages = ['foo', 'bar']
635
636 fetch.apt_mark(packages, sentinel.mark, fatal=True)
637
638 self.assertTrue(log.called)
639 mock_call.assert_called_with(
640 ['apt-mark', sentinel.mark, 'foo', 'bar'],
641 universal_newlines=True)
642
643 @patch('charmhelpers.fetch.ubuntu.apt_mark')
644 def test_apt_hold(self, apt_mark):
645 fetch.apt_hold(sentinel.packages)
646 apt_mark.assert_called_once_with(sentinel.packages, 'hold',
647 fatal=False)
648
649 @patch('charmhelpers.fetch.ubuntu.apt_mark')
650 def test_apt_hold_fatal(self, apt_mark):
651 fetch.apt_hold(sentinel.packages, fatal=sentinel.fatal)
652 apt_mark.assert_called_once_with(sentinel.packages, 'hold',
653 fatal=sentinel.fatal)
654
655 @patch('charmhelpers.fetch.ubuntu.apt_mark')
656 def test_apt_unhold(self, apt_mark):
657 fetch.apt_unhold(sentinel.packages)
658 apt_mark.assert_called_once_with(sentinel.packages, 'unhold',
659 fatal=False)
660
661 @patch('charmhelpers.fetch.ubuntu.apt_mark')
662 def test_apt_unhold_fatal(self, apt_mark):
663 fetch.apt_unhold(sentinel.packages, fatal=sentinel.fatal)
664 apt_mark.assert_called_once_with(sentinel.packages, 'unhold',
665 fatal=sentinel.fatal)
666
667 @patch('subprocess.check_call')
668 def test_apt_update_fatal(self, check_call):
669 fetch.apt_update(fatal=True)
670 check_call.assert_called_with(
671 ['apt-get', 'update'],
672 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
673
674 @patch('subprocess.call')
675 def test_apt_update_nonfatal(self, call):
676 fetch.apt_update()
677 call.assert_called_with(
678 ['apt-get', 'update'],
679 env=getenv({'DEBIAN_FRONTEND': 'noninteractive'}))
680
681 @patch('subprocess.check_call')
682 @patch('time.sleep')
683 def test_run_apt_command_retries_if_fatal(self, check_call, sleep):
684 """The _run_apt_command function retries the command if it can't get
685 the APT lock."""
686 self.called = False
687
688 def side_effect(*args, **kwargs):
689 """
690 First, raise an exception (can't acquire lock), then return 0
691 (the lock is grabbed).
692 """
693 if not self.called:
694 self.called = True
695 raise subprocess.CalledProcessError(
696 returncode=100, cmd="some command")
697 else:
698 return 0
699
700 check_call.side_effect = side_effect
701 check_call.return_value = 0
702
703 from charmhelpers.fetch.ubuntu import _run_apt_command
704 _run_apt_command(["some", "command"], fatal=True)
705 self.assertTrue(sleep.called)
706
707 @patch('apt_pkg.Cache')
708 def test_get_upstream_version(self, cache):
709 cache.side_effect = fake_apt_cache
710 self.assertEqual(fetch.get_upstream_version('vim'), '7.3.547')
711 self.assertEqual(fetch.get_upstream_version('emacs'), None)
712 self.assertEqual(fetch.get_upstream_version('unknown'), None)

Subscribers

People subscribed via source and target branches