2
from tempfile import NamedTemporaryFile
4
from yaml import safe_load
5
from charmhelpers.core.host import (
9
from charmhelpers.core.hookenv import (
17
from urllib.parse import urlparse, urlunparse
19
from urlparse import urlparse, urlunparse
22
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
23
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
25
PROPOSED_POCKET = """# Proposed
26
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
28
CLOUD_ARCHIVE_POCKETS = {
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',
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',
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',
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',
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',
72
# The order of this list is very important. Handlers should be listed in from
73
# least- to most-specific URL matching.
75
'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
76
'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
77
'charmhelpers.fetch.giturl.GitUrlFetchHandler',
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.
85
class SourceConfigError(Exception):
89
class UnhandledSource(Exception):
93
class AptLockError(Exception):
97
class BaseFetchHandler(object):
99
"""Base class for FetchHandler implementations in fetch plugins"""
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"
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))
111
def parse_url(self, url):
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)
121
def filter_installed_packages(packages):
122
"""Returns a list of packages that require installation"""
125
for package in packages:
128
p.current_ver or _pkgs.append(package)
130
log('Package {} has no installation candidate.'.format(package),
132
_pkgs.append(package)
136
def apt_cache(in_memory=True):
137
"""Build and return an apt cache"""
141
apt_pkg.config.set("Dir::Cache::pkgcache", "")
142
apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
143
return apt_pkg.Cache()
146
def apt_install(packages, options=None, fatal=False):
147
"""Install one or more packages"""
149
options = ['--option=Dpkg::Options::=--force-confold']
151
cmd = ['apt-get', '--assume-yes']
153
cmd.append('install')
154
if isinstance(packages, six.string_types):
158
log("Installing {} with options: {}".format(packages,
160
_run_apt_command(cmd, fatal)
163
def apt_upgrade(options=None, fatal=False, dist=False):
164
"""Upgrade all packages"""
166
options = ['--option=Dpkg::Options::=--force-confold']
168
cmd = ['apt-get', '--assume-yes']
171
cmd.append('dist-upgrade')
173
cmd.append('upgrade')
174
log("Upgrading with options: {}".format(options))
175
_run_apt_command(cmd, fatal)
178
def apt_update(fatal=False):
179
"""Update local apt cache"""
180
cmd = ['apt-get', 'update']
181
_run_apt_command(cmd, fatal)
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):
191
log("Purging {}".format(packages))
192
_run_apt_command(cmd, fatal)
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):
202
log("Holding {}".format(packages))
205
subprocess.check_call(cmd)
210
def add_source(source, key=None):
211
"""Add a package source to this system.
213
@param source: a URL or sources.list entry, as supported by
214
add-apt-repository(1). Examples::
217
deb https://stub:key@private.example.com/ubuntu trusty main
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
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.
235
log('Source is not present. Skipping')
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']),
246
pocket = source.split(':')[-1]
247
if pocket not in CLOUD_ARCHIVE_POCKETS:
248
raise SourceConfigError(
249
'Unsupported cloud: source option %s' %
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':
261
log("Unknown source: {!r}".format(source))
264
if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
265
with NamedTemporaryFile('w+') as key_file:
269
subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
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',
279
def configure_sources(update=False,
280
sources_var='install_sources',
281
keys_var='install_keys'):
283
Configure multiple sources from charm configuration.
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().
292
- "http://example.com/repo precise main"
297
Note that 'null' (a.k.a. None) should not be quoted.
299
sources = safe_load((config(sources_var) or '').strip()) or []
300
keys = safe_load((config(keys_var) or '').strip()) or None
302
if isinstance(sources, six.string_types):
306
for source in sources:
307
add_source(source, None)
309
if isinstance(keys, six.string_types):
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)
318
apt_update(fatal=True)
321
def install_remote(source, *args, **kwargs):
323
Install a file tree from a remote source
325
The specified source should be a url of the form:
326
scheme://[host]/path[#[option=value][&...]]
328
Schemes supported are based on this modules submodules.
329
Options supported are submodule-specific.
330
Additional arguments are passed through to the submodule.
334
dest = install_remote('http://example.com/archive.tgz',
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`.
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]
347
for handler in handlers:
349
installed_to = handler.install(source, *args, **kwargs)
350
except UnhandledSource:
353
raise UnhandledSource("No handler found for source {}".format(source))
357
def install_from_config(config_var_name):
358
charm_config = config()
359
source = charm_config[config_var_name]
360
return install_remote(source)
363
def plugins(fetch_handlers=None):
364
if not fetch_handlers:
365
fetch_handlers = FETCH_HANDLERS
367
for handler_name in fetch_handlers:
368
package, classname = handler_name.rsplit('.', 1)
370
handler_class = getattr(
371
importlib.import_module(package),
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(
382
def _run_apt_command(cmd, fatal=False):
384
Run an APT command, checking output and retrying if the fatal flag is set
387
:param: cmd: str: The apt command to run.
388
:param: fatal: bool: Whether the command's output should be checked and
391
env = os.environ.copy()
393
if 'DEBIAN_FRONTEND' not in env:
394
env['DEBIAN_FRONTEND'] = 'noninteractive'
400
# If the command is considered "fatal", we need to retry if the apt
401
# lock was not acquired.
403
while result is None or result == APT_NO_LOCK:
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:
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)
416
subprocess.call(cmd, env=env)