~axino/charm-haproxy/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/fetch/ubuntu.py

  • Committer: mergebot at canonical
  • Author(s): "Barry Price"
  • Date: 2019-03-21 15:28:26 UTC
  • mfrom: (121.1.5 charm-haproxy)
  • Revision ID: mergebot@juju-139df4-prod-is-toolbox-0.canonical.com-20190321152826-kicit95vtrkrqnf0
charm-helpers sync

Reviewed-on: https://code.launchpad.net/~barryprice/charm-haproxy/charm-helpers-sync/+merge/364882
Reviewed-by: Nick Moffitt <nick.moffitt@canonical.com>

Show diffs side-by-side

added added

removed removed

Lines of Context:
12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
 
 
15
from collections import OrderedDict
15
16
import os
 
17
import platform
 
18
import re
16
19
import six
17
20
import time
18
21
import subprocess
19
22
 
20
 
from tempfile import NamedTemporaryFile
21
 
from charmhelpers.core.host import (
22
 
    lsb_release
 
23
from charmhelpers.core.host import get_distrib_codename
 
24
 
 
25
from charmhelpers.core.hookenv import (
 
26
    log,
 
27
    DEBUG,
 
28
    WARNING,
 
29
    env_proxy_settings,
23
30
)
24
 
from charmhelpers.core.hookenv import log
25
 
from charmhelpers.fetch import SourceConfigError
 
31
from charmhelpers.fetch import SourceConfigError, GPGKeyError
26
32
 
 
33
PROPOSED_POCKET = (
 
34
    "# Proposed\n"
 
35
    "deb http://archive.ubuntu.com/ubuntu {}-proposed main universe "
 
36
    "multiverse restricted\n")
 
37
PROPOSED_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.
 
42
ARCH_TO_PROPOSED_POCKET = {
 
43
    'x86_64': PROPOSED_POCKET,
 
44
    'ppc64le': PROPOSED_PORTS_POCKET,
 
45
    'aarch64': PROPOSED_PORTS_POCKET,
 
46
    's390x': PROPOSED_PORTS_POCKET,
 
47
}
 
48
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
 
49
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
27
50
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
28
51
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
29
52
"""
30
 
 
31
 
PROPOSED_POCKET = """# Proposed
32
 
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
33
 
"""
34
 
 
35
53
CLOUD_ARCHIVE_POCKETS = {
36
54
    # Folsom
37
55
    'folsom': 'precise-updates/folsom',
 
56
    'folsom/updates': 'precise-updates/folsom',
38
57
    'precise-folsom': 'precise-updates/folsom',
39
58
    'precise-folsom/updates': 'precise-updates/folsom',
40
59
    'precise-updates/folsom': 'precise-updates/folsom',
43
62
    'precise-proposed/folsom': 'precise-proposed/folsom',
44
63
    # Grizzly
45
64
    'grizzly': 'precise-updates/grizzly',
 
65
    'grizzly/updates': 'precise-updates/grizzly',
46
66
    'precise-grizzly': 'precise-updates/grizzly',
47
67
    'precise-grizzly/updates': 'precise-updates/grizzly',
48
68
    'precise-updates/grizzly': 'precise-updates/grizzly',
51
71
    'precise-proposed/grizzly': 'precise-proposed/grizzly',
52
72
    # Havana
53
73
    'havana': 'precise-updates/havana',
 
74
    'havana/updates': 'precise-updates/havana',
54
75
    'precise-havana': 'precise-updates/havana',
55
76
    'precise-havana/updates': 'precise-updates/havana',
56
77
    'precise-updates/havana': 'precise-updates/havana',
59
80
    'precise-proposed/havana': 'precise-proposed/havana',
60
81
    # Icehouse
61
82
    'icehouse': 'precise-updates/icehouse',
 
83
    'icehouse/updates': 'precise-updates/icehouse',
62
84
    'precise-icehouse': 'precise-updates/icehouse',
63
85
    'precise-icehouse/updates': 'precise-updates/icehouse',
64
86
    'precise-updates/icehouse': 'precise-updates/icehouse',
67
89
    'precise-proposed/icehouse': 'precise-proposed/icehouse',
68
90
    # Juno
69
91
    'juno': 'trusty-updates/juno',
 
92
    'juno/updates': 'trusty-updates/juno',
70
93
    'trusty-juno': 'trusty-updates/juno',
71
94
    'trusty-juno/updates': 'trusty-updates/juno',
72
95
    'trusty-updates/juno': 'trusty-updates/juno',
75
98
    'trusty-proposed/juno': 'trusty-proposed/juno',
76
99
    # Kilo
77
100
    'kilo': 'trusty-updates/kilo',
 
101
    'kilo/updates': 'trusty-updates/kilo',
78
102
    'trusty-kilo': 'trusty-updates/kilo',
79
103
    'trusty-kilo/updates': 'trusty-updates/kilo',
80
104
    'trusty-updates/kilo': 'trusty-updates/kilo',
83
107
    'trusty-proposed/kilo': 'trusty-proposed/kilo',
84
108
    # Liberty
85
109
    'liberty': 'trusty-updates/liberty',
 
110
    'liberty/updates': 'trusty-updates/liberty',
86
111
    'trusty-liberty': 'trusty-updates/liberty',
87
112
    'trusty-liberty/updates': 'trusty-updates/liberty',
88
113
    'trusty-updates/liberty': 'trusty-updates/liberty',
91
116
    'trusty-proposed/liberty': 'trusty-proposed/liberty',
92
117
    # Mitaka
93
118
    'mitaka': 'trusty-updates/mitaka',
 
119
    'mitaka/updates': 'trusty-updates/mitaka',
94
120
    'trusty-mitaka': 'trusty-updates/mitaka',
95
121
    'trusty-mitaka/updates': 'trusty-updates/mitaka',
96
122
    'trusty-updates/mitaka': 'trusty-updates/mitaka',
99
125
    'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
100
126
    # Newton
101
127
    'newton': 'xenial-updates/newton',
 
128
    'newton/updates': 'xenial-updates/newton',
102
129
    'xenial-newton': 'xenial-updates/newton',
103
130
    'xenial-newton/updates': 'xenial-updates/newton',
104
131
    'xenial-updates/newton': 'xenial-updates/newton',
107
134
    'xenial-proposed/newton': 'xenial-proposed/newton',
108
135
    # Ocata
109
136
    'ocata': 'xenial-updates/ocata',
 
137
    'ocata/updates': 'xenial-updates/ocata',
110
138
    'xenial-ocata': 'xenial-updates/ocata',
111
139
    'xenial-ocata/updates': 'xenial-updates/ocata',
112
140
    'xenial-updates/ocata': 'xenial-updates/ocata',
113
141
    'ocata/proposed': 'xenial-proposed/ocata',
114
142
    'xenial-ocata/proposed': 'xenial-proposed/ocata',
115
 
    'xenial-ocata/newton': 'xenial-proposed/ocata',
 
143
    'xenial-proposed/ocata': 'xenial-proposed/ocata',
 
144
    # Pike
 
145
    'pike': 'xenial-updates/pike',
 
146
    'xenial-pike': 'xenial-updates/pike',
 
147
    'xenial-pike/updates': 'xenial-updates/pike',
 
148
    'xenial-updates/pike': 'xenial-updates/pike',
 
149
    'pike/proposed': 'xenial-proposed/pike',
 
150
    'xenial-pike/proposed': 'xenial-proposed/pike',
 
151
    'xenial-proposed/pike': 'xenial-proposed/pike',
 
152
    # Queens
 
153
    'queens': 'xenial-updates/queens',
 
154
    'xenial-queens': 'xenial-updates/queens',
 
155
    'xenial-queens/updates': 'xenial-updates/queens',
 
156
    'xenial-updates/queens': 'xenial-updates/queens',
 
157
    'queens/proposed': 'xenial-proposed/queens',
 
158
    'xenial-queens/proposed': 'xenial-proposed/queens',
 
159
    'xenial-proposed/queens': 'xenial-proposed/queens',
 
160
    # Rocky
 
161
    'rocky': 'bionic-updates/rocky',
 
162
    'bionic-rocky': 'bionic-updates/rocky',
 
163
    'bionic-rocky/updates': 'bionic-updates/rocky',
 
164
    'bionic-updates/rocky': 'bionic-updates/rocky',
 
165
    'rocky/proposed': 'bionic-proposed/rocky',
 
166
    'bionic-rocky/proposed': 'bionic-proposed/rocky',
 
167
    'bionic-proposed/rocky': 'bionic-proposed/rocky',
 
168
    # Stein
 
169
    'stein': 'bionic-updates/stein',
 
170
    'bionic-stein': 'bionic-updates/stein',
 
171
    'bionic-stein/updates': 'bionic-updates/stein',
 
172
    'bionic-updates/stein': 'bionic-updates/stein',
 
173
    'stein/proposed': 'bionic-proposed/stein',
 
174
    'bionic-stein/proposed': 'bionic-proposed/stein',
 
175
    'bionic-proposed/stein': 'bionic-proposed/stein',
116
176
}
117
177
 
 
178
 
118
179
APT_NO_LOCK = 100  # The return code for "couldn't acquire lock" in APT.
119
180
CMD_RETRY_DELAY = 10  # Wait 10 seconds between command retries.
120
 
CMD_RETRY_COUNT = 30  # Retry a failing fatal command X times.
 
181
CMD_RETRY_COUNT = 3  # Retry a failing fatal command X times.
121
182
 
122
183
 
123
184
def filter_installed_packages(packages):
135
196
    return _pkgs
136
197
 
137
198
 
 
199
def filter_missing_packages(packages):
 
200
    """Return a list of packages that are installed.
 
201
 
 
202
    :param packages: list of packages to evaluate.
 
203
    :returns list: Packages that are installed.
 
204
    """
 
205
    return list(
 
206
        set(packages) -
 
207
        set(filter_installed_packages(packages))
 
208
    )
 
209
 
 
210
 
138
211
def apt_cache(in_memory=True, progress=None):
139
212
    """Build and return an apt cache."""
140
213
    from apt import apt_pkg
145
218
    return apt_pkg.Cache(progress)
146
219
 
147
220
 
148
 
def install(packages, options=None, fatal=False):
 
221
def apt_install(packages, options=None, fatal=False):
149
222
    """Install one or more packages."""
150
223
    if options is None:
151
224
        options = ['--option=Dpkg::Options::=--force-confold']
162
235
    _run_apt_command(cmd, fatal)
163
236
 
164
237
 
165
 
def upgrade(options=None, fatal=False, dist=False):
 
238
def apt_upgrade(options=None, fatal=False, dist=False):
166
239
    """Upgrade all packages."""
167
240
    if options is None:
168
241
        options = ['--option=Dpkg::Options::=--force-confold']
177
250
    _run_apt_command(cmd, fatal)
178
251
 
179
252
 
180
 
def update(fatal=False):
 
253
def apt_update(fatal=False):
181
254
    """Update local apt cache."""
182
255
    cmd = ['apt-get', 'update']
183
256
    _run_apt_command(cmd, fatal)
184
257
 
185
258
 
186
 
def purge(packages, fatal=False):
 
259
def apt_purge(packages, fatal=False):
187
260
    """Purge one or more packages."""
188
261
    cmd = ['apt-get', '--assume-yes', 'purge']
189
262
    if isinstance(packages, six.string_types):
194
267
    _run_apt_command(cmd, fatal)
195
268
 
196
269
 
 
270
def apt_autoremove(purge=True, fatal=False):
 
271
    """Purge one or more packages."""
 
272
    cmd = ['apt-get', '--assume-yes', 'autoremove']
 
273
    if purge:
 
274
        cmd.append('--purge')
 
275
    _run_apt_command(cmd, fatal)
 
276
 
 
277
 
197
278
def apt_mark(packages, mark, fatal=False):
198
279
    """Flag one or more packages using apt-mark."""
199
280
    log("Marking {} as {}".format(packages, mark))
217
298
    return apt_mark(packages, 'unhold', fatal=fatal)
218
299
 
219
300
 
220
 
def add_source(source, key=None):
 
301
def import_key(key):
 
302
    """Import an ASCII Armor key.
 
303
 
 
304
    A Radix64 format keyid is also supported for backwards
 
305
    compatibility. In this case Ubuntu keyserver will be
 
306
    queried for a key via HTTPS by its keyid. This method
 
307
    is less preferrable because https proxy servers may
 
308
    require traffic decryption which is equivalent to a
 
309
    man-in-the-middle attack (a proxy server impersonates
 
310
    keyserver TLS certificates and has to be explicitly
 
311
    trusted by the system).
 
312
 
 
313
    :param key: A GPG key in ASCII armor format,
 
314
                  including BEGIN and END markers or a keyid.
 
315
    :type key: (bytes, str)
 
316
    :raises: GPGKeyError if the key could not be imported
 
317
    """
 
318
    key = key.strip()
 
319
    if '-' in key or '\n' in key:
 
320
        # Send everything not obviously a keyid to GPG to import, as
 
321
        # we trust its validation better than our own. eg. handling
 
322
        # comments before the key.
 
323
        log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
 
324
        if ('-----BEGIN PGP PUBLIC KEY BLOCK-----' in key and
 
325
                '-----END PGP PUBLIC KEY BLOCK-----' in key):
 
326
            log("Writing provided PGP key in the binary format", level=DEBUG)
 
327
            if six.PY3:
 
328
                key_bytes = key.encode('utf-8')
 
329
            else:
 
330
                key_bytes = key
 
331
            key_name = _get_keyid_by_gpg_key(key_bytes)
 
332
            key_gpg = _dearmor_gpg_key(key_bytes)
 
333
            _write_apt_gpg_keyfile(key_name=key_name, key_material=key_gpg)
 
334
        else:
 
335
            raise GPGKeyError("ASCII armor markers missing from GPG key")
 
336
    else:
 
337
        log("PGP key found (looks like Radix64 format)", level=WARNING)
 
338
        log("SECURELY importing PGP key from keyserver; "
 
339
            "full key not provided.", level=WARNING)
 
340
        # as of bionic add-apt-repository uses curl with an HTTPS keyserver URL
 
341
        # to retrieve GPG keys. `apt-key adv` command is deprecated as is
 
342
        # apt-key in general as noted in its manpage. See lp:1433761 for more
 
343
        # history. Instead, /etc/apt/trusted.gpg.d is used directly to drop
 
344
        # gpg
 
345
        key_asc = _get_key_by_keyid(key)
 
346
        # write the key in GPG format so that apt-key list shows it
 
347
        key_gpg = _dearmor_gpg_key(key_asc)
 
348
        _write_apt_gpg_keyfile(key_name=key, key_material=key_gpg)
 
349
 
 
350
 
 
351
def _get_keyid_by_gpg_key(key_material):
 
352
    """Get a GPG key fingerprint by GPG key material.
 
353
    Gets a GPG key fingerprint (40-digit, 160-bit) by the ASCII armor-encoded
 
354
    or binary GPG key material. Can be used, for example, to generate file
 
355
    names for keys passed via charm options.
 
356
 
 
357
    :param key_material: ASCII armor-encoded or binary GPG key material
 
358
    :type key_material: bytes
 
359
    :raises: GPGKeyError if invalid key material has been provided
 
360
    :returns: A GPG key fingerprint
 
361
    :rtype: str
 
362
    """
 
363
    # Use the same gpg command for both Xenial and Bionic
 
364
    cmd = 'gpg --with-colons --with-fingerprint'
 
365
    ps = subprocess.Popen(cmd.split(),
 
366
                          stdout=subprocess.PIPE,
 
367
                          stderr=subprocess.PIPE,
 
368
                          stdin=subprocess.PIPE)
 
369
    out, err = ps.communicate(input=key_material)
 
370
    if six.PY3:
 
371
        out = out.decode('utf-8')
 
372
        err = err.decode('utf-8')
 
373
    if 'gpg: no valid OpenPGP data found.' in err:
 
374
        raise GPGKeyError('Invalid GPG key material provided')
 
375
    # from gnupg2 docs: fpr :: Fingerprint (fingerprint is in field 10)
 
376
    return re.search(r"^fpr:{9}([0-9A-F]{40}):$", out, re.MULTILINE).group(1)
 
377
 
 
378
 
 
379
def _get_key_by_keyid(keyid):
 
380
    """Get a key via HTTPS from the Ubuntu keyserver.
 
381
    Different key ID formats are supported by SKS keyservers (the longer ones
 
382
    are more secure, see "dead beef attack" and https://evil32.com/). Since
 
383
    HTTPS is used, if SSLBump-like HTTPS proxies are in place, they will
 
384
    impersonate keyserver.ubuntu.com and generate a certificate with
 
385
    keyserver.ubuntu.com in the CN field or in SubjAltName fields of a
 
386
    certificate. If such proxy behavior is expected it is necessary to add the
 
387
    CA certificate chain containing the intermediate CA of the SSLBump proxy to
 
388
    every machine that this code runs on via ca-certs cloud-init directive (via
 
389
    cloudinit-userdata model-config) or via other means (such as through a
 
390
    custom charm option). Also note that DNS resolution for the hostname in a
 
391
    URL is done at a proxy server - not at the client side.
 
392
 
 
393
    8-digit (32 bit) key ID
 
394
    https://keyserver.ubuntu.com/pks/lookup?search=0x4652B4E6
 
395
    16-digit (64 bit) key ID
 
396
    https://keyserver.ubuntu.com/pks/lookup?search=0x6E85A86E4652B4E6
 
397
    40-digit key ID:
 
398
    https://keyserver.ubuntu.com/pks/lookup?search=0x35F77D63B5CEC106C577ED856E85A86E4652B4E6
 
399
 
 
400
    :param keyid: An 8, 16 or 40 hex digit keyid to find a key for
 
401
    :type keyid: (bytes, str)
 
402
    :returns: A key material for the specified GPG key id
 
403
    :rtype: (str, bytes)
 
404
    :raises: subprocess.CalledProcessError
 
405
    """
 
406
    # options=mr - machine-readable output (disables html wrappers)
 
407
    keyserver_url = ('https://keyserver.ubuntu.com'
 
408
                     '/pks/lookup?op=get&options=mr&exact=on&search=0x{}')
 
409
    curl_cmd = ['curl', keyserver_url.format(keyid)]
 
410
    # use proxy server settings in order to retrieve the key
 
411
    return subprocess.check_output(curl_cmd,
 
412
                                   env=env_proxy_settings(['https']))
 
413
 
 
414
 
 
415
def _dearmor_gpg_key(key_asc):
 
416
    """Converts a GPG key in the ASCII armor format to the binary format.
 
417
 
 
418
    :param key_asc: A GPG key in ASCII armor format.
 
419
    :type key_asc: (str, bytes)
 
420
    :returns: A GPG key in binary format
 
421
    :rtype: (str, bytes)
 
422
    :raises: GPGKeyError
 
423
    """
 
424
    ps = subprocess.Popen(['gpg', '--dearmor'],
 
425
                          stdout=subprocess.PIPE,
 
426
                          stderr=subprocess.PIPE,
 
427
                          stdin=subprocess.PIPE)
 
428
    out, err = ps.communicate(input=key_asc)
 
429
    # no need to decode output as it is binary (invalid utf-8), only error
 
430
    if six.PY3:
 
431
        err = err.decode('utf-8')
 
432
    if 'gpg: no valid OpenPGP data found.' in err:
 
433
        raise GPGKeyError('Invalid GPG key material. Check your network setup'
 
434
                          ' (MTU, routing, DNS) and/or proxy server settings'
 
435
                          ' as well as destination keyserver status.')
 
436
    else:
 
437
        return out
 
438
 
 
439
 
 
440
def _write_apt_gpg_keyfile(key_name, key_material):
 
441
    """Writes GPG key material into a file at a provided path.
 
442
 
 
443
    :param key_name: A key name to use for a key file (could be a fingerprint)
 
444
    :type key_name: str
 
445
    :param key_material: A GPG key material (binary)
 
446
    :type key_material: (str, bytes)
 
447
    """
 
448
    with open('/etc/apt/trusted.gpg.d/{}.gpg'.format(key_name),
 
449
              'wb') as keyf:
 
450
        keyf.write(key_material)
 
451
 
 
452
 
 
453
def add_source(source, key=None, fail_invalid=False):
221
454
    """Add a package source to this system.
222
455
 
223
456
    @param source: a URL or sources.list entry, as supported by
233
466
        such as 'cloud:icehouse'
234
467
        'distro' may be used as a noop
235
468
 
 
469
    Full list of source specifications supported by the function are:
 
470
 
 
471
    'distro': A NOP; i.e. it has no effect.
 
472
    'proposed': the proposed deb spec [2] is wrtten to
 
473
      /etc/apt/sources.list/proposed
 
474
    'distro-proposed': adds <version>-proposed to the debs [2]
 
475
    'ppa:<ppa-name>': add-apt-repository --yes <ppa_name>
 
476
    'deb <deb-spec>': add-apt-repository --yes deb <deb-spec>
 
477
    'http://....': add-apt-repository --yes http://...
 
478
    'cloud-archive:<spec>': add-apt-repository -yes cloud-archive:<spec>
 
479
    'cloud:<release>[-staging]': specify a Cloud Archive pocket <release> with
 
480
      optional staging version.  If staging is used then the staging PPA [2]
 
481
      with be used.  If staging is NOT used then the cloud archive [3] will be
 
482
      added, and the 'ubuntu-cloud-keyring' package will be added for the
 
483
      current distro.
 
484
 
 
485
    Otherwise the source is not recognised and this is logged to the juju log.
 
486
    However, no error is raised, unless sys_error_on_exit is True.
 
487
 
 
488
    [1] deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
 
489
        where {} is replaced with the derived pocket name.
 
490
    [2] deb http://archive.ubuntu.com/ubuntu {}-proposed \
 
491
        main universe multiverse restricted
 
492
        where {} is replaced with the lsb_release codename (e.g. xenial)
 
493
    [3] deb http://ubuntu-cloud.archive.canonical.com/ubuntu <pocket>
 
494
        to /etc/apt/sources.list.d/cloud-archive-list
 
495
 
236
496
    @param key: A key to be added to the system's APT keyring and used
237
497
    to verify the signatures on packages. Ideally, this should be an
238
498
    ASCII format GPG public key including the block headers. A GPG key
240
500
    available to retrieve the actual public key from a public keyserver
241
501
    placing your Juju environment at risk. ppa and cloud archive keys
242
502
    are securely added automtically, so sould not be provided.
 
503
 
 
504
    @param fail_invalid: (boolean) if True, then the function raises a
 
505
    SourceConfigError is there is no matching installation source.
 
506
 
 
507
    @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a
 
508
    valid pocket in CLOUD_ARCHIVE_POCKETS
243
509
    """
 
510
    _mapping = OrderedDict([
 
511
        (r"^distro$", lambda: None),  # This is a NOP
 
512
        (r"^(?:proposed|distro-proposed)$", _add_proposed),
 
513
        (r"^cloud-archive:(.*)$", _add_apt_repository),
 
514
        (r"^((?:deb |http:|https:|ppa:).*)$", _add_apt_repository),
 
515
        (r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging),
 
516
        (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
 
517
        (r"^cloud:(.*)$", _add_cloud_pocket),
 
518
        (r"^snap:.*-(.*)-(.*)$", _add_cloud_distro_check),
 
519
    ])
244
520
    if source is None:
245
 
        log('Source is not present. Skipping')
246
 
        return
247
 
 
248
 
    if (source.startswith('ppa:') or
249
 
        source.startswith('http') or
250
 
        source.startswith('deb ') or
251
 
            source.startswith('cloud-archive:')):
252
 
        cmd = ['add-apt-repository', '--yes', source]
253
 
        _run_with_retries(cmd)
254
 
    elif source.startswith('cloud:'):
255
 
        install(filter_installed_packages(['ubuntu-cloud-keyring']),
 
521
        source = ''
 
522
    for r, fn in six.iteritems(_mapping):
 
523
        m = re.match(r, source)
 
524
        if m:
 
525
            # call the assoicated function with the captured groups
 
526
            # raises SourceConfigError on error.
 
527
            fn(*m.groups())
 
528
            if key:
 
529
                try:
 
530
                    import_key(key)
 
531
                except GPGKeyError as e:
 
532
                    raise SourceConfigError(str(e))
 
533
            break
 
534
    else:
 
535
        # nothing matched.  log an error and maybe sys.exit
 
536
        err = "Unknown source: {!r}".format(source)
 
537
        log(err)
 
538
        if fail_invalid:
 
539
            raise SourceConfigError(err)
 
540
 
 
541
 
 
542
def _add_proposed():
 
543
    """Add the PROPOSED_POCKET as /etc/apt/source.list.d/proposed.list
 
544
 
 
545
    Uses get_distrib_codename to determine the correct stanza for
 
546
    the deb line.
 
547
 
 
548
    For intel architecutres PROPOSED_POCKET is used for the release, but for
 
549
    other architectures PROPOSED_PORTS_POCKET is used for the release.
 
550
    """
 
551
    release = get_distrib_codename()
 
552
    arch = platform.machine()
 
553
    if arch not in six.iterkeys(ARCH_TO_PROPOSED_POCKET):
 
554
        raise SourceConfigError("Arch {} not supported for (distro-)proposed"
 
555
                                .format(arch))
 
556
    with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
 
557
        apt.write(ARCH_TO_PROPOSED_POCKET[arch].format(release))
 
558
 
 
559
 
 
560
def _add_apt_repository(spec):
 
561
    """Add the spec using add_apt_repository
 
562
 
 
563
    :param spec: the parameter to pass to add_apt_repository
 
564
    :type spec: str
 
565
    """
 
566
    if '{series}' in spec:
 
567
        series = get_distrib_codename()
 
568
        spec = spec.replace('{series}', series)
 
569
    # software-properties package for bionic properly reacts to proxy settings
 
570
    # passed as environment variables (See lp:1433761). This is not the case
 
571
    # LTS and non-LTS releases below bionic.
 
572
    _run_with_retries(['add-apt-repository', '--yes', spec],
 
573
                      cmd_env=env_proxy_settings(['https']))
 
574
 
 
575
 
 
576
def _add_cloud_pocket(pocket):
 
577
    """Add a cloud pocket as /etc/apt/sources.d/cloud-archive.list
 
578
 
 
579
    Note that this overwrites the existing file if there is one.
 
580
 
 
581
    This function also converts the simple pocket in to the actual pocket using
 
582
    the CLOUD_ARCHIVE_POCKETS mapping.
 
583
 
 
584
    :param pocket: string representing the pocket to add a deb spec for.
 
585
    :raises: SourceConfigError if the cloud pocket doesn't exist or the
 
586
        requested release doesn't match the current distro version.
 
587
    """
 
588
    apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
256
589
                fatal=True)
257
 
        pocket = source.split(':')[-1]
258
 
        if pocket not in CLOUD_ARCHIVE_POCKETS:
259
 
            raise SourceConfigError(
260
 
                'Unsupported cloud: source option %s' %
261
 
                pocket)
262
 
        actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
263
 
        with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
264
 
            apt.write(CLOUD_ARCHIVE.format(actual_pocket))
265
 
    elif source == 'proposed':
266
 
        release = lsb_release()['DISTRIB_CODENAME']
267
 
        with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
268
 
            apt.write(PROPOSED_POCKET.format(release))
269
 
    elif source == 'distro':
270
 
        pass
271
 
    else:
272
 
        log("Unknown source: {!r}".format(source))
273
 
 
274
 
    if key:
275
 
        if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
276
 
            with NamedTemporaryFile('w+') as key_file:
277
 
                key_file.write(key)
278
 
                key_file.flush()
279
 
                key_file.seek(0)
280
 
                subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
281
 
        else:
282
 
            # Note that hkp: is in no way a secure protocol. Using a
283
 
            # GPG key id is pointless from a security POV unless you
284
 
            # absolutely trust your network and DNS.
285
 
            subprocess.check_call(['apt-key', 'adv', '--keyserver',
286
 
                                   'hkp://keyserver.ubuntu.com:80', '--recv',
287
 
                                   key])
 
590
    if pocket not in CLOUD_ARCHIVE_POCKETS:
 
591
        raise SourceConfigError(
 
592
            'Unsupported cloud: source option %s' %
 
593
            pocket)
 
594
    actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
 
595
    with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
 
596
        apt.write(CLOUD_ARCHIVE.format(actual_pocket))
 
597
 
 
598
 
 
599
def _add_cloud_staging(cloud_archive_release, openstack_release):
 
600
    """Add the cloud staging repository which is in
 
601
    ppa:ubuntu-cloud-archive/<openstack_release>-staging
 
602
 
 
603
    This function checks that the cloud_archive_release matches the current
 
604
    codename for the distro that charm is being installed on.
 
605
 
 
606
    :param cloud_archive_release: string, codename for the release.
 
607
    :param openstack_release: String, codename for the openstack release.
 
608
    :raises: SourceConfigError if the cloud_archive_release doesn't match the
 
609
        current version of the os.
 
610
    """
 
611
    _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
 
612
    ppa = 'ppa:ubuntu-cloud-archive/{}-staging'.format(openstack_release)
 
613
    cmd = 'add-apt-repository -y {}'.format(ppa)
 
614
    _run_with_retries(cmd.split(' '))
 
615
 
 
616
 
 
617
def _add_cloud_distro_check(cloud_archive_release, openstack_release):
 
618
    """Add the cloud pocket, but also check the cloud_archive_release against
 
619
    the current distro, and use the openstack_release as the full lookup.
 
620
 
 
621
    This just calls _add_cloud_pocket() with the openstack_release as pocket
 
622
    to get the correct cloud-archive.list for dpkg to work with.
 
623
 
 
624
    :param cloud_archive_release:String, codename for the distro release.
 
625
    :param openstack_release: String, spec for the release to look up in the
 
626
        CLOUD_ARCHIVE_POCKETS
 
627
    :raises: SourceConfigError if this is the wrong distro, or the pocket spec
 
628
        doesn't exist.
 
629
    """
 
630
    _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
 
631
    _add_cloud_pocket("{}-{}".format(cloud_archive_release, openstack_release))
 
632
 
 
633
 
 
634
def _verify_is_ubuntu_rel(release, os_release):
 
635
    """Verify that the release is in the same as the current ubuntu release.
 
636
 
 
637
    :param release: String, lowercase for the release.
 
638
    :param os_release: String, the os_release being asked for
 
639
    :raises: SourceConfigError if the release is not the same as the ubuntu
 
640
        release.
 
641
    """
 
642
    ubuntu_rel = get_distrib_codename()
 
643
    if release != ubuntu_rel:
 
644
        raise SourceConfigError(
 
645
            'Invalid Cloud Archive release specified: {}-{} on this Ubuntu'
 
646
            'version ({})'.format(release, os_release, ubuntu_rel))
288
647
 
289
648
 
290
649
def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
300
659
    :param: cmd_env: dict: Environment variables to add to the command run.
301
660
    """
302
661
 
303
 
    env = os.environ.copy()
 
662
    env = None
 
663
    kwargs = {}
304
664
    if cmd_env:
 
665
        env = os.environ.copy()
305
666
        env.update(cmd_env)
 
667
        kwargs['env'] = env
306
668
 
307
669
    if not retry_message:
308
670
        retry_message = "Failed executing '{}'".format(" ".join(cmd))
314
676
    retry_results = (None,) + retry_exitcodes
315
677
    while result in retry_results:
316
678
        try:
317
 
            result = subprocess.check_call(cmd, env=env)
 
679
            # result = subprocess.check_call(cmd, env=env)
 
680
            result = subprocess.check_call(cmd, **kwargs)
318
681
        except subprocess.CalledProcessError as e:
319
682
            retry_count = retry_count + 1
320
683
            if retry_count > max_retries:
327
690
def _run_apt_command(cmd, fatal=False):
328
691
    """Run an apt command with optional retries.
329
692
 
 
693
    :param: cmd: str: The apt command to run.
330
694
    :param: fatal: bool: Whether the command's output should be checked and
331
695
        retried.
332
696
    """
353
717
    cache = apt_cache()
354
718
    try:
355
719
        pkg = cache[package]
356
 
    except:
 
720
    except Exception:
357
721
        # the package is unknown to the current apt cache.
358
722
        return None
359
723