20
23
PROPOSED_POCKET = """# Proposed
21
24
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
26
CLOUD_ARCHIVE_POCKETS = {
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',
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',
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',
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',
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',
70
# The order of this list is very important. Handlers should be listed in from
71
# least- to most-specific URL matching.
73
'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
74
'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
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.
82
class SourceConfigError(Exception):
86
class UnhandledSource(Exception):
90
class AptLockError(Exception):
94
class BaseFetchHandler(object):
96
"""Base class for FetchHandler implementations in fetch plugins"""
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"
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))
108
def parse_url(self, url):
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)
25
118
def filter_installed_packages(packages):
26
119
"""Returns a list of packages that require installation"""
28
cache = apt_pkg.Cache()
30
122
for package in packages:
50
154
cmd.extend(packages)
51
155
log("Installing {} with options: {}".format(packages,
54
subprocess.check_call(cmd)
157
_run_apt_command(cmd, fatal)
160
def apt_upgrade(options=None, fatal=False, dist=False):
161
"""Upgrade all packages"""
163
options = ['--option=Dpkg::Options::=--force-confold']
165
cmd = ['apt-get', '--assume-yes']
168
cmd.append('dist-upgrade')
170
cmd.append('upgrade')
171
log("Upgrading with options: {}".format(options))
172
_run_apt_command(cmd, fatal)
59
175
def apt_update(fatal=False):
60
176
"""Update local apt cache"""
61
177
cmd = ['apt-get', 'update']
63
subprocess.check_call(cmd)
178
_run_apt_command(cmd, fatal)
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)
74
187
cmd.extend(packages)
75
188
log("Purging {}".format(packages))
189
_run_apt_command(cmd, fatal)
192
def apt_hold(packages, fatal=False):
193
"""Hold one or more packages"""
194
cmd = ['apt-mark', 'hold']
195
if isinstance(packages, basestring):
199
log("Holding {}".format(packages))
77
202
subprocess.check_call(cmd)
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.
210
@param source: a URL or sources.list entry, as supported by
211
add-apt-repository(1). Examples::
214
deb https://stub:key@private.example.com/ubuntu trusty main
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'
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.
231
log('Source is not present. Skipping')
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']),
89
242
pocket = source.split(':')[-1]
243
if pocket not in CLOUD_ARCHIVE_POCKETS:
244
raise SourceConfigError(
245
'Unsupported cloud: source option %s' %
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))
255
raise SourceConfigError("Unknown source: {!r}".format(source))
97
subprocess.check_call(['apt-key', 'import', key])
100
class SourceConfigError(Exception):
258
if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
259
with NamedTemporaryFile() as key_file:
263
subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
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',
104
273
def configure_sources(update=False,
105
274
sources_var='install_sources',
106
275
keys_var='install_keys'):
108
Configure multiple sources from charm configuration
277
Configure multiple sources from charm configuration.
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().
113
286
- "http://example.com/repo precise main"
118
291
Note that 'null' (a.k.a. None) should not be quoted.
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
296
if isinstance(sources, basestring):
300
for source in sources:
301
add_source(source, None)
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):
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)
131
312
apt_update(fatal=True)
133
# The order of this list is very important. Handlers should be listed in from
134
# least- to most-specific URL matching.
136
'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
137
'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
141
class UnhandledSource(Exception):
145
def install_remote(source):
315
def install_remote(source, *args, **kwargs):
147
317
Install a file tree from a remote source
149
319
The specified source should be a url of the form:
150
320
scheme://[host]/path[#[option=value][&...]]
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.
328
dest = install_remote('http://example.com/archive.tgz',
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`.
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:
160
installed_to = handler.install(source)
343
installed_to = handler.install(source, *args, **kwargs)
161
344
except UnhandledSource:
163
346
if not installed_to:
200
361
for handler_name in fetch_handlers:
201
362
package, classname = handler_name.rsplit('.', 1)
203
handler_class = getattr(importlib.import_module(package), classname)
364
handler_class = getattr(
365
importlib.import_module(package),
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(
209
373
return plugin_list
376
def _run_apt_command(cmd, fatal=False):
378
Run an APT command, checking output and retrying if the fatal flag is set
381
:param: cmd: str: The apt command to run.
382
:param: fatal: bool: Whether the command's output should be checked and
385
env = os.environ.copy()
387
if 'DEBIAN_FRONTEND' not in env:
388
env['DEBIAN_FRONTEND'] = 'noninteractive'
394
# If the command is considered "fatal", we need to retry if the apt
395
# lock was not acquired.
397
while result is None or result == APT_NO_LOCK:
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:
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)
410
subprocess.call(cmd, env=env)