~niedbalski/charms/trusty/memcached/replication

« back to all changes in this revision

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

  • Committer: Jorge Niedbalski
  • Author(s): Felipe Reyes
  • Date: 2014-12-09 22:11:53 UTC
  • mfrom: (60.1.33 memcached)
  • Revision ID: jorge.niedbalski@canonical.com-20141209221153-1iewwgtv14f7nk3v
[freyes, r=niedbalski] MP is a rewrite in python to leverage charmhelpers and secure memcached 

Show diffs side-by-side

added added

removed removed

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