~gandelman-a/charms/precise/nova-vmware/trunk

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/fetch/__init__.py

  • Committer: Adam Gandelman
  • Date: 2013-10-09 00:52:24 UTC
  • Revision ID: adamg@canonical.com-20131009005224-ub1yu8wdkch45xka
Init new repo.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import importlib
 
2
from yaml import safe_load
 
3
from charmhelpers.core.host import (
 
4
    lsb_release
 
5
)
 
6
from urlparse import (
 
7
    urlparse,
 
8
    urlunparse,
 
9
)
 
10
import subprocess
 
11
from charmhelpers.core.hookenv import (
 
12
    config,
 
13
    log,
 
14
)
 
15
import apt_pkg
 
16
 
 
17
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
 
18
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
 
19
"""
 
20
PROPOSED_POCKET = """# Proposed
 
21
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
 
22
"""
 
23
 
 
24
 
 
25
def filter_installed_packages(packages):
 
26
    """Returns a list of packages that require installation"""
 
27
    apt_pkg.init()
 
28
    cache = apt_pkg.Cache()
 
29
    _pkgs = []
 
30
    for package in packages:
 
31
        try:
 
32
            p = cache[package]
 
33
            p.current_ver or _pkgs.append(package)
 
34
        except KeyError:
 
35
            log('Package {} has no installation candidate.'.format(package),
 
36
                level='WARNING')
 
37
            _pkgs.append(package)
 
38
    return _pkgs
 
39
 
 
40
 
 
41
def apt_install(packages, options=None, fatal=False):
 
42
    """Install one or more packages"""
 
43
    options = options or []
 
44
    cmd = ['apt-get', '-y']
 
45
    cmd.extend(options)
 
46
    cmd.append('install')
 
47
    if isinstance(packages, basestring):
 
48
        cmd.append(packages)
 
49
    else:
 
50
        cmd.extend(packages)
 
51
    log("Installing {} with options: {}".format(packages,
 
52
                                                options))
 
53
    if fatal:
 
54
        subprocess.check_call(cmd)
 
55
    else:
 
56
        subprocess.call(cmd)
 
57
 
 
58
 
 
59
def apt_update(fatal=False):
 
60
    """Update local apt cache"""
 
61
    cmd = ['apt-get', 'update']
 
62
    if fatal:
 
63
        subprocess.check_call(cmd)
 
64
    else:
 
65
        subprocess.call(cmd)
 
66
 
 
67
 
 
68
def apt_purge(packages, fatal=False):
 
69
    """Purge one or more packages"""
 
70
    cmd = ['apt-get', '-y', 'purge']
 
71
    if isinstance(packages, basestring):
 
72
        cmd.append(packages)
 
73
    else:
 
74
        cmd.extend(packages)
 
75
    log("Purging {}".format(packages))
 
76
    if fatal:
 
77
        subprocess.check_call(cmd)
 
78
    else:
 
79
        subprocess.call(cmd)
 
80
 
 
81
 
 
82
def add_source(source, key=None):
 
83
    if ((source.startswith('ppa:') or
 
84
         source.startswith('http:'))):
 
85
        subprocess.check_call(['add-apt-repository', '--yes', source])
 
86
    elif source.startswith('cloud:'):
 
87
        apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
 
88
                    fatal=True)
 
89
        pocket = source.split(':')[-1]
 
90
        with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
 
91
            apt.write(CLOUD_ARCHIVE.format(pocket))
 
92
    elif source == 'proposed':
 
93
        release = lsb_release()['DISTRIB_CODENAME']
 
94
        with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
 
95
            apt.write(PROPOSED_POCKET.format(release))
 
96
    if key:
 
97
        subprocess.check_call(['apt-key', 'import', key])
 
98
 
 
99
 
 
100
class SourceConfigError(Exception):
 
101
    pass
 
102
 
 
103
 
 
104
def configure_sources(update=False,
 
105
                      sources_var='install_sources',
 
106
                      keys_var='install_keys'):
 
107
    """
 
108
    Configure multiple sources from charm configuration
 
109
 
 
110
    Example config:
 
111
        install_sources:
 
112
          - "ppa:foo"
 
113
          - "http://example.com/repo precise main"
 
114
        install_keys:
 
115
          - null
 
116
          - "a1b2c3d4"
 
117
 
 
118
    Note that 'null' (a.k.a. None) should not be quoted.
 
119
    """
 
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)
 
124
    else:
 
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])
 
130
    if update:
 
131
        apt_update(fatal=True)
 
132
 
 
133
# The order of this list is very important. Handlers should be listed in from
 
134
# least- to most-specific URL matching.
 
135
FETCH_HANDLERS = (
 
136
    'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
 
137
    'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
 
138
)
 
139
 
 
140
 
 
141
class UnhandledSource(Exception):
 
142
    pass
 
143
 
 
144
 
 
145
def install_remote(source):
 
146
    """
 
147
    Install a file tree from a remote source
 
148
 
 
149
    The specified source should be a url of the form:
 
150
        scheme://[host]/path[#[option=value][&...]]
 
151
 
 
152
    Schemes supported are based on this modules submodules
 
153
    Options supported are submodule-specific"""
 
154
    # We ONLY check for True here because can_handle may return a string
 
155
    # explaining why it can't handle a given source.
 
156
    handlers = [h for h in plugins() if h.can_handle(source) is True]
 
157
    installed_to = None
 
158
    for handler in handlers:
 
159
        try:
 
160
            installed_to = handler.install(source)
 
161
        except UnhandledSource:
 
162
            pass
 
163
    if not installed_to:
 
164
        raise UnhandledSource("No handler found for source {}".format(source))
 
165
    return installed_to
 
166
 
 
167
 
 
168
def install_from_config(config_var_name):
 
169
    charm_config = config()
 
170
    source = charm_config[config_var_name]
 
171
    return install_remote(source)
 
172
 
 
173
 
 
174
class BaseFetchHandler(object):
 
175
    """Base class for FetchHandler implementations in fetch plugins"""
 
176
    def can_handle(self, source):
 
177
        """Returns True if the source can be handled. Otherwise returns
 
178
        a string explaining why it cannot"""
 
179
        return "Wrong source type"
 
180
 
 
181
    def install(self, source):
 
182
        """Try to download and unpack the source. Return the path to the
 
183
        unpacked files or raise UnhandledSource."""
 
184
        raise UnhandledSource("Wrong source type {}".format(source))
 
185
 
 
186
    def parse_url(self, url):
 
187
        return urlparse(url)
 
188
 
 
189
    def base_url(self, url):
 
190
        """Return url without querystring or fragment"""
 
191
        parts = list(self.parse_url(url))
 
192
        parts[4:] = ['' for i in parts[4:]]
 
193
        return urlunparse(parts)
 
194
 
 
195
 
 
196
def plugins(fetch_handlers=None):
 
197
    if not fetch_handlers:
 
198
        fetch_handlers = FETCH_HANDLERS
 
199
    plugin_list = []
 
200
    for handler_name in fetch_handlers:
 
201
        package, classname = handler_name.rsplit('.', 1)
 
202
        try:
 
203
            handler_class = getattr(importlib.import_module(package), classname)
 
204
            plugin_list.append(handler_class())
 
205
        except (ImportError, AttributeError):
 
206
            # Skip missing plugins so that they can be ommitted from
 
207
            # installation if desired
 
208
            log("FetchHandler {} not found, skipping plugin".format(handler_name))
 
209
    return plugin_list