~canonical-is-sa/charms/trusty/elasticsearch/trunk

« back to all changes in this revision

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

  • Committer: Michael Nelson
  • Date: 2014-11-06 02:48:04 UTC
  • mfrom: (35.1.8 firewall_optional)
  • Revision ID: michael.nelson@canonical.com-20141106024804-uxduhm2huet147vi
[r=simondavy][bugs=1386664,1376396] Bug fix for firewall on ec2 and enable firewall to be configured off (as well as update of tests and charmhelpers).

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