~ajkavanagh/charms/trusty/memcached/add-spaces-support

« back to all changes in this revision

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

  • Committer: Jorge Niedbalski
  • Date: 2016-09-20 18:01:20 UTC
  • mfrom: (72.2.3 memcached.py3)
  • Revision ID: jorge.niedbalski@canonical.com-20160920180120-apb4ut3cugwxcrer
[freyes, r=niedbalski] Fixes bug LP: #1576458

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# Licensed under the Apache License, Version 2.0 (the "License");
 
4
# you may not use this file except in compliance with the License.
 
5
# You may obtain a copy of the License at
 
6
#
 
7
#  http://www.apache.org/licenses/LICENSE-2.0
 
8
#
 
9
# Unless required by applicable law or agreed to in writing, software
 
10
# distributed under the License is distributed on an "AS IS" BASIS,
 
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
12
# See the License for the specific language governing permissions and
 
13
# limitations under the License.
 
14
 
 
15
import os
 
16
import six
 
17
import time
 
18
import subprocess
 
19
 
 
20
from tempfile import NamedTemporaryFile
 
21
from charmhelpers.core.host import (
 
22
    lsb_release
 
23
)
 
24
from charmhelpers.core.hookenv import log
 
25
from charmhelpers.fetch import SourceConfigError
 
26
 
 
27
CLOUD_ARCHIVE = ('# Ubuntu Cloud Archive deb'
 
28
                 ' http://ubuntu-cloud.archive.canonical.com/ubuntu'
 
29
                 ' {} main')
 
30
PROPOSED_POCKET = ('# Proposed deb http://archive.ubuntu.com/ubuntu'
 
31
                   ' {}-proposed main universe multiverse restricted')
 
32
CLOUD_ARCHIVE_POCKETS = {
 
33
    # Folsom
 
34
    'folsom': 'precise-updates/folsom',
 
35
    'precise-folsom': 'precise-updates/folsom',
 
36
    'precise-folsom/updates': 'precise-updates/folsom',
 
37
    'precise-updates/folsom': 'precise-updates/folsom',
 
38
    'folsom/proposed': 'precise-proposed/folsom',
 
39
    'precise-folsom/proposed': 'precise-proposed/folsom',
 
40
    'precise-proposed/folsom': 'precise-proposed/folsom',
 
41
    # Grizzly
 
42
    'grizzly': 'precise-updates/grizzly',
 
43
    'precise-grizzly': 'precise-updates/grizzly',
 
44
    'precise-grizzly/updates': 'precise-updates/grizzly',
 
45
    'precise-updates/grizzly': 'precise-updates/grizzly',
 
46
    'grizzly/proposed': 'precise-proposed/grizzly',
 
47
    'precise-grizzly/proposed': 'precise-proposed/grizzly',
 
48
    'precise-proposed/grizzly': 'precise-proposed/grizzly',
 
49
    # Havana
 
50
    'havana': 'precise-updates/havana',
 
51
    'precise-havana': 'precise-updates/havana',
 
52
    'precise-havana/updates': 'precise-updates/havana',
 
53
    'precise-updates/havana': 'precise-updates/havana',
 
54
    'havana/proposed': 'precise-proposed/havana',
 
55
    'precise-havana/proposed': 'precise-proposed/havana',
 
56
    'precise-proposed/havana': 'precise-proposed/havana',
 
57
    # Icehouse
 
58
    'icehouse': 'precise-updates/icehouse',
 
59
    'precise-icehouse': 'precise-updates/icehouse',
 
60
    'precise-icehouse/updates': 'precise-updates/icehouse',
 
61
    'precise-updates/icehouse': 'precise-updates/icehouse',
 
62
    'icehouse/proposed': 'precise-proposed/icehouse',
 
63
    'precise-icehouse/proposed': 'precise-proposed/icehouse',
 
64
    'precise-proposed/icehouse': 'precise-proposed/icehouse',
 
65
    # Juno
 
66
    'juno': 'trusty-updates/juno',
 
67
    'trusty-juno': 'trusty-updates/juno',
 
68
    'trusty-juno/updates': 'trusty-updates/juno',
 
69
    'trusty-updates/juno': 'trusty-updates/juno',
 
70
    'juno/proposed': 'trusty-proposed/juno',
 
71
    'trusty-juno/proposed': 'trusty-proposed/juno',
 
72
    'trusty-proposed/juno': 'trusty-proposed/juno',
 
73
    # Kilo
 
74
    'kilo': 'trusty-updates/kilo',
 
75
    'trusty-kilo': 'trusty-updates/kilo',
 
76
    'trusty-kilo/updates': 'trusty-updates/kilo',
 
77
    'trusty-updates/kilo': 'trusty-updates/kilo',
 
78
    'kilo/proposed': 'trusty-proposed/kilo',
 
79
    'trusty-kilo/proposed': 'trusty-proposed/kilo',
 
80
    'trusty-proposed/kilo': 'trusty-proposed/kilo',
 
81
    # Liberty
 
82
    'liberty': 'trusty-updates/liberty',
 
83
    'trusty-liberty': 'trusty-updates/liberty',
 
84
    'trusty-liberty/updates': 'trusty-updates/liberty',
 
85
    'trusty-updates/liberty': 'trusty-updates/liberty',
 
86
    'liberty/proposed': 'trusty-proposed/liberty',
 
87
    'trusty-liberty/proposed': 'trusty-proposed/liberty',
 
88
    'trusty-proposed/liberty': 'trusty-proposed/liberty',
 
89
    # Mitaka
 
90
    'mitaka': 'trusty-updates/mitaka',
 
91
    'trusty-mitaka': 'trusty-updates/mitaka',
 
92
    'trusty-mitaka/updates': 'trusty-updates/mitaka',
 
93
    'trusty-updates/mitaka': 'trusty-updates/mitaka',
 
94
    'mitaka/proposed': 'trusty-proposed/mitaka',
 
95
    'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
 
96
    'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
 
97
    # Newton
 
98
    'newton': 'xenial-updates/newton',
 
99
    'xenial-newton': 'xenial-updates/newton',
 
100
    'xenial-newton/updates': 'xenial-updates/newton',
 
101
    'xenial-updates/newton': 'xenial-updates/newton',
 
102
    'newton/proposed': 'xenial-proposed/newton',
 
103
    'xenial-newton/proposed': 'xenial-proposed/newton',
 
104
    'xenial-proposed/newton': 'xenial-proposed/newton',
 
105
}
 
106
 
 
107
APT_NO_LOCK = 100  # The return code for "couldn't acquire lock" in APT.
 
108
APT_NO_LOCK_RETRY_DELAY = 10  # Wait 10 seconds between apt lock checks.
 
109
APT_NO_LOCK_RETRY_COUNT = 30  # Retry to acquire the lock X times.
 
110
 
 
111
 
 
112
def filter_installed_packages(packages):
 
113
    """Return a list of packages that require installation."""
 
114
    cache = apt_cache()
 
115
    _pkgs = []
 
116
    for package in packages:
 
117
        try:
 
118
            p = cache[package]
 
119
            p.current_ver or _pkgs.append(package)
 
120
        except KeyError:
 
121
            log('Package {} has no installation candidate.'.format(package),
 
122
                level='WARNING')
 
123
            _pkgs.append(package)
 
124
    return _pkgs
 
125
 
 
126
 
 
127
def apt_cache(in_memory=True, progress=None):
 
128
    """Build and return an apt cache."""
 
129
    from apt import apt_pkg
 
130
    apt_pkg.init()
 
131
    if in_memory:
 
132
        apt_pkg.config.set("Dir::Cache::pkgcache", "")
 
133
        apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
 
134
    return apt_pkg.Cache(progress)
 
135
 
 
136
 
 
137
def install(packages, options=None, fatal=False):
 
138
    """Install one or more packages."""
 
139
    if options is None:
 
140
        options = ['--option=Dpkg::Options::=--force-confold']
 
141
 
 
142
    cmd = ['apt-get', '--assume-yes']
 
143
    cmd.extend(options)
 
144
    cmd.append('install')
 
145
    if isinstance(packages, six.string_types):
 
146
        cmd.append(packages)
 
147
    else:
 
148
        cmd.extend(packages)
 
149
    log("Installing {} with options: {}".format(packages,
 
150
                                                options))
 
151
    _run_apt_command(cmd, fatal)
 
152
 
 
153
 
 
154
def upgrade(options=None, fatal=False, dist=False):
 
155
    """Upgrade all packages."""
 
156
    if options is None:
 
157
        options = ['--option=Dpkg::Options::=--force-confold']
 
158
 
 
159
    cmd = ['apt-get', '--assume-yes']
 
160
    cmd.extend(options)
 
161
    if dist:
 
162
        cmd.append('dist-upgrade')
 
163
    else:
 
164
        cmd.append('upgrade')
 
165
    log("Upgrading with options: {}".format(options))
 
166
    _run_apt_command(cmd, fatal)
 
167
 
 
168
 
 
169
def update(fatal=False):
 
170
    """Update local apt cache."""
 
171
    cmd = ['apt-get', 'update']
 
172
    _run_apt_command(cmd, fatal)
 
173
 
 
174
 
 
175
def purge(packages, fatal=False):
 
176
    """Purge one or more packages."""
 
177
    cmd = ['apt-get', '--assume-yes', 'purge']
 
178
    if isinstance(packages, six.string_types):
 
179
        cmd.append(packages)
 
180
    else:
 
181
        cmd.extend(packages)
 
182
    log("Purging {}".format(packages))
 
183
    _run_apt_command(cmd, fatal)
 
184
 
 
185
 
 
186
def apt_mark(packages, mark, fatal=False):
 
187
    """Flag one or more packages using apt-mark."""
 
188
    log("Marking {} as {}".format(packages, mark))
 
189
    cmd = ['apt-mark', mark]
 
190
    if isinstance(packages, six.string_types):
 
191
        cmd.append(packages)
 
192
    else:
 
193
        cmd.extend(packages)
 
194
 
 
195
    if fatal:
 
196
        subprocess.check_call(cmd, universal_newlines=True)
 
197
    else:
 
198
        subprocess.call(cmd, universal_newlines=True)
 
199
 
 
200
 
 
201
def apt_hold(packages, fatal=False):
 
202
    return apt_mark(packages, 'hold', fatal=fatal)
 
203
 
 
204
 
 
205
def apt_unhold(packages, fatal=False):
 
206
    return apt_mark(packages, 'unhold', fatal=fatal)
 
207
 
 
208
 
 
209
def add_source(source, key=None):
 
210
    """Add a package source to this system.
 
211
 
 
212
    @param source: a URL or sources.list entry, as supported by
 
213
    add-apt-repository(1). Examples::
 
214
 
 
215
        ppa:charmers/example
 
216
        deb https://stub:key@private.example.com/ubuntu trusty main
 
217
 
 
218
    In addition:
 
219
        'proposed:' may be used to enable the standard 'proposed'
 
220
        pocket for the release.
 
221
        'cloud:' may be used to activate official cloud archive pockets,
 
222
        such as 'cloud:icehouse'
 
223
        'distro' may be used as a noop
 
224
 
 
225
    @param key: A key to be added to the system's APT keyring and used
 
226
    to verify the signatures on packages. Ideally, this should be an
 
227
    ASCII format GPG public key including the block headers. A GPG key
 
228
    id may also be used, but be aware that only insecure protocols are
 
229
    available to retrieve the actual public key from a public keyserver
 
230
    placing your Juju environment at risk. ppa and cloud archive keys
 
231
    are securely added automtically, so sould not be provided.
 
232
    """
 
233
    if source is None:
 
234
        log('Source is not present. Skipping')
 
235
        return
 
236
 
 
237
    if (source.startswith('ppa:') or
 
238
        source.startswith('http') or
 
239
        source.startswith('deb ') or
 
240
            source.startswith('cloud-archive:')):
 
241
        subprocess.check_call(['add-apt-repository', '--yes', source])
 
242
    elif source.startswith('cloud:'):
 
243
        install(filter_installed_packages(['ubuntu-cloud-keyring']),
 
244
                fatal=True)
 
245
        pocket = source.split(':')[-1]
 
246
        if pocket not in CLOUD_ARCHIVE_POCKETS:
 
247
            raise SourceConfigError(
 
248
                'Unsupported cloud: source option %s' %
 
249
                pocket)
 
250
        actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
 
251
        with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
 
252
            apt.write(CLOUD_ARCHIVE.format(actual_pocket))
 
253
    elif source == 'proposed':
 
254
        release = lsb_release()['DISTRIB_CODENAME']
 
255
        with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
 
256
            apt.write(PROPOSED_POCKET.format(release))
 
257
    elif source == 'distro':
 
258
        pass
 
259
    else:
 
260
        log("Unknown source: {!r}".format(source))
 
261
 
 
262
    if key:
 
263
        if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
 
264
            with NamedTemporaryFile('w+') as key_file:
 
265
                key_file.write(key)
 
266
                key_file.flush()
 
267
                key_file.seek(0)
 
268
                subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
 
269
        else:
 
270
            # Note that hkp: is in no way a secure protocol. Using a
 
271
            # GPG key id is pointless from a security POV unless you
 
272
            # absolutely trust your network and DNS.
 
273
            subprocess.check_call(['apt-key', 'adv', '--keyserver',
 
274
                                   'hkp://keyserver.ubuntu.com:80', '--recv',
 
275
                                   key])
 
276
 
 
277
 
 
278
def _run_apt_command(cmd, fatal=False):
 
279
    """Run an APT command.
 
280
 
 
281
    Checks the output and retries if the fatal flag is set
 
282
    to True.
 
283
 
 
284
    :param: cmd: str: The apt command to run.
 
285
    :param: fatal: bool: Whether the command's output should be checked and
 
286
        retried.
 
287
    """
 
288
    env = os.environ.copy()
 
289
 
 
290
    if 'DEBIAN_FRONTEND' not in env:
 
291
        env['DEBIAN_FRONTEND'] = 'noninteractive'
 
292
 
 
293
    if fatal:
 
294
        retry_count = 0
 
295
        result = None
 
296
 
 
297
        # If the command is considered "fatal", we need to retry if the apt
 
298
        # lock was not acquired.
 
299
 
 
300
        while result is None or result == APT_NO_LOCK:
 
301
            try:
 
302
                result = subprocess.check_call(cmd, env=env)
 
303
            except subprocess.CalledProcessError as e:
 
304
                retry_count = retry_count + 1
 
305
                if retry_count > APT_NO_LOCK_RETRY_COUNT:
 
306
                    raise
 
307
                result = e.returncode
 
308
                log("Couldn't acquire DPKG lock. Will retry in {} seconds."
 
309
                    "".format(APT_NO_LOCK_RETRY_DELAY))
 
310
                time.sleep(APT_NO_LOCK_RETRY_DELAY)
 
311
 
 
312
    else:
 
313
        subprocess.call(cmd, env=env)