2
from yaml import safe_load
3
from charmhelpers.core.host import (
11
from charmhelpers.core.hookenv import (
18
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
19
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
21
PROPOSED_POCKET = """# Proposed
22
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
24
CLOUD_ARCHIVE_POCKETS = {
26
'folsom': 'precise-updates/folsom',
27
'precise-folsom': 'precise-updates/folsom',
28
'precise-folsom/updates': 'precise-updates/folsom',
29
'precise-updates/folsom': 'precise-updates/folsom',
30
'folsom/proposed': 'precise-proposed/folsom',
31
'precise-folsom/proposed': 'precise-proposed/folsom',
32
'precise-proposed/folsom': 'precise-proposed/folsom',
34
'grizzly': 'precise-updates/grizzly',
35
'precise-grizzly': 'precise-updates/grizzly',
36
'precise-grizzly/updates': 'precise-updates/grizzly',
37
'precise-updates/grizzly': 'precise-updates/grizzly',
38
'grizzly/proposed': 'precise-proposed/grizzly',
39
'precise-grizzly/proposed': 'precise-proposed/grizzly',
40
'precise-proposed/grizzly': 'precise-proposed/grizzly',
42
'havana': 'precise-updates/havana',
43
'precise-havana': 'precise-updates/havana',
44
'precise-havana/updates': 'precise-updates/havana',
45
'precise-updates/havana': 'precise-updates/havana',
46
'havana/proposed': 'precise-proposed/havana',
47
'precies-havana/proposed': 'precise-proposed/havana',
48
'precise-proposed/havana': 'precise-proposed/havana',
52
def filter_installed_packages(packages):
53
"""Returns a list of packages that require installation"""
55
cache = apt_pkg.Cache()
57
for package in packages:
60
p.current_ver or _pkgs.append(package)
62
log('Package {} has no installation candidate.'.format(package),
68
def apt_install(packages, options=None, fatal=False):
69
"""Install one or more packages"""
71
options = ['--option=Dpkg::Options::=--force-confold']
73
cmd = ['apt-get', '--assume-yes']
76
if isinstance(packages, basestring):
80
log("Installing {} with options: {}".format(packages,
82
env = os.environ.copy()
83
if 'DEBIAN_FRONTEND' not in env:
84
env['DEBIAN_FRONTEND'] = 'noninteractive'
87
subprocess.check_call(cmd, env=env)
89
subprocess.call(cmd, env=env)
92
def apt_update(fatal=False):
93
"""Update local apt cache"""
94
cmd = ['apt-get', 'update']
96
subprocess.check_call(cmd)
101
def apt_purge(packages, fatal=False):
102
"""Purge one or more packages"""
103
cmd = ['apt-get', '--assume-yes', 'purge']
104
if isinstance(packages, basestring):
108
log("Purging {}".format(packages))
110
subprocess.check_call(cmd)
115
def apt_hold(packages, fatal=False):
116
"""Hold one or more packages"""
117
cmd = ['apt-mark', 'hold']
118
if isinstance(packages, basestring):
122
log("Holding {}".format(packages))
124
subprocess.check_call(cmd)
129
def add_source(source, key=None):
130
if (source.startswith('ppa:') or
131
source.startswith('http:') or
132
source.startswith('deb ') or
133
source.startswith('cloud-archive:')):
134
subprocess.check_call(['add-apt-repository', '--yes', source])
135
elif source.startswith('cloud:'):
136
apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
138
pocket = source.split(':')[-1]
139
if pocket not in CLOUD_ARCHIVE_POCKETS:
140
raise SourceConfigError(
141
'Unsupported cloud: source option %s' %
143
actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
144
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
145
apt.write(CLOUD_ARCHIVE.format(actual_pocket))
146
elif source == 'proposed':
147
release = lsb_release()['DISTRIB_CODENAME']
148
with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
149
apt.write(PROPOSED_POCKET.format(release))
151
subprocess.check_call(['apt-key', 'import', key])
154
class SourceConfigError(Exception):
158
def configure_sources(update=False,
159
sources_var='install_sources',
160
keys_var='install_keys'):
162
Configure multiple sources from charm configuration
167
- "http://example.com/repo precise main"
172
Note that 'null' (a.k.a. None) should not be quoted.
174
sources = safe_load(config(sources_var))
175
keys = config(keys_var)
177
keys = safe_load(keys)
178
if isinstance(sources, basestring) and (
179
keys is None or isinstance(keys, basestring)):
180
add_source(sources, keys)
182
if not len(sources) == len(keys):
183
msg = 'Install sources and keys lists are different lengths'
184
raise SourceConfigError(msg)
185
for src_num in range(len(sources)):
186
add_source(sources[src_num], keys[src_num])
188
apt_update(fatal=True)
190
# The order of this list is very important. Handlers should be listed in from
191
# least- to most-specific URL matching.
193
'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
194
'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
198
class UnhandledSource(Exception):
202
def install_remote(source):
204
Install a file tree from a remote source
206
The specified source should be a url of the form:
207
scheme://[host]/path[#[option=value][&...]]
209
Schemes supported are based on this modules submodules
210
Options supported are submodule-specific"""
211
# We ONLY check for True here because can_handle may return a string
212
# explaining why it can't handle a given source.
213
handlers = [h for h in plugins() if h.can_handle(source) is True]
215
for handler in handlers:
217
installed_to = handler.install(source)
218
except UnhandledSource:
221
raise UnhandledSource("No handler found for source {}".format(source))
225
def install_from_config(config_var_name):
226
charm_config = config()
227
source = charm_config[config_var_name]
228
return install_remote(source)
231
class BaseFetchHandler(object):
233
"""Base class for FetchHandler implementations in fetch plugins"""
235
def can_handle(self, source):
236
"""Returns True if the source can be handled. Otherwise returns
237
a string explaining why it cannot"""
238
return "Wrong source type"
240
def install(self, source):
241
"""Try to download and unpack the source. Return the path to the
242
unpacked files or raise UnhandledSource."""
243
raise UnhandledSource("Wrong source type {}".format(source))
245
def parse_url(self, url):
248
def base_url(self, url):
249
"""Return url without querystring or fragment"""
250
parts = list(self.parse_url(url))
251
parts[4:] = ['' for i in parts[4:]]
252
return urlunparse(parts)
255
def plugins(fetch_handlers=None):
256
if not fetch_handlers:
257
fetch_handlers = FETCH_HANDLERS
259
for handler_name in fetch_handlers:
260
package, classname = handler_name.rsplit('.', 1)
262
handler_class = getattr(
263
importlib.import_module(package),
265
plugin_list.append(handler_class())
266
except (ImportError, AttributeError):
267
# Skip missing plugins so that they can be ommitted from
268
# installation if desired
269
log("FetchHandler {} not found, skipping plugin".format(