~brad-marshall/charms/trusty/apache2-wsgi/fix-haproxy-relations

« back to all changes in this revision

Viewing changes to hooks/lib/charmhelpers/contrib/jujugui/utils.py

  • Committer: Robin Winslow
  • Date: 2014-05-27 14:00:44 UTC
  • Revision ID: robin.winslow@canonical.com-20140527140044-8rpmb3wx4djzwa83
Add all files

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""Juju GUI charm utilities."""
 
2
 
 
3
__all__ = [
 
4
    'AGENT',
 
5
    'APACHE',
 
6
    'API_PORT',
 
7
    'CURRENT_DIR',
 
8
    'HAPROXY',
 
9
    'IMPROV',
 
10
    'JUJU_DIR',
 
11
    'JUJU_GUI_DIR',
 
12
    'JUJU_GUI_SITE',
 
13
    'JUJU_PEM',
 
14
    'WEB_PORT',
 
15
    'bzr_checkout',
 
16
    'chain',
 
17
    'cmd_log',
 
18
    'fetch_api',
 
19
    'fetch_gui',
 
20
    'find_missing_packages',
 
21
    'first_path_in_dir',
 
22
    'get_api_address',
 
23
    'get_npm_cache_archive_url',
 
24
    'get_release_file_url',
 
25
    'get_staging_dependencies',
 
26
    'get_zookeeper_address',
 
27
    'legacy_juju',
 
28
    'log_hook',
 
29
    'merge',
 
30
    'parse_source',
 
31
    'prime_npm_cache',
 
32
    'render_to_file',
 
33
    'save_or_create_certificates',
 
34
    'setup_apache',
 
35
    'setup_gui',
 
36
    'start_agent',
 
37
    'start_gui',
 
38
    'start_improv',
 
39
    'write_apache_config',
 
40
]
 
41
 
 
42
from contextlib import contextmanager
 
43
import errno
 
44
import json
 
45
import os
 
46
import logging
 
47
import shutil
 
48
from subprocess import CalledProcessError
 
49
import tempfile
 
50
from urlparse import urlparse
 
51
 
 
52
import apt
 
53
import tempita
 
54
 
 
55
from launchpadlib.launchpad import Launchpad
 
56
from shelltoolbox import (
 
57
    Serializer,
 
58
    apt_get_install,
 
59
    command,
 
60
    environ,
 
61
    install_extra_repositories,
 
62
    run,
 
63
    script_name,
 
64
    search_file,
 
65
    su,
 
66
)
 
67
from charmhelpers.core.host import (
 
68
    service_start,
 
69
)
 
70
from charmhelpers.core.hookenv import (
 
71
    log,
 
72
    config,
 
73
    unit_get,
 
74
)
 
75
 
 
76
 
 
77
AGENT = 'juju-api-agent'
 
78
APACHE = 'apache2'
 
79
IMPROV = 'juju-api-improv'
 
80
HAPROXY = 'haproxy'
 
81
 
 
82
API_PORT = 8080
 
83
WEB_PORT = 8000
 
84
 
 
85
CURRENT_DIR = os.getcwd()
 
86
JUJU_DIR = os.path.join(CURRENT_DIR, 'juju')
 
87
JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui')
 
88
JUJU_GUI_SITE = '/etc/apache2/sites-available/juju-gui'
 
89
JUJU_GUI_PORTS = '/etc/apache2/ports.conf'
 
90
JUJU_PEM = 'juju.includes-private-key.pem'
 
91
BUILD_REPOSITORIES = ('ppa:chris-lea/node.js-legacy',)
 
92
DEB_BUILD_DEPENDENCIES = (
 
93
    'bzr', 'imagemagick', 'make', 'nodejs', 'npm',
 
94
)
 
95
DEB_STAGE_DEPENDENCIES = (
 
96
    'zookeeper',
 
97
)
 
98
 
 
99
 
 
100
# Store the configuration from on invocation to the next.
 
101
config_json = Serializer('/tmp/config.json')
 
102
# Bazaar checkout command.
 
103
bzr_checkout = command('bzr', 'co', '--lightweight')
 
104
# Whether or not the charm is deployed using juju-core.
 
105
# If juju-core has been used to deploy the charm, an agent.conf file must
 
106
# be present in the charm parent directory.
 
107
legacy_juju = lambda: not os.path.exists(
 
108
    os.path.join(CURRENT_DIR, '..', 'agent.conf'))
 
109
 
 
110
 
 
111
def _get_build_dependencies():
 
112
    """Install deb dependencies for building."""
 
113
    log('Installing build dependencies.')
 
114
    cmd_log(install_extra_repositories(*BUILD_REPOSITORIES))
 
115
    cmd_log(apt_get_install(*DEB_BUILD_DEPENDENCIES))
 
116
 
 
117
 
 
118
def get_api_address(unit_dir):
 
119
    """Return the Juju API address stored in the uniter agent.conf file."""
 
120
    import yaml  # python-yaml is only installed if juju-core is used.
 
121
    # XXX 2013-03-27 frankban bug=1161443:
 
122
        # currently the uniter agent.conf file does not include the API
 
123
        # address. For now retrieve it from the machine agent file.
 
124
    base_dir = os.path.abspath(os.path.join(unit_dir, '..'))
 
125
    for dirname in os.listdir(base_dir):
 
126
        if dirname.startswith('machine-'):
 
127
            agent_conf = os.path.join(base_dir, dirname, 'agent.conf')
 
128
            break
 
129
    else:
 
130
        raise IOError('Juju agent configuration file not found.')
 
131
    contents = yaml.load(open(agent_conf))
 
132
    return contents['apiinfo']['addrs'][0]
 
133
 
 
134
 
 
135
def get_staging_dependencies():
 
136
    """Install deb dependencies for the stage (improv) environment."""
 
137
    log('Installing stage dependencies.')
 
138
    cmd_log(apt_get_install(*DEB_STAGE_DEPENDENCIES))
 
139
 
 
140
 
 
141
def first_path_in_dir(directory):
 
142
    """Return the full path of the first file/dir in *directory*."""
 
143
    return os.path.join(directory, os.listdir(directory)[0])
 
144
 
 
145
 
 
146
def _get_by_attr(collection, attr, value):
 
147
    """Return the first item in collection having attr == value.
 
148
 
 
149
    Return None if the item is not found.
 
150
    """
 
151
    for item in collection:
 
152
        if getattr(item, attr) == value:
 
153
            return item
 
154
 
 
155
 
 
156
def get_release_file_url(project, series_name, release_version):
 
157
    """Return the URL of the release file hosted in Launchpad.
 
158
 
 
159
    The returned URL points to a release file for the given project, series
 
160
    name and release version.
 
161
    The argument *project* is a project object as returned by launchpadlib.
 
162
    The arguments *series_name* and *release_version* are strings. If
 
163
    *release_version* is None, the URL of the latest release will be returned.
 
164
    """
 
165
    series = _get_by_attr(project.series, 'name', series_name)
 
166
    if series is None:
 
167
        raise ValueError('%r: series not found' % series_name)
 
168
    # Releases are returned by Launchpad in reverse date order.
 
169
    releases = list(series.releases)
 
170
    if not releases:
 
171
        raise ValueError('%r: series does not contain releases' % series_name)
 
172
    if release_version is not None:
 
173
        release = _get_by_attr(releases, 'version', release_version)
 
174
        if release is None:
 
175
            raise ValueError('%r: release not found' % release_version)
 
176
        releases = [release]
 
177
    for release in releases:
 
178
        for file_ in release.files:
 
179
            if str(file_).endswith('.tgz'):
 
180
                return file_.file_link
 
181
    raise ValueError('%r: file not found' % release_version)
 
182
 
 
183
 
 
184
def get_zookeeper_address(agent_file_path):
 
185
    """Retrieve the Zookeeper address contained in the given *agent_file_path*.
 
186
 
 
187
    The *agent_file_path* is a path to a file containing a line similar to the
 
188
    following::
 
189
 
 
190
        env JUJU_ZOOKEEPER="address"
 
191
    """
 
192
    line = search_file('JUJU_ZOOKEEPER', agent_file_path).strip()
 
193
    return line.split('=')[1].strip('"')
 
194
 
 
195
 
 
196
@contextmanager
 
197
def log_hook():
 
198
    """Log when a hook starts and stops its execution.
 
199
 
 
200
    Also log to stdout possible CalledProcessError exceptions raised executing
 
201
    the hook.
 
202
    """
 
203
    script = script_name()
 
204
    log(">>> Entering {}".format(script))
 
205
    try:
 
206
        yield
 
207
    except CalledProcessError as err:
 
208
        log('Exception caught:')
 
209
        log(err.output)
 
210
        raise
 
211
    finally:
 
212
        log("<<< Exiting {}".format(script))
 
213
 
 
214
 
 
215
def parse_source(source):
 
216
    """Parse the ``juju-gui-source`` option.
 
217
 
 
218
    Return a tuple of two elements representing info on how to deploy Juju GUI.
 
219
    Examples:
 
220
       - ('stable', None): latest stable release;
 
221
       - ('stable', '0.1.0'): stable release v0.1.0;
 
222
       - ('trunk', None): latest trunk release;
 
223
       - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1;
 
224
       - ('branch', 'lp:juju-gui'): release is made from a branch;
 
225
       - ('url', 'http://example.com/gui'): release from a downloaded file.
 
226
    """
 
227
    if source.startswith('url:'):
 
228
        source = source[4:]
 
229
        # Support file paths, including relative paths.
 
230
        if urlparse(source).scheme == '':
 
231
            if not source.startswith('/'):
 
232
                source = os.path.join(os.path.abspath(CURRENT_DIR), source)
 
233
            source = "file://%s" % source
 
234
        return 'url', source
 
235
    if source in ('stable', 'trunk'):
 
236
        return source, None
 
237
    if source.startswith('lp:') or source.startswith('http://'):
 
238
        return 'branch', source
 
239
    if 'build' in source:
 
240
        return 'trunk', source
 
241
    return 'stable', source
 
242
 
 
243
 
 
244
def render_to_file(template_name, context, destination):
 
245
    """Render the given *template_name* into *destination* using *context*.
 
246
 
 
247
    The tempita template language is used to render contents
 
248
    (see http://pythonpaste.org/tempita/).
 
249
    The argument *template_name* is the name or path of the template file:
 
250
    it may be either a path relative to ``../config`` or an absolute path.
 
251
    The argument *destination* is a file path.
 
252
    The argument *context* is a dict-like object.
 
253
    """
 
254
    template_path = os.path.abspath(template_name)
 
255
    template = tempita.Template.from_filename(template_path)
 
256
    with open(destination, 'w') as stream:
 
257
        stream.write(template.substitute(context))
 
258
 
 
259
 
 
260
results_log = None
 
261
 
 
262
 
 
263
def _setupLogging():
 
264
    global results_log
 
265
    if results_log is not None:
 
266
        return
 
267
    cfg = config()
 
268
    logging.basicConfig(
 
269
        filename=cfg['command-log-file'],
 
270
        level=logging.INFO,
 
271
        format="%(asctime)s: %(name)s@%(levelname)s %(message)s")
 
272
    results_log = logging.getLogger('juju-gui')
 
273
 
 
274
 
 
275
def cmd_log(results):
 
276
    global results_log
 
277
    if not results:
 
278
        return
 
279
    if results_log is None:
 
280
        _setupLogging()
 
281
    # Since 'results' may be multi-line output, start it on a separate line
 
282
    # from the logger timestamp, etc.
 
283
    results_log.info('\n' + results)
 
284
 
 
285
 
 
286
def start_improv(staging_env, ssl_cert_path,
 
287
                 config_path='/etc/init/juju-api-improv.conf'):
 
288
    """Start a simulated juju environment using ``improv.py``."""
 
289
    log('Setting up staging start up script.')
 
290
    context = {
 
291
        'juju_dir': JUJU_DIR,
 
292
        'keys': ssl_cert_path,
 
293
        'port': API_PORT,
 
294
        'staging_env': staging_env,
 
295
    }
 
296
    render_to_file('config/juju-api-improv.conf.template', context, config_path)
 
297
    log('Starting the staging backend.')
 
298
    with su('root'):
 
299
        service_start(IMPROV)
 
300
 
 
301
 
 
302
def start_agent(
 
303
        ssl_cert_path, config_path='/etc/init/juju-api-agent.conf',
 
304
        read_only=False):
 
305
    """Start the Juju agent and connect to the current environment."""
 
306
    # Retrieve the Zookeeper address from the start up script.
 
307
    unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..'))
 
308
    agent_file = '/etc/init/juju-{0}.conf'.format(os.path.basename(unit_dir))
 
309
    zookeeper = get_zookeeper_address(agent_file)
 
310
    log('Setting up API agent start up script.')
 
311
    context = {
 
312
        'juju_dir': JUJU_DIR,
 
313
        'keys': ssl_cert_path,
 
314
        'port': API_PORT,
 
315
        'zookeeper': zookeeper,
 
316
        'read_only': read_only
 
317
    }
 
318
    render_to_file('config/juju-api-agent.conf.template', context, config_path)
 
319
    log('Starting API agent.')
 
320
    with su('root'):
 
321
        service_start(AGENT)
 
322
 
 
323
 
 
324
def start_gui(
 
325
        console_enabled, login_help, readonly, in_staging, ssl_cert_path,
 
326
        charmworld_url, serve_tests, haproxy_path='/etc/haproxy/haproxy.cfg',
 
327
        config_js_path=None, secure=True, sandbox=False):
 
328
    """Set up and start the Juju GUI server."""
 
329
    with su('root'):
 
330
        run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR)
 
331
    # XXX 2013-02-05 frankban bug=1116320:
 
332
        # External insecure resources are still loaded when testing in the
 
333
        # debug environment. For now, switch to the production environment if
 
334
        # the charm is configured to serve tests.
 
335
    if in_staging and not serve_tests:
 
336
        build_dirname = 'build-debug'
 
337
    else:
 
338
        build_dirname = 'build-prod'
 
339
    build_dir = os.path.join(JUJU_GUI_DIR, build_dirname)
 
340
    log('Generating the Juju GUI configuration file.')
 
341
    is_legacy_juju = legacy_juju()
 
342
    user, password = None, None
 
343
    if (is_legacy_juju and in_staging) or sandbox:
 
344
        user, password = 'admin', 'admin'
 
345
    else:
 
346
        user, password = None, None
 
347
 
 
348
    api_backend = 'python' if is_legacy_juju else 'go'
 
349
    if secure:
 
350
        protocol = 'wss'
 
351
    else:
 
352
        log('Running in insecure mode! Port 80 will serve unencrypted.')
 
353
        protocol = 'ws'
 
354
 
 
355
    context = {
 
356
        'raw_protocol': protocol,
 
357
        'address': unit_get('public-address'),
 
358
        'console_enabled': json.dumps(console_enabled),
 
359
        'login_help': json.dumps(login_help),
 
360
        'password': json.dumps(password),
 
361
        'api_backend': json.dumps(api_backend),
 
362
        'readonly': json.dumps(readonly),
 
363
        'user': json.dumps(user),
 
364
        'protocol': json.dumps(protocol),
 
365
        'sandbox': json.dumps(sandbox),
 
366
        'charmworld_url': json.dumps(charmworld_url),
 
367
    }
 
368
    if config_js_path is None:
 
369
        config_js_path = os.path.join(
 
370
            build_dir, 'juju-ui', 'assets', 'config.js')
 
371
    render_to_file('config/config.js.template', context, config_js_path)
 
372
 
 
373
    write_apache_config(build_dir, serve_tests)
 
374
 
 
375
    log('Generating haproxy configuration file.')
 
376
    if is_legacy_juju:
 
377
        # The PyJuju API agent is listening on localhost.
 
378
        api_address = '127.0.0.1:{0}'.format(API_PORT)
 
379
    else:
 
380
        # Retrieve the juju-core API server address.
 
381
        api_address = get_api_address(os.path.join(CURRENT_DIR, '..'))
 
382
    context = {
 
383
        'api_address': api_address,
 
384
        'api_pem': JUJU_PEM,
 
385
        'legacy_juju': is_legacy_juju,
 
386
        'ssl_cert_path': ssl_cert_path,
 
387
        # In PyJuju environments, use the same certificate for both HTTPS and
 
388
        # WebSocket connections. In juju-core the system already has the proper
 
389
        # certificate installed.
 
390
        'web_pem': JUJU_PEM,
 
391
        'web_port': WEB_PORT,
 
392
        'secure': secure
 
393
    }
 
394
    render_to_file('config/haproxy.cfg.template', context, haproxy_path)
 
395
    log('Starting Juju GUI.')
 
396
 
 
397
 
 
398
def write_apache_config(build_dir, serve_tests=False):
 
399
    log('Generating the apache site configuration file.')
 
400
    context = {
 
401
        'port': WEB_PORT,
 
402
        'serve_tests': serve_tests,
 
403
        'server_root': build_dir,
 
404
        'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''),
 
405
    }
 
406
    render_to_file('config/apache-ports.template', context, JUJU_GUI_PORTS)
 
407
    render_to_file('config/apache-site.template', context, JUJU_GUI_SITE)
 
408
 
 
409
 
 
410
def get_npm_cache_archive_url(Launchpad=Launchpad):
 
411
    """Figure out the URL of the most recent NPM cache archive on Launchpad."""
 
412
    launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production')
 
413
    project = launchpad.projects['juju-gui']
 
414
    # Find the URL of the most recently created NPM cache archive.
 
415
    npm_cache_url = get_release_file_url(project, 'npm-cache', None)
 
416
    return npm_cache_url
 
417
 
 
418
 
 
419
def prime_npm_cache(npm_cache_url):
 
420
    """Download NPM cache archive and prime the NPM cache with it."""
 
421
    # Download the cache archive and then uncompress it into the NPM cache.
 
422
    npm_cache_archive = os.path.join(CURRENT_DIR, 'npm-cache.tgz')
 
423
    cmd_log(run('curl', '-L', '-o', npm_cache_archive, npm_cache_url))
 
424
    npm_cache_dir = os.path.expanduser('~/.npm')
 
425
    # The NPM cache directory probably does not exist, so make it if not.
 
426
    try:
 
427
        os.mkdir(npm_cache_dir)
 
428
    except OSError, e:
 
429
        # If the directory already exists then ignore the error.
 
430
        if e.errno != errno.EEXIST:  # File exists.
 
431
            raise
 
432
    uncompress = command('tar', '-x', '-z', '-C', npm_cache_dir, '-f')
 
433
    cmd_log(uncompress(npm_cache_archive))
 
434
 
 
435
 
 
436
def fetch_gui(juju_gui_source, logpath):
 
437
    """Retrieve the Juju GUI release/branch."""
 
438
    # Retrieve a Juju GUI release.
 
439
    origin, version_or_branch = parse_source(juju_gui_source)
 
440
    if origin == 'branch':
 
441
        # Make sure we have the dependencies necessary for us to actually make
 
442
        # a build.
 
443
        _get_build_dependencies()
 
444
        # Create a release starting from a branch.
 
445
        juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source')
 
446
        log('Retrieving Juju GUI source checkout from %s.' % version_or_branch)
 
447
        cmd_log(run('rm', '-rf', juju_gui_source_dir))
 
448
        cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir))
 
449
        log('Preparing a Juju GUI release.')
 
450
        logdir = os.path.dirname(logpath)
 
451
        fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir)
 
452
        log('Output from "make distfile" sent to %s' % name)
 
453
        with environ(NO_BZR='1'):
 
454
            run('make', '-C', juju_gui_source_dir, 'distfile',
 
455
                stdout=fd, stderr=fd)
 
456
        release_tarball = first_path_in_dir(
 
457
            os.path.join(juju_gui_source_dir, 'releases'))
 
458
    else:
 
459
        log('Retrieving Juju GUI release.')
 
460
        if origin == 'url':
 
461
            file_url = version_or_branch
 
462
        else:
 
463
            # Retrieve a release from Launchpad.
 
464
            launchpad = Launchpad.login_anonymously(
 
465
                'Juju GUI charm', 'production')
 
466
            project = launchpad.projects['juju-gui']
 
467
            file_url = get_release_file_url(project, origin, version_or_branch)
 
468
        log('Downloading release file from %s.' % file_url)
 
469
        release_tarball = os.path.join(CURRENT_DIR, 'release.tgz')
 
470
        cmd_log(run('curl', '-L', '-o', release_tarball, file_url))
 
471
    return release_tarball
 
472
 
 
473
 
 
474
def fetch_api(juju_api_branch):
 
475
    """Retrieve the Juju branch."""
 
476
    # Retrieve Juju API source checkout.
 
477
    log('Retrieving Juju API source checkout.')
 
478
    cmd_log(run('rm', '-rf', JUJU_DIR))
 
479
    cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR))
 
480
 
 
481
 
 
482
def setup_gui(release_tarball):
 
483
    """Set up Juju GUI."""
 
484
    # Uncompress the release tarball.
 
485
    log('Installing Juju GUI.')
 
486
    release_dir = os.path.join(CURRENT_DIR, 'release')
 
487
    cmd_log(run('rm', '-rf', release_dir))
 
488
    os.mkdir(release_dir)
 
489
    uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f')
 
490
    cmd_log(uncompress(release_tarball))
 
491
    # Link the Juju GUI dir to the contents of the release tarball.
 
492
    cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR))
 
493
 
 
494
 
 
495
def setup_apache():
 
496
    """Set up apache."""
 
497
    log('Setting up apache.')
 
498
    if not os.path.exists(JUJU_GUI_SITE):
 
499
        cmd_log(run('touch', JUJU_GUI_SITE))
 
500
        cmd_log(run('chown', 'ubuntu:', JUJU_GUI_SITE))
 
501
        cmd_log(
 
502
            run('ln', '-s', JUJU_GUI_SITE,
 
503
                '/etc/apache2/sites-enabled/juju-gui'))
 
504
 
 
505
    if not os.path.exists(JUJU_GUI_PORTS):
 
506
        cmd_log(run('touch', JUJU_GUI_PORTS))
 
507
        cmd_log(run('chown', 'ubuntu:', JUJU_GUI_PORTS))
 
508
 
 
509
    with su('root'):
 
510
        run('a2dissite', 'default')
 
511
        run('a2ensite', 'juju-gui')
 
512
 
 
513
 
 
514
def save_or_create_certificates(
 
515
        ssl_cert_path, ssl_cert_contents, ssl_key_contents):
 
516
    """Generate the SSL certificates.
 
517
 
 
518
    If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them
 
519
    as certificates; otherwise, generate them.
 
520
 
 
521
    Also create a pem file, suitable for use in the haproxy configuration,
 
522
    concatenating the key and the certificate files.
 
523
    """
 
524
    crt_path = os.path.join(ssl_cert_path, 'juju.crt')
 
525
    key_path = os.path.join(ssl_cert_path, 'juju.key')
 
526
    if not os.path.exists(ssl_cert_path):
 
527
        os.makedirs(ssl_cert_path)
 
528
    if ssl_cert_contents and ssl_key_contents:
 
529
        # Save the provided certificates.
 
530
        with open(crt_path, 'w') as cert_file:
 
531
            cert_file.write(ssl_cert_contents)
 
532
        with open(key_path, 'w') as key_file:
 
533
            key_file.write(ssl_key_contents)
 
534
    else:
 
535
        # Generate certificates.
 
536
        # See http://superuser.com/questions/226192/openssl-without-prompt
 
537
        cmd_log(run(
 
538
            'openssl', 'req', '-new', '-newkey', 'rsa:4096',
 
539
            '-days', '365', '-nodes', '-x509', '-subj',
 
540
            # These are arbitrary test values for the certificate.
 
541
            '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com',
 
542
            '-keyout', key_path, '-out', crt_path))
 
543
    # Generate the pem file.
 
544
    pem_path = os.path.join(ssl_cert_path, JUJU_PEM)
 
545
    if os.path.exists(pem_path):
 
546
        os.remove(pem_path)
 
547
    with open(pem_path, 'w') as pem_file:
 
548
        shutil.copyfileobj(open(key_path), pem_file)
 
549
        shutil.copyfileobj(open(crt_path), pem_file)
 
550
 
 
551
 
 
552
def find_missing_packages(*packages):
 
553
    """Given a list of packages, return the packages which are not installed.
 
554
    """
 
555
    cache = apt.Cache()
 
556
    missing = set()
 
557
    for pkg_name in packages:
 
558
        try:
 
559
            pkg = cache[pkg_name]
 
560
        except KeyError:
 
561
            missing.add(pkg_name)
 
562
            continue
 
563
        if pkg.is_installed:
 
564
            continue
 
565
        missing.add(pkg_name)
 
566
    return missing
 
567
 
 
568
 
 
569
## Backend support decorators
 
570
 
 
571
def chain(name):
 
572
    """Helper method to compose a set of mixin objects into a callable.
 
573
 
 
574
    Each method is called in the context of its mixin instance, and its
 
575
    argument is the Backend instance.
 
576
    """
 
577
    # Chain method calls through all implementing mixins.
 
578
    def method(self):
 
579
        for mixin in self.mixins:
 
580
            a_callable = getattr(type(mixin), name, None)
 
581
            if a_callable:
 
582
                a_callable(mixin, self)
 
583
 
 
584
    method.__name__ = name
 
585
    return method
 
586
 
 
587
 
 
588
def merge(name):
 
589
    """Helper to merge a property from a set of strategy objects
 
590
    into a unified set.
 
591
    """
 
592
    # Return merged property from every providing mixin as a set.
 
593
    @property
 
594
    def method(self):
 
595
        result = set()
 
596
        for mixin in self.mixins:
 
597
            segment = getattr(type(mixin), name, None)
 
598
            if segment and isinstance(segment, (list, tuple, set)):
 
599
                result |= set(segment)
 
600
 
 
601
        return result
 
602
    return method