~paulgear/charms/trusty/ntpmaster/fix-python3-on-trusty

« back to all changes in this revision

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

  • Committer: David Britton
  • Date: 2014-09-27 02:30:58 UTC
  • mfrom: (10.1.1 resync-charm-helpers)
  • Revision ID: dpb@canonical.com-20140927023058-6hvbi1kcshw3aokj
resync charmhelpers, ad makefile targets to speed up future syncs [r=dpb] [a=tribaal]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
import importlib
 
2
from tempfile import NamedTemporaryFile
 
3
import time
2
4
from yaml import safe_load
3
5
from charmhelpers.core.host import (
4
6
    lsb_release
12
14
    config,
13
15
    log,
14
16
)
15
 
import apt_pkg
 
17
import os
 
18
 
16
19
 
17
20
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
18
21
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
20
23
PROPOSED_POCKET = """# Proposed
21
24
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
22
25
"""
 
26
CLOUD_ARCHIVE_POCKETS = {
 
27
    # Folsom
 
28
    'folsom': 'precise-updates/folsom',
 
29
    'precise-folsom': 'precise-updates/folsom',
 
30
    'precise-folsom/updates': 'precise-updates/folsom',
 
31
    'precise-updates/folsom': 'precise-updates/folsom',
 
32
    'folsom/proposed': 'precise-proposed/folsom',
 
33
    'precise-folsom/proposed': 'precise-proposed/folsom',
 
34
    'precise-proposed/folsom': 'precise-proposed/folsom',
 
35
    # Grizzly
 
36
    'grizzly': 'precise-updates/grizzly',
 
37
    'precise-grizzly': 'precise-updates/grizzly',
 
38
    'precise-grizzly/updates': 'precise-updates/grizzly',
 
39
    'precise-updates/grizzly': 'precise-updates/grizzly',
 
40
    'grizzly/proposed': 'precise-proposed/grizzly',
 
41
    'precise-grizzly/proposed': 'precise-proposed/grizzly',
 
42
    'precise-proposed/grizzly': 'precise-proposed/grizzly',
 
43
    # Havana
 
44
    'havana': 'precise-updates/havana',
 
45
    'precise-havana': 'precise-updates/havana',
 
46
    'precise-havana/updates': 'precise-updates/havana',
 
47
    'precise-updates/havana': 'precise-updates/havana',
 
48
    'havana/proposed': 'precise-proposed/havana',
 
49
    'precise-havana/proposed': 'precise-proposed/havana',
 
50
    'precise-proposed/havana': 'precise-proposed/havana',
 
51
    # Icehouse
 
52
    'icehouse': 'precise-updates/icehouse',
 
53
    'precise-icehouse': 'precise-updates/icehouse',
 
54
    'precise-icehouse/updates': 'precise-updates/icehouse',
 
55
    'precise-updates/icehouse': 'precise-updates/icehouse',
 
56
    'icehouse/proposed': 'precise-proposed/icehouse',
 
57
    'precise-icehouse/proposed': 'precise-proposed/icehouse',
 
58
    'precise-proposed/icehouse': 'precise-proposed/icehouse',
 
59
    # Juno
 
60
    'juno': 'trusty-updates/juno',
 
61
    'trusty-juno': 'trusty-updates/juno',
 
62
    'trusty-juno/updates': 'trusty-updates/juno',
 
63
    'trusty-updates/juno': 'trusty-updates/juno',
 
64
    'juno/proposed': 'trusty-proposed/juno',
 
65
    'juno/proposed': 'trusty-proposed/juno',
 
66
    'trusty-juno/proposed': 'trusty-proposed/juno',
 
67
    'trusty-proposed/juno': 'trusty-proposed/juno',
 
68
}
 
69
 
 
70
# The order of this list is very important. Handlers should be listed in from
 
71
# least- to most-specific URL matching.
 
72
FETCH_HANDLERS = (
 
73
    'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
 
74
    'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
 
75
)
 
76
 
 
77
APT_NO_LOCK = 100  # The return code for "couldn't acquire lock" in APT.
 
78
APT_NO_LOCK_RETRY_DELAY = 10  # Wait 10 seconds between apt lock checks.
 
79
APT_NO_LOCK_RETRY_COUNT = 30  # Retry to acquire the lock X times.
 
80
 
 
81
 
 
82
class SourceConfigError(Exception):
 
83
    pass
 
84
 
 
85
 
 
86
class UnhandledSource(Exception):
 
87
    pass
 
88
 
 
89
 
 
90
class AptLockError(Exception):
 
91
    pass
 
92
 
 
93
 
 
94
class BaseFetchHandler(object):
 
95
 
 
96
    """Base class for FetchHandler implementations in fetch plugins"""
 
97
 
 
98
    def can_handle(self, source):
 
99
        """Returns True if the source can be handled. Otherwise returns
 
100
        a string explaining why it cannot"""
 
101
        return "Wrong source type"
 
102
 
 
103
    def install(self, source):
 
104
        """Try to download and unpack the source. Return the path to the
 
105
        unpacked files or raise UnhandledSource."""
 
106
        raise UnhandledSource("Wrong source type {}".format(source))
 
107
 
 
108
    def parse_url(self, url):
 
109
        return urlparse(url)
 
110
 
 
111
    def base_url(self, url):
 
112
        """Return url without querystring or fragment"""
 
113
        parts = list(self.parse_url(url))
 
114
        parts[4:] = ['' for i in parts[4:]]
 
115
        return urlunparse(parts)
23
116
 
24
117
 
25
118
def filter_installed_packages(packages):
26
119
    """Returns a list of packages that require installation"""
27
 
    apt_pkg.init()
28
 
    cache = apt_pkg.Cache()
 
120
    cache = apt_cache()
29
121
    _pkgs = []
30
122
    for package in packages:
31
123
        try:
38
130
    return _pkgs
39
131
 
40
132
 
 
133
def apt_cache(in_memory=True):
 
134
    """Build and return an apt cache"""
 
135
    import apt_pkg
 
136
    apt_pkg.init()
 
137
    if in_memory:
 
138
        apt_pkg.config.set("Dir::Cache::pkgcache", "")
 
139
        apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
 
140
    return apt_pkg.Cache()
 
141
 
 
142
 
41
143
def apt_install(packages, options=None, fatal=False):
42
144
    """Install one or more packages"""
43
 
    options = options or []
44
 
    cmd = ['apt-get', '-y']
 
145
    if options is None:
 
146
        options = ['--option=Dpkg::Options::=--force-confold']
 
147
 
 
148
    cmd = ['apt-get', '--assume-yes']
45
149
    cmd.extend(options)
46
150
    cmd.append('install')
47
151
    if isinstance(packages, basestring):
50
154
        cmd.extend(packages)
51
155
    log("Installing {} with options: {}".format(packages,
52
156
                                                options))
53
 
    if fatal:
54
 
        subprocess.check_call(cmd)
 
157
    _run_apt_command(cmd, fatal)
 
158
 
 
159
 
 
160
def apt_upgrade(options=None, fatal=False, dist=False):
 
161
    """Upgrade all packages"""
 
162
    if options is None:
 
163
        options = ['--option=Dpkg::Options::=--force-confold']
 
164
 
 
165
    cmd = ['apt-get', '--assume-yes']
 
166
    cmd.extend(options)
 
167
    if dist:
 
168
        cmd.append('dist-upgrade')
55
169
    else:
56
 
        subprocess.call(cmd)
 
170
        cmd.append('upgrade')
 
171
    log("Upgrading with options: {}".format(options))
 
172
    _run_apt_command(cmd, fatal)
57
173
 
58
174
 
59
175
def apt_update(fatal=False):
60
176
    """Update local apt cache"""
61
177
    cmd = ['apt-get', 'update']
62
 
    if fatal:
63
 
        subprocess.check_call(cmd)
64
 
    else:
65
 
        subprocess.call(cmd)
 
178
    _run_apt_command(cmd, fatal)
66
179
 
67
180
 
68
181
def apt_purge(packages, fatal=False):
69
182
    """Purge one or more packages"""
70
 
    cmd = ['apt-get', '-y', 'purge']
 
183
    cmd = ['apt-get', '--assume-yes', 'purge']
71
184
    if isinstance(packages, basestring):
72
185
        cmd.append(packages)
73
186
    else:
74
187
        cmd.extend(packages)
75
188
    log("Purging {}".format(packages))
 
189
    _run_apt_command(cmd, fatal)
 
190
 
 
191
 
 
192
def apt_hold(packages, fatal=False):
 
193
    """Hold one or more packages"""
 
194
    cmd = ['apt-mark', 'hold']
 
195
    if isinstance(packages, basestring):
 
196
        cmd.append(packages)
 
197
    else:
 
198
        cmd.extend(packages)
 
199
    log("Holding {}".format(packages))
 
200
 
76
201
    if fatal:
77
202
        subprocess.check_call(cmd)
78
203
    else:
80
205
 
81
206
 
82
207
def add_source(source, key=None):
83
 
    if ((source.startswith('ppa:') or
84
 
         source.startswith('http:'))):
 
208
    """Add a package source to this system.
 
209
 
 
210
    @param source: a URL or sources.list entry, as supported by
 
211
    add-apt-repository(1). Examples::
 
212
 
 
213
        ppa:charmers/example
 
214
        deb https://stub:key@private.example.com/ubuntu trusty main
 
215
 
 
216
    In addition:
 
217
        'proposed:' may be used to enable the standard 'proposed'
 
218
        pocket for the release.
 
219
        'cloud:' may be used to activate official cloud archive pockets,
 
220
        such as 'cloud:icehouse'
 
221
 
 
222
    @param key: A key to be added to the system's APT keyring and used
 
223
    to verify the signatures on packages. Ideally, this should be an
 
224
    ASCII format GPG public key including the block headers. A GPG key
 
225
    id may also be used, but be aware that only insecure protocols are
 
226
    available to retrieve the actual public key from a public keyserver
 
227
    placing your Juju environment at risk. ppa and cloud archive keys
 
228
    are securely added automtically, so sould not be provided.
 
229
    """
 
230
    if source is None:
 
231
        log('Source is not present. Skipping')
 
232
        return
 
233
 
 
234
    if (source.startswith('ppa:') or
 
235
        source.startswith('http') or
 
236
        source.startswith('deb ') or
 
237
            source.startswith('cloud-archive:')):
85
238
        subprocess.check_call(['add-apt-repository', '--yes', source])
86
239
    elif source.startswith('cloud:'):
87
240
        apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
88
241
                    fatal=True)
89
242
        pocket = source.split(':')[-1]
 
243
        if pocket not in CLOUD_ARCHIVE_POCKETS:
 
244
            raise SourceConfigError(
 
245
                'Unsupported cloud: source option %s' %
 
246
                pocket)
 
247
        actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
90
248
        with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
91
 
            apt.write(CLOUD_ARCHIVE.format(pocket))
 
249
            apt.write(CLOUD_ARCHIVE.format(actual_pocket))
92
250
    elif source == 'proposed':
93
251
        release = lsb_release()['DISTRIB_CODENAME']
94
252
        with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
95
253
            apt.write(PROPOSED_POCKET.format(release))
 
254
    else:
 
255
        raise SourceConfigError("Unknown source: {!r}".format(source))
 
256
 
96
257
    if key:
97
 
        subprocess.check_call(['apt-key', 'import', key])
98
 
 
99
 
 
100
 
class SourceConfigError(Exception):
101
 
    pass
 
258
        if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
 
259
            with NamedTemporaryFile() as key_file:
 
260
                key_file.write(key)
 
261
                key_file.flush()
 
262
                key_file.seek(0)
 
263
                subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
 
264
        else:
 
265
            # Note that hkp: is in no way a secure protocol. Using a
 
266
            # GPG key id is pointless from a security POV unless you
 
267
            # absolutely trust your network and DNS.
 
268
            subprocess.check_call(['apt-key', 'adv', '--keyserver',
 
269
                                   'hkp://keyserver.ubuntu.com:80', '--recv',
 
270
                                   key])
102
271
 
103
272
 
104
273
def configure_sources(update=False,
105
274
                      sources_var='install_sources',
106
275
                      keys_var='install_keys'):
107
276
    """
108
 
    Configure multiple sources from charm configuration
 
277
    Configure multiple sources from charm configuration.
 
278
 
 
279
    The lists are encoded as yaml fragments in the configuration.
 
280
    The frament needs to be included as a string. Sources and their
 
281
    corresponding keys are of the types supported by add_source().
109
282
 
110
283
    Example config:
111
 
        install_sources:
 
284
        install_sources: |
112
285
          - "ppa:foo"
113
286
          - "http://example.com/repo precise main"
114
 
        install_keys:
 
287
        install_keys: |
115
288
          - null
116
289
          - "a1b2c3d4"
117
290
 
118
291
    Note that 'null' (a.k.a. None) should not be quoted.
119
292
    """
120
 
    sources = safe_load(config(sources_var))
121
 
    keys = safe_load(config(keys_var))
122
 
    if isinstance(sources, basestring) and isinstance(keys, basestring):
123
 
        add_source(sources, keys)
 
293
    sources = safe_load((config(sources_var) or '').strip()) or []
 
294
    keys = safe_load((config(keys_var) or '').strip()) or None
 
295
 
 
296
    if isinstance(sources, basestring):
 
297
        sources = [sources]
 
298
 
 
299
    if keys is None:
 
300
        for source in sources:
 
301
            add_source(source, None)
124
302
    else:
125
 
        if not len(sources) == len(keys):
126
 
            msg = 'Install sources and keys lists are different lengths'
127
 
            raise SourceConfigError(msg)
128
 
        for src_num in range(len(sources)):
129
 
            add_source(sources[src_num], keys[src_num])
 
303
        if isinstance(keys, basestring):
 
304
            keys = [keys]
 
305
 
 
306
        if len(sources) != len(keys):
 
307
            raise SourceConfigError(
 
308
                'Install sources and keys lists are different lengths')
 
309
        for source, key in zip(sources, keys):
 
310
            add_source(source, key)
130
311
    if update:
131
312
        apt_update(fatal=True)
132
313
 
133
 
# The order of this list is very important. Handlers should be listed in from
134
 
# least- to most-specific URL matching.
135
 
FETCH_HANDLERS = (
136
 
    'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
137
 
    'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
138
 
)
139
 
 
140
 
 
141
 
class UnhandledSource(Exception):
142
 
    pass
143
 
 
144
 
 
145
 
def install_remote(source):
 
314
 
 
315
def install_remote(source, *args, **kwargs):
146
316
    """
147
317
    Install a file tree from a remote source
148
318
 
149
319
    The specified source should be a url of the form:
150
320
        scheme://[host]/path[#[option=value][&...]]
151
321
 
152
 
    Schemes supported are based on this modules submodules
153
 
    Options supported are submodule-specific"""
 
322
    Schemes supported are based on this modules submodules.
 
323
    Options supported are submodule-specific.
 
324
    Additional arguments are passed through to the submodule.
 
325
 
 
326
    For example::
 
327
 
 
328
        dest = install_remote('http://example.com/archive.tgz',
 
329
                              checksum='deadbeef',
 
330
                              hash_type='sha1')
 
331
 
 
332
    This will download `archive.tgz`, validate it using SHA1 and, if
 
333
    the file is ok, extract it and return the directory in which it
 
334
    was extracted.  If the checksum fails, it will raise
 
335
    :class:`charmhelpers.core.host.ChecksumError`.
 
336
    """
154
337
    # We ONLY check for True here because can_handle may return a string
155
338
    # explaining why it can't handle a given source.
156
339
    handlers = [h for h in plugins() if h.can_handle(source) is True]
157
340
    installed_to = None
158
341
    for handler in handlers:
159
342
        try:
160
 
            installed_to = handler.install(source)
 
343
            installed_to = handler.install(source, *args, **kwargs)
161
344
        except UnhandledSource:
162
345
            pass
163
346
    if not installed_to:
171
354
    return install_remote(source)
172
355
 
173
356
 
174
 
class BaseFetchHandler(object):
175
 
    """Base class for FetchHandler implementations in fetch plugins"""
176
 
    def can_handle(self, source):
177
 
        """Returns True if the source can be handled. Otherwise returns
178
 
        a string explaining why it cannot"""
179
 
        return "Wrong source type"
180
 
 
181
 
    def install(self, source):
182
 
        """Try to download and unpack the source. Return the path to the
183
 
        unpacked files or raise UnhandledSource."""
184
 
        raise UnhandledSource("Wrong source type {}".format(source))
185
 
 
186
 
    def parse_url(self, url):
187
 
        return urlparse(url)
188
 
 
189
 
    def base_url(self, url):
190
 
        """Return url without querystring or fragment"""
191
 
        parts = list(self.parse_url(url))
192
 
        parts[4:] = ['' for i in parts[4:]]
193
 
        return urlunparse(parts)
194
 
 
195
 
 
196
357
def plugins(fetch_handlers=None):
197
358
    if not fetch_handlers:
198
359
        fetch_handlers = FETCH_HANDLERS
200
361
    for handler_name in fetch_handlers:
201
362
        package, classname = handler_name.rsplit('.', 1)
202
363
        try:
203
 
            handler_class = getattr(importlib.import_module(package), classname)
 
364
            handler_class = getattr(
 
365
                importlib.import_module(package),
 
366
                classname)
204
367
            plugin_list.append(handler_class())
205
368
        except (ImportError, AttributeError):
206
369
            # Skip missing plugins so that they can be ommitted from
207
370
            # installation if desired
208
 
            log("FetchHandler {} not found, skipping plugin".format(handler_name))
 
371
            log("FetchHandler {} not found, skipping plugin".format(
 
372
                handler_name))
209
373
    return plugin_list
 
374
 
 
375
 
 
376
def _run_apt_command(cmd, fatal=False):
 
377
    """
 
378
    Run an APT command, checking output and retrying if the fatal flag is set
 
379
    to True.
 
380
 
 
381
    :param: cmd: str: The apt command to run.
 
382
    :param: fatal: bool: Whether the command's output should be checked and
 
383
        retried.
 
384
    """
 
385
    env = os.environ.copy()
 
386
 
 
387
    if 'DEBIAN_FRONTEND' not in env:
 
388
        env['DEBIAN_FRONTEND'] = 'noninteractive'
 
389
 
 
390
    if fatal:
 
391
        retry_count = 0
 
392
        result = None
 
393
 
 
394
        # If the command is considered "fatal", we need to retry if the apt
 
395
        # lock was not acquired.
 
396
 
 
397
        while result is None or result == APT_NO_LOCK:
 
398
            try:
 
399
                result = subprocess.check_call(cmd, env=env)
 
400
            except subprocess.CalledProcessError, e:
 
401
                retry_count = retry_count + 1
 
402
                if retry_count > APT_NO_LOCK_RETRY_COUNT:
 
403
                    raise
 
404
                result = e.returncode
 
405
                log("Couldn't acquire DPKG lock. Will retry in {} seconds."
 
406
                    "".format(APT_NO_LOCK_RETRY_DELAY))
 
407
                time.sleep(APT_NO_LOCK_RETRY_DELAY)
 
408
 
 
409
    else:
 
410
        subprocess.call(cmd, env=env)