54
56
'icehouse/proposed': 'precise-proposed/icehouse',
55
57
'precise-icehouse/proposed': 'precise-proposed/icehouse',
56
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',
75
'charmhelpers.fetch.giturl.GitUrlFetchHandler',
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.
83
class SourceConfigError(Exception):
87
class UnhandledSource(Exception):
91
class AptLockError(Exception):
95
class BaseFetchHandler(object):
97
"""Base class for FetchHandler implementations in fetch plugins"""
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"
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))
109
def parse_url(self, url):
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)
60
119
def filter_installed_packages(packages):
61
120
"""Returns a list of packages that require installation"""
63
cache = apt_pkg.Cache()
65
123
for package in packages:
110
171
cmd.append('upgrade')
111
172
log("Upgrading with options: {}".format(options))
113
env = os.environ.copy()
114
if 'DEBIAN_FRONTEND' not in env:
115
env['DEBIAN_FRONTEND'] = 'noninteractive'
118
subprocess.check_call(cmd, env=env)
120
subprocess.call(cmd, env=env)
173
_run_apt_command(cmd, fatal)
123
176
def apt_update(fatal=False):
124
177
"""Update local apt cache"""
125
178
cmd = ['apt-get', 'update']
127
subprocess.check_call(cmd)
179
_run_apt_command(cmd, fatal)
132
182
def apt_purge(packages, fatal=False):
160
208
def add_source(source, key=None):
209
"""Add a package source to this system.
211
@param source: a URL or sources.list entry, as supported by
212
add-apt-repository(1). Examples::
215
deb https://stub:key@private.example.com/ubuntu trusty main
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
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.
161
232
if source is None:
162
233
log('Source is not present. Skipping')
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':
259
log("Unknown source: {!r}".format(source))
186
subprocess.check_call(['apt-key', 'adv', '--keyserver',
187
'keyserver.ubuntu.com', '--recv',
191
class SourceConfigError(Exception):
262
if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
263
with NamedTemporaryFile() as key_file:
267
subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
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',
195
277
def configure_sources(update=False,
196
278
sources_var='install_sources',
197
279
keys_var='install_keys'):
199
Configure multiple sources from charm configuration
281
Configure multiple sources from charm configuration.
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().
204
290
- "http://example.com/repo precise main"
209
295
Note that 'null' (a.k.a. None) should not be quoted.
211
sources = safe_load(config(sources_var))
212
keys = config(keys_var)
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
300
if isinstance(sources, basestring):
304
for source in sources:
305
add_source(source, None)
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):
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)
225
316
apt_update(fatal=True)
227
# The order of this list is very important. Handlers should be listed in from
228
# least- to most-specific URL matching.
230
'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
231
'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
235
class UnhandledSource(Exception):
239
def install_remote(source):
319
def install_remote(source, *args, **kwargs):
241
321
Install a file tree from a remote source
243
323
The specified source should be a url of the form:
244
324
scheme://[host]/path[#[option=value][&...]]
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.
332
dest = install_remote('http://example.com/archive.tgz',
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`.
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:
254
installed_to = handler.install(source)
347
installed_to = handler.install(source, *args, **kwargs)
255
348
except UnhandledSource:
257
350
if not installed_to:
265
358
return install_remote(source)
268
class BaseFetchHandler(object):
270
"""Base class for FetchHandler implementations in fetch plugins"""
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"
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))
282
def parse_url(self, url):
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)
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(
308
377
return plugin_list
380
def _run_apt_command(cmd, fatal=False):
382
Run an APT command, checking output and retrying if the fatal flag is set
385
:param: cmd: str: The apt command to run.
386
:param: fatal: bool: Whether the command's output should be checked and
389
env = os.environ.copy()
391
if 'DEBIAN_FRONTEND' not in env:
392
env['DEBIAN_FRONTEND'] = 'noninteractive'
398
# If the command is considered "fatal", we need to retry if the apt
399
# lock was not acquired.
401
while result is None or result == APT_NO_LOCK:
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:
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)
414
subprocess.call(cmd, env=env)