~jorge/charms/precise/mysql/fix-metadata

« back to all changes in this revision

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

  • Committer: Edward Hope-Morley
  • Date: 2014-02-19 14:49:31 UTC
  • mto: This revision was merged to the branch mainline in revision 121.
  • Revision ID: edward.hope-morley@canonical.com-20140219144931-ujfwlf11fx2y55h4
[dosaboy] added support for 'source' and 'key' config options so that
          an alternative archive can be added to get more recent
          packages e.g. ceph packages from the coud archive.

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
import os
 
17
 
 
18
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
 
19
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
 
20
"""
 
21
PROPOSED_POCKET = """# Proposed
 
22
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
 
23
"""
 
24
CLOUD_ARCHIVE_POCKETS = {
 
25
    # Folsom
 
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',
 
33
    # Grizzly
 
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',
 
41
    # Havana
 
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
    'precise-havana/proposed': 'precise-proposed/havana',
 
48
    'precise-proposed/havana': 'precise-proposed/havana',
 
49
    # Icehouse
 
50
    'icehouse': 'precise-updates/icehouse',
 
51
    'precise-icehouse': 'precise-updates/icehouse',
 
52
    'precise-icehouse/updates': 'precise-updates/icehouse',
 
53
    'precise-updates/icehouse': 'precise-updates/icehouse',
 
54
    'icehouse/proposed': 'precise-proposed/icehouse',
 
55
    'precise-icehouse/proposed': 'precise-proposed/icehouse',
 
56
    'precise-proposed/icehouse': 'precise-proposed/icehouse',
 
57
}
 
58
 
 
59
 
 
60
def filter_installed_packages(packages):
 
61
    """Returns a list of packages that require installation"""
 
62
    apt_pkg.init()
 
63
    cache = apt_pkg.Cache()
 
64
    _pkgs = []
 
65
    for package in packages:
 
66
        try:
 
67
            p = cache[package]
 
68
            p.current_ver or _pkgs.append(package)
 
69
        except KeyError:
 
70
            log('Package {} has no installation candidate.'.format(package),
 
71
                level='WARNING')
 
72
            _pkgs.append(package)
 
73
    return _pkgs
 
74
 
 
75
 
 
76
def apt_install(packages, options=None, fatal=False):
 
77
    """Install one or more packages"""
 
78
    if options is None:
 
79
        options = ['--option=Dpkg::Options::=--force-confold']
 
80
 
 
81
    cmd = ['apt-get', '--assume-yes']
 
82
    cmd.extend(options)
 
83
    cmd.append('install')
 
84
    if isinstance(packages, basestring):
 
85
        cmd.append(packages)
 
86
    else:
 
87
        cmd.extend(packages)
 
88
    log("Installing {} with options: {}".format(packages,
 
89
                                                options))
 
90
    env = os.environ.copy()
 
91
    if 'DEBIAN_FRONTEND' not in env:
 
92
        env['DEBIAN_FRONTEND'] = 'noninteractive'
 
93
 
 
94
    if fatal:
 
95
        subprocess.check_call(cmd, env=env)
 
96
    else:
 
97
        subprocess.call(cmd, env=env)
 
98
 
 
99
 
 
100
def apt_update(fatal=False):
 
101
    """Update local apt cache"""
 
102
    cmd = ['apt-get', 'update']
 
103
    if fatal:
 
104
        subprocess.check_call(cmd)
 
105
    else:
 
106
        subprocess.call(cmd)
 
107
 
 
108
 
 
109
def apt_purge(packages, fatal=False):
 
110
    """Purge one or more packages"""
 
111
    cmd = ['apt-get', '--assume-yes', 'purge']
 
112
    if isinstance(packages, basestring):
 
113
        cmd.append(packages)
 
114
    else:
 
115
        cmd.extend(packages)
 
116
    log("Purging {}".format(packages))
 
117
    if fatal:
 
118
        subprocess.check_call(cmd)
 
119
    else:
 
120
        subprocess.call(cmd)
 
121
 
 
122
 
 
123
def apt_hold(packages, fatal=False):
 
124
    """Hold one or more packages"""
 
125
    cmd = ['apt-mark', 'hold']
 
126
    if isinstance(packages, basestring):
 
127
        cmd.append(packages)
 
128
    else:
 
129
        cmd.extend(packages)
 
130
    log("Holding {}".format(packages))
 
131
    if fatal:
 
132
        subprocess.check_call(cmd)
 
133
    else:
 
134
        subprocess.call(cmd)
 
135
 
 
136
 
 
137
def add_source(source, key=None):
 
138
    if (source.startswith('ppa:') or
 
139
        source.startswith('http') or
 
140
        source.startswith('deb ') or
 
141
            source.startswith('cloud-archive:')):
 
142
        subprocess.check_call(['add-apt-repository', '--yes', source])
 
143
    elif source.startswith('cloud:'):
 
144
        apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
 
145
                    fatal=True)
 
146
        pocket = source.split(':')[-1]
 
147
        if pocket not in CLOUD_ARCHIVE_POCKETS:
 
148
            raise SourceConfigError(
 
149
                'Unsupported cloud: source option %s' %
 
150
                pocket)
 
151
        actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
 
152
        with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
 
153
            apt.write(CLOUD_ARCHIVE.format(actual_pocket))
 
154
    elif source == 'proposed':
 
155
        release = lsb_release()['DISTRIB_CODENAME']
 
156
        with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
 
157
            apt.write(PROPOSED_POCKET.format(release))
 
158
    if key:
 
159
        subprocess.check_call(['apt-key', 'adv', '--keyserver',
 
160
                               'keyserver.ubuntu.com', '--recv',
 
161
                               key])
 
162
 
 
163
 
 
164
class SourceConfigError(Exception):
 
165
    pass
 
166
 
 
167
 
 
168
def configure_sources(update=False,
 
169
                      sources_var='install_sources',
 
170
                      keys_var='install_keys'):
 
171
    """
 
172
    Configure multiple sources from charm configuration
 
173
 
 
174
    Example config:
 
175
        install_sources:
 
176
          - "ppa:foo"
 
177
          - "http://example.com/repo precise main"
 
178
        install_keys:
 
179
          - null
 
180
          - "a1b2c3d4"
 
181
 
 
182
    Note that 'null' (a.k.a. None) should not be quoted.
 
183
    """
 
184
    sources = safe_load(config(sources_var))
 
185
    keys = config(keys_var)
 
186
    if keys is not None:
 
187
        keys = safe_load(keys)
 
188
    if isinstance(sources, basestring) and (
 
189
            keys is None or isinstance(keys, basestring)):
 
190
        add_source(sources, keys)
 
191
    else:
 
192
        if not len(sources) == len(keys):
 
193
            msg = 'Install sources and keys lists are different lengths'
 
194
            raise SourceConfigError(msg)
 
195
        for src_num in range(len(sources)):
 
196
            add_source(sources[src_num], keys[src_num])
 
197
    if update:
 
198
        apt_update(fatal=True)
 
199
 
 
200
# The order of this list is very important. Handlers should be listed in from
 
201
# least- to most-specific URL matching.
 
202
FETCH_HANDLERS = (
 
203
    'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
 
204
    'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
 
205
)
 
206
 
 
207
 
 
208
class UnhandledSource(Exception):
 
209
    pass
 
210
 
 
211
 
 
212
def install_remote(source):
 
213
    """
 
214
    Install a file tree from a remote source
 
215
 
 
216
    The specified source should be a url of the form:
 
217
        scheme://[host]/path[#[option=value][&...]]
 
218
 
 
219
    Schemes supported are based on this modules submodules
 
220
    Options supported are submodule-specific"""
 
221
    # We ONLY check for True here because can_handle may return a string
 
222
    # explaining why it can't handle a given source.
 
223
    handlers = [h for h in plugins() if h.can_handle(source) is True]
 
224
    installed_to = None
 
225
    for handler in handlers:
 
226
        try:
 
227
            installed_to = handler.install(source)
 
228
        except UnhandledSource:
 
229
            pass
 
230
    if not installed_to:
 
231
        raise UnhandledSource("No handler found for source {}".format(source))
 
232
    return installed_to
 
233
 
 
234
 
 
235
def install_from_config(config_var_name):
 
236
    charm_config = config()
 
237
    source = charm_config[config_var_name]
 
238
    return install_remote(source)
 
239
 
 
240
 
 
241
class BaseFetchHandler(object):
 
242
 
 
243
    """Base class for FetchHandler implementations in fetch plugins"""
 
244
 
 
245
    def can_handle(self, source):
 
246
        """Returns True if the source can be handled. Otherwise returns
 
247
        a string explaining why it cannot"""
 
248
        return "Wrong source type"
 
249
 
 
250
    def install(self, source):
 
251
        """Try to download and unpack the source. Return the path to the
 
252
        unpacked files or raise UnhandledSource."""
 
253
        raise UnhandledSource("Wrong source type {}".format(source))
 
254
 
 
255
    def parse_url(self, url):
 
256
        return urlparse(url)
 
257
 
 
258
    def base_url(self, url):
 
259
        """Return url without querystring or fragment"""
 
260
        parts = list(self.parse_url(url))
 
261
        parts[4:] = ['' for i in parts[4:]]
 
262
        return urlunparse(parts)
 
263
 
 
264
 
 
265
def plugins(fetch_handlers=None):
 
266
    if not fetch_handlers:
 
267
        fetch_handlers = FETCH_HANDLERS
 
268
    plugin_list = []
 
269
    for handler_name in fetch_handlers:
 
270
        package, classname = handler_name.rsplit('.', 1)
 
271
        try:
 
272
            handler_class = getattr(
 
273
                importlib.import_module(package),
 
274
                classname)
 
275
            plugin_list.append(handler_class())
 
276
        except (ImportError, AttributeError):
 
277
            # Skip missing plugins so that they can be ommitted from
 
278
            # installation if desired
 
279
            log("FetchHandler {} not found, skipping plugin".format(
 
280
                handler_name))
 
281
    return plugin_list