309 lines
9.6 KiB
Python
309 lines
9.6 KiB
Python
import importlib
|
|
from yaml import safe_load
|
|
from charmhelpers.core.host import (
|
|
lsb_release
|
|
)
|
|
from urlparse import (
|
|
urlparse,
|
|
urlunparse,
|
|
)
|
|
import subprocess
|
|
from charmhelpers.core.hookenv import (
|
|
config,
|
|
log,
|
|
)
|
|
import apt_pkg
|
|
import os
|
|
|
|
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
|
|
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
|
|
"""
|
|
PROPOSED_POCKET = """# Proposed
|
|
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
|
|
"""
|
|
CLOUD_ARCHIVE_POCKETS = {
|
|
# Folsom
|
|
'folsom': 'precise-updates/folsom',
|
|
'precise-folsom': 'precise-updates/folsom',
|
|
'precise-folsom/updates': 'precise-updates/folsom',
|
|
'precise-updates/folsom': 'precise-updates/folsom',
|
|
'folsom/proposed': 'precise-proposed/folsom',
|
|
'precise-folsom/proposed': 'precise-proposed/folsom',
|
|
'precise-proposed/folsom': 'precise-proposed/folsom',
|
|
# Grizzly
|
|
'grizzly': 'precise-updates/grizzly',
|
|
'precise-grizzly': 'precise-updates/grizzly',
|
|
'precise-grizzly/updates': 'precise-updates/grizzly',
|
|
'precise-updates/grizzly': 'precise-updates/grizzly',
|
|
'grizzly/proposed': 'precise-proposed/grizzly',
|
|
'precise-grizzly/proposed': 'precise-proposed/grizzly',
|
|
'precise-proposed/grizzly': 'precise-proposed/grizzly',
|
|
# Havana
|
|
'havana': 'precise-updates/havana',
|
|
'precise-havana': 'precise-updates/havana',
|
|
'precise-havana/updates': 'precise-updates/havana',
|
|
'precise-updates/havana': 'precise-updates/havana',
|
|
'havana/proposed': 'precise-proposed/havana',
|
|
'precise-havana/proposed': 'precise-proposed/havana',
|
|
'precise-proposed/havana': 'precise-proposed/havana',
|
|
# Icehouse
|
|
'icehouse': 'precise-updates/icehouse',
|
|
'precise-icehouse': 'precise-updates/icehouse',
|
|
'precise-icehouse/updates': 'precise-updates/icehouse',
|
|
'precise-updates/icehouse': 'precise-updates/icehouse',
|
|
'icehouse/proposed': 'precise-proposed/icehouse',
|
|
'precise-icehouse/proposed': 'precise-proposed/icehouse',
|
|
'precise-proposed/icehouse': 'precise-proposed/icehouse',
|
|
}
|
|
|
|
|
|
def filter_installed_packages(packages):
|
|
"""Returns a list of packages that require installation"""
|
|
apt_pkg.init()
|
|
cache = apt_pkg.Cache()
|
|
_pkgs = []
|
|
for package in packages:
|
|
try:
|
|
p = cache[package]
|
|
p.current_ver or _pkgs.append(package)
|
|
except KeyError:
|
|
log('Package {} has no installation candidate.'.format(package),
|
|
level='WARNING')
|
|
_pkgs.append(package)
|
|
return _pkgs
|
|
|
|
|
|
def apt_install(packages, options=None, fatal=False):
|
|
"""Install one or more packages"""
|
|
if options is None:
|
|
options = ['--option=Dpkg::Options::=--force-confold']
|
|
|
|
cmd = ['apt-get', '--assume-yes']
|
|
cmd.extend(options)
|
|
cmd.append('install')
|
|
if isinstance(packages, basestring):
|
|
cmd.append(packages)
|
|
else:
|
|
cmd.extend(packages)
|
|
log("Installing {} with options: {}".format(packages,
|
|
options))
|
|
env = os.environ.copy()
|
|
if 'DEBIAN_FRONTEND' not in env:
|
|
env['DEBIAN_FRONTEND'] = 'noninteractive'
|
|
|
|
if fatal:
|
|
subprocess.check_call(cmd, env=env)
|
|
else:
|
|
subprocess.call(cmd, env=env)
|
|
|
|
|
|
def apt_upgrade(options=None, fatal=False, dist=False):
|
|
"""Upgrade all packages"""
|
|
if options is None:
|
|
options = ['--option=Dpkg::Options::=--force-confold']
|
|
|
|
cmd = ['apt-get', '--assume-yes']
|
|
cmd.extend(options)
|
|
if dist:
|
|
cmd.append('dist-upgrade')
|
|
else:
|
|
cmd.append('upgrade')
|
|
log("Upgrading with options: {}".format(options))
|
|
|
|
env = os.environ.copy()
|
|
if 'DEBIAN_FRONTEND' not in env:
|
|
env['DEBIAN_FRONTEND'] = 'noninteractive'
|
|
|
|
if fatal:
|
|
subprocess.check_call(cmd, env=env)
|
|
else:
|
|
subprocess.call(cmd, env=env)
|
|
|
|
|
|
def apt_update(fatal=False):
|
|
"""Update local apt cache"""
|
|
cmd = ['apt-get', 'update']
|
|
if fatal:
|
|
subprocess.check_call(cmd)
|
|
else:
|
|
subprocess.call(cmd)
|
|
|
|
|
|
def apt_purge(packages, fatal=False):
|
|
"""Purge one or more packages"""
|
|
cmd = ['apt-get', '--assume-yes', 'purge']
|
|
if isinstance(packages, basestring):
|
|
cmd.append(packages)
|
|
else:
|
|
cmd.extend(packages)
|
|
log("Purging {}".format(packages))
|
|
if fatal:
|
|
subprocess.check_call(cmd)
|
|
else:
|
|
subprocess.call(cmd)
|
|
|
|
|
|
def apt_hold(packages, fatal=False):
|
|
"""Hold one or more packages"""
|
|
cmd = ['apt-mark', 'hold']
|
|
if isinstance(packages, basestring):
|
|
cmd.append(packages)
|
|
else:
|
|
cmd.extend(packages)
|
|
log("Holding {}".format(packages))
|
|
if fatal:
|
|
subprocess.check_call(cmd)
|
|
else:
|
|
subprocess.call(cmd)
|
|
|
|
|
|
def add_source(source, key=None):
|
|
if source is None:
|
|
log('Source is not present. Skipping')
|
|
return
|
|
|
|
if (source.startswith('ppa:') or
|
|
source.startswith('http') or
|
|
source.startswith('deb ') or
|
|
source.startswith('cloud-archive:')):
|
|
subprocess.check_call(['add-apt-repository', '--yes', source])
|
|
elif source.startswith('cloud:'):
|
|
apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
|
|
fatal=True)
|
|
pocket = source.split(':')[-1]
|
|
if pocket not in CLOUD_ARCHIVE_POCKETS:
|
|
raise SourceConfigError(
|
|
'Unsupported cloud: source option %s' %
|
|
pocket)
|
|
actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
|
|
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
|
|
apt.write(CLOUD_ARCHIVE.format(actual_pocket))
|
|
elif source == 'proposed':
|
|
release = lsb_release()['DISTRIB_CODENAME']
|
|
with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
|
|
apt.write(PROPOSED_POCKET.format(release))
|
|
if key:
|
|
subprocess.check_call(['apt-key', 'adv', '--keyserver',
|
|
'keyserver.ubuntu.com', '--recv',
|
|
key])
|
|
|
|
|
|
class SourceConfigError(Exception):
|
|
pass
|
|
|
|
|
|
def configure_sources(update=False,
|
|
sources_var='install_sources',
|
|
keys_var='install_keys'):
|
|
"""
|
|
Configure multiple sources from charm configuration
|
|
|
|
Example config:
|
|
install_sources:
|
|
- "ppa:foo"
|
|
- "http://example.com/repo precise main"
|
|
install_keys:
|
|
- null
|
|
- "a1b2c3d4"
|
|
|
|
Note that 'null' (a.k.a. None) should not be quoted.
|
|
"""
|
|
sources = safe_load(config(sources_var))
|
|
keys = config(keys_var)
|
|
if keys is not None:
|
|
keys = safe_load(keys)
|
|
if isinstance(sources, basestring) and (
|
|
keys is None or isinstance(keys, basestring)):
|
|
add_source(sources, keys)
|
|
else:
|
|
if not len(sources) == len(keys):
|
|
msg = 'Install sources and keys lists are different lengths'
|
|
raise SourceConfigError(msg)
|
|
for src_num in range(len(sources)):
|
|
add_source(sources[src_num], keys[src_num])
|
|
if update:
|
|
apt_update(fatal=True)
|
|
|
|
# The order of this list is very important. Handlers should be listed in from
|
|
# least- to most-specific URL matching.
|
|
FETCH_HANDLERS = (
|
|
'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
|
|
'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
|
|
)
|
|
|
|
|
|
class UnhandledSource(Exception):
|
|
pass
|
|
|
|
|
|
def install_remote(source):
|
|
"""
|
|
Install a file tree from a remote source
|
|
|
|
The specified source should be a url of the form:
|
|
scheme://[host]/path[#[option=value][&...]]
|
|
|
|
Schemes supported are based on this modules submodules
|
|
Options supported are submodule-specific"""
|
|
# We ONLY check for True here because can_handle may return a string
|
|
# explaining why it can't handle a given source.
|
|
handlers = [h for h in plugins() if h.can_handle(source) is True]
|
|
installed_to = None
|
|
for handler in handlers:
|
|
try:
|
|
installed_to = handler.install(source)
|
|
except UnhandledSource:
|
|
pass
|
|
if not installed_to:
|
|
raise UnhandledSource("No handler found for source {}".format(source))
|
|
return installed_to
|
|
|
|
|
|
def install_from_config(config_var_name):
|
|
charm_config = config()
|
|
source = charm_config[config_var_name]
|
|
return install_remote(source)
|
|
|
|
|
|
class BaseFetchHandler(object):
|
|
|
|
"""Base class for FetchHandler implementations in fetch plugins"""
|
|
|
|
def can_handle(self, source):
|
|
"""Returns True if the source can be handled. Otherwise returns
|
|
a string explaining why it cannot"""
|
|
return "Wrong source type"
|
|
|
|
def install(self, source):
|
|
"""Try to download and unpack the source. Return the path to the
|
|
unpacked files or raise UnhandledSource."""
|
|
raise UnhandledSource("Wrong source type {}".format(source))
|
|
|
|
def parse_url(self, url):
|
|
return urlparse(url)
|
|
|
|
def base_url(self, url):
|
|
"""Return url without querystring or fragment"""
|
|
parts = list(self.parse_url(url))
|
|
parts[4:] = ['' for i in parts[4:]]
|
|
return urlunparse(parts)
|
|
|
|
|
|
def plugins(fetch_handlers=None):
|
|
if not fetch_handlers:
|
|
fetch_handlers = FETCH_HANDLERS
|
|
plugin_list = []
|
|
for handler_name in fetch_handlers:
|
|
package, classname = handler_name.rsplit('.', 1)
|
|
try:
|
|
handler_class = getattr(
|
|
importlib.import_module(package),
|
|
classname)
|
|
plugin_list.append(handler_class())
|
|
except (ImportError, AttributeError):
|
|
# Skip missing plugins so that they can be ommitted from
|
|
# installation if desired
|
|
log("FetchHandler {} not found, skipping plugin".format(
|
|
handler_name))
|
|
return plugin_list
|