~hopem/charms/precise/ci-configurator/relations-cleanup

« back to all changes in this revision

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

[hopem,r=wolsen]

synced ~canonical-ci/charm-helpers/trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
import importlib
 
2
import time
2
3
from yaml import safe_load
3
4
from charmhelpers.core.host import (
4
5
    lsb_release
13
14
    log,
14
15
)
15
16
import apt_pkg
 
17
import os
 
18
 
16
19
 
17
20
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
18
21
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
20
23
PROPOSED_POCKET = """# Proposed
21
24
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
22
25
"""
 
26
CLOUD_ARCHIVE_POCKETS = {
 
27
    # Folsom
 
28
    'folsom': 'precise-updates/folsom',
 
29
    'precise-folsom': 'precise-updates/folsom',
 
30
    'precise-folsom/updates': 'precise-updates/folsom',
 
31
    'precise-updates/folsom': 'precise-updates/folsom',
 
32
    'folsom/proposed': 'precise-proposed/folsom',
 
33
    'precise-folsom/proposed': 'precise-proposed/folsom',
 
34
    'precise-proposed/folsom': 'precise-proposed/folsom',
 
35
    # Grizzly
 
36
    'grizzly': 'precise-updates/grizzly',
 
37
    'precise-grizzly': 'precise-updates/grizzly',
 
38
    'precise-grizzly/updates': 'precise-updates/grizzly',
 
39
    'precise-updates/grizzly': 'precise-updates/grizzly',
 
40
    'grizzly/proposed': 'precise-proposed/grizzly',
 
41
    'precise-grizzly/proposed': 'precise-proposed/grizzly',
 
42
    'precise-proposed/grizzly': 'precise-proposed/grizzly',
 
43
    # Havana
 
44
    'havana': 'precise-updates/havana',
 
45
    'precise-havana': 'precise-updates/havana',
 
46
    'precise-havana/updates': 'precise-updates/havana',
 
47
    'precise-updates/havana': 'precise-updates/havana',
 
48
    'havana/proposed': 'precise-proposed/havana',
 
49
    'precise-havana/proposed': 'precise-proposed/havana',
 
50
    'precise-proposed/havana': 'precise-proposed/havana',
 
51
    # Icehouse
 
52
    'icehouse': 'precise-updates/icehouse',
 
53
    'precise-icehouse': 'precise-updates/icehouse',
 
54
    'precise-icehouse/updates': 'precise-updates/icehouse',
 
55
    'precise-updates/icehouse': 'precise-updates/icehouse',
 
56
    'icehouse/proposed': 'precise-proposed/icehouse',
 
57
    'precise-icehouse/proposed': 'precise-proposed/icehouse',
 
58
    'precise-proposed/icehouse': 'precise-proposed/icehouse',
 
59
}
 
60
 
 
61
# The order of this list is very important. Handlers should be listed in from
 
62
# least- to most-specific URL matching.
 
63
FETCH_HANDLERS = (
 
64
    'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
 
65
    'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
 
66
)
 
67
 
 
68
APT_NO_LOCK = 100  # The return code for "couldn't acquire lock" in APT.
 
69
APT_NO_LOCK_RETRY_DELAY = 10  # Wait 10 seconds between apt lock checks.
 
70
APT_NO_LOCK_RETRY_COUNT = 30  # Retry to acquire the lock X times.
 
71
 
 
72
 
 
73
class SourceConfigError(Exception):
 
74
    pass
 
75
 
 
76
 
 
77
class UnhandledSource(Exception):
 
78
    pass
 
79
 
 
80
 
 
81
class AptLockError(Exception):
 
82
    pass
 
83
 
 
84
 
 
85
class BaseFetchHandler(object):
 
86
 
 
87
    """Base class for FetchHandler implementations in fetch plugins"""
 
88
 
 
89
    def can_handle(self, source):
 
90
        """Returns True if the source can be handled. Otherwise returns
 
91
        a string explaining why it cannot"""
 
92
        return "Wrong source type"
 
93
 
 
94
    def install(self, source):
 
95
        """Try to download and unpack the source. Return the path to the
 
96
        unpacked files or raise UnhandledSource."""
 
97
        raise UnhandledSource("Wrong source type {}".format(source))
 
98
 
 
99
    def parse_url(self, url):
 
100
        return urlparse(url)
 
101
 
 
102
    def base_url(self, url):
 
103
        """Return url without querystring or fragment"""
 
104
        parts = list(self.parse_url(url))
 
105
        parts[4:] = ['' for i in parts[4:]]
 
106
        return urlunparse(parts)
23
107
 
24
108
 
25
109
def filter_installed_packages(packages):
26
110
    """Returns a list of packages that require installation"""
27
111
    apt_pkg.init()
 
112
 
 
113
    # Tell apt to build an in-memory cache to prevent race conditions (if
 
114
    # another process is already building the cache).
 
115
    apt_pkg.config.set("Dir::Cache::pkgcache", "")
 
116
 
28
117
    cache = apt_pkg.Cache()
29
118
    _pkgs = []
30
119
    for package in packages:
40
129
 
41
130
def apt_install(packages, options=None, fatal=False):
42
131
    """Install one or more packages"""
43
 
    options = options or []
44
 
    cmd = ['apt-get', '-y']
 
132
    if options is None:
 
133
        options = ['--option=Dpkg::Options::=--force-confold']
 
134
 
 
135
    cmd = ['apt-get', '--assume-yes']
45
136
    cmd.extend(options)
46
137
    cmd.append('install')
47
138
    if isinstance(packages, basestring):
50
141
        cmd.extend(packages)
51
142
    log("Installing {} with options: {}".format(packages,
52
143
                                                options))
53
 
    if fatal:
54
 
        subprocess.check_call(cmd)
 
144
    _run_apt_command(cmd, fatal)
 
145
 
 
146
 
 
147
def apt_upgrade(options=None, fatal=False, dist=False):
 
148
    """Upgrade all packages"""
 
149
    if options is None:
 
150
        options = ['--option=Dpkg::Options::=--force-confold']
 
151
 
 
152
    cmd = ['apt-get', '--assume-yes']
 
153
    cmd.extend(options)
 
154
    if dist:
 
155
        cmd.append('dist-upgrade')
55
156
    else:
56
 
        subprocess.call(cmd)
 
157
        cmd.append('upgrade')
 
158
    log("Upgrading with options: {}".format(options))
 
159
    _run_apt_command(cmd, fatal)
57
160
 
58
161
 
59
162
def apt_update(fatal=False):
60
163
    """Update local apt cache"""
61
164
    cmd = ['apt-get', 'update']
62
 
    if fatal:
63
 
        subprocess.check_call(cmd)
64
 
    else:
65
 
        subprocess.call(cmd)
 
165
    _run_apt_command(cmd, fatal)
66
166
 
67
167
 
68
168
def apt_purge(packages, fatal=False):
69
169
    """Purge one or more packages"""
70
 
    cmd = ['apt-get', '-y', 'purge']
 
170
    cmd = ['apt-get', '--assume-yes', 'purge']
71
171
    if isinstance(packages, basestring):
72
172
        cmd.append(packages)
73
173
    else:
74
174
        cmd.extend(packages)
75
175
    log("Purging {}".format(packages))
 
176
    _run_apt_command(cmd, fatal)
 
177
 
 
178
 
 
179
def apt_hold(packages, fatal=False):
 
180
    """Hold one or more packages"""
 
181
    cmd = ['apt-mark', 'hold']
 
182
    if isinstance(packages, basestring):
 
183
        cmd.append(packages)
 
184
    else:
 
185
        cmd.extend(packages)
 
186
    log("Holding {}".format(packages))
 
187
 
76
188
    if fatal:
77
189
        subprocess.check_call(cmd)
78
190
    else:
80
192
 
81
193
 
82
194
def add_source(source, key=None):
83
 
    if ((source.startswith('ppa:') or
84
 
         source.startswith('http:'))):
 
195
    if source is None:
 
196
        log('Source is not present. Skipping')
 
197
        return
 
198
 
 
199
    if (source.startswith('ppa:') or
 
200
        source.startswith('http') or
 
201
        source.startswith('deb ') or
 
202
            source.startswith('cloud-archive:')):
85
203
        subprocess.check_call(['add-apt-repository', '--yes', source])
86
204
    elif source.startswith('cloud:'):
87
205
        apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
88
206
                    fatal=True)
89
207
        pocket = source.split(':')[-1]
 
208
        if pocket not in CLOUD_ARCHIVE_POCKETS:
 
209
            raise SourceConfigError(
 
210
                'Unsupported cloud: source option %s' %
 
211
                pocket)
 
212
        actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
90
213
        with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
91
 
            apt.write(CLOUD_ARCHIVE.format(pocket))
 
214
            apt.write(CLOUD_ARCHIVE.format(actual_pocket))
92
215
    elif source == 'proposed':
93
216
        release = lsb_release()['DISTRIB_CODENAME']
94
217
        with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
95
218
            apt.write(PROPOSED_POCKET.format(release))
96
219
    if key:
97
 
        subprocess.check_call(['apt-key', 'import', key])
98
 
 
99
 
 
100
 
class SourceConfigError(Exception):
101
 
    pass
 
220
        subprocess.check_call(['apt-key', 'adv', '--keyserver',
 
221
                               'hkp://keyserver.ubuntu.com:80', '--recv',
 
222
                               key])
102
223
 
103
224
 
104
225
def configure_sources(update=False,
118
239
    Note that 'null' (a.k.a. None) should not be quoted.
119
240
    """
120
241
    sources = safe_load(config(sources_var))
121
 
    keys = safe_load(config(keys_var))
122
 
    if isinstance(sources, basestring) and isinstance(keys, basestring):
 
242
    keys = config(keys_var)
 
243
    if keys is not None:
 
244
        keys = safe_load(keys)
 
245
    if isinstance(sources, basestring) and (
 
246
            keys is None or isinstance(keys, basestring)):
123
247
        add_source(sources, keys)
124
248
    else:
125
249
        if not len(sources) == len(keys):
130
254
    if update:
131
255
        apt_update(fatal=True)
132
256
 
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
257
 
145
258
def install_remote(source):
146
259
    """
171
284
    return install_remote(source)
172
285
 
173
286
 
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
287
def plugins(fetch_handlers=None):
197
288
    if not fetch_handlers:
198
289
        fetch_handlers = FETCH_HANDLERS
200
291
    for handler_name in fetch_handlers:
201
292
        package, classname = handler_name.rsplit('.', 1)
202
293
        try:
203
 
            handler_class = getattr(importlib.import_module(package), classname)
 
294
            handler_class = getattr(
 
295
                importlib.import_module(package),
 
296
                classname)
204
297
            plugin_list.append(handler_class())
205
298
        except (ImportError, AttributeError):
206
299
            # Skip missing plugins so that they can be ommitted from
207
300
            # installation if desired
208
 
            log("FetchHandler {} not found, skipping plugin".format(handler_name))
 
301
            log("FetchHandler {} not found, skipping plugin".format(
 
302
                handler_name))
209
303
    return plugin_list
 
304
 
 
305
 
 
306
def _run_apt_command(cmd, fatal=False):
 
307
    """
 
308
    Run an APT command, checking output and retrying if the fatal flag is set
 
309
    to True.
 
310
 
 
311
    :param: cmd: str: The apt command to run.
 
312
    :param: fatal: bool: Whether the command's output should be checked and
 
313
        retried.
 
314
    """
 
315
    env = os.environ.copy()
 
316
 
 
317
    if 'DEBIAN_FRONTEND' not in env:
 
318
        env['DEBIAN_FRONTEND'] = 'noninteractive'
 
319
 
 
320
    if fatal:
 
321
        retry_count = 0
 
322
        result = None
 
323
 
 
324
        # If the command is considered "fatal", we need to retry if the apt
 
325
        # lock was not acquired.
 
326
 
 
327
        while result is None or result == APT_NO_LOCK:
 
328
            try:
 
329
                result = subprocess.check_call(cmd, env=env)
 
330
            except subprocess.CalledProcessError, e:
 
331
                retry_count = retry_count + 1
 
332
                if retry_count > APT_NO_LOCK_RETRY_COUNT:
 
333
                    raise
 
334
                result = e.returncode
 
335
                log("Couldn't acquire DPKG lock. Will retry in {} seconds."
 
336
                    "".format(APT_NO_LOCK_RETRY_DELAY))
 
337
                time.sleep(APT_NO_LOCK_RETRY_DELAY)
 
338
 
 
339
    else:
 
340
        subprocess.call(cmd, env=env)