~postgresql-charmers/postgresql-charm/built

« back to all changes in this revision

Viewing changes to lib/charms/apt.py

  • Committer: Stuart Bishop
  • Date: 2016-02-18 10:53:55 UTC
  • Revision ID: git-v1:a0c4e5cb498bcf9402f52809730edbd220edf665
charm-build of master

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2015-2016 Canonical Ltd.
 
2
#
 
3
# This file is part of the Apt layer for Juju.
 
4
#
 
5
# This program is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License version 3, as
 
7
# published by the Free Software Foundation.
 
8
#
 
9
# This program is distributed in the hope that it will be useful, but
 
10
# WITHOUT ANY WARRANTY; without even the implied warranties of
 
11
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 
12
# PURPOSE.  See the GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
16
 
 
17
'''
 
18
charms.reactive helpers for dealing with deb packages.
 
19
 
 
20
Add apt package sources using add_source(). Queue deb packages for
 
21
installation with install(). Configure and work with your software
 
22
once the apt.installed.{packagename} state is set.
 
23
'''
 
24
import itertools
 
25
import subprocess
 
26
 
 
27
from charmhelpers import fetch
 
28
from charmhelpers.core import hookenv, unitdata
 
29
from charms import reactive
 
30
 
 
31
 
 
32
__all__ = ['add_source', 'update', 'queue_install', 'install_queued',
 
33
           'installed', 'purge', 'ensure_package_status']
 
34
 
 
35
 
 
36
def add_source(source, key=None):
 
37
    '''Add an apt source.
 
38
 
 
39
    Sets the apt.needs_update state.
 
40
 
 
41
    A source may be either a line that can be added directly to
 
42
    sources.list(5), or in the form ppa:<user>/<ppa-name> for adding
 
43
    Personal Package Archives, or a distribution component to enable.
 
44
 
 
45
    The package signing key should be an ASCII armoured GPG key. While
 
46
    GPG key ids are also supported, the retrieval mechanism is insecure.
 
47
    There is no need to specify the package signing key for PPAs or for
 
48
    the main Ubuntu archives.
 
49
    '''
 
50
    # Maybe we should remember which sources have been added already
 
51
    # so we don't waste time re-adding them. Is this time significant?
 
52
    fetch.add_source(source, key)
 
53
    reactive.set_state('apt.needs_update')
 
54
 
 
55
 
 
56
def queue_install(packages, options=None):
 
57
    """Queue one or more deb packages for install.
 
58
 
 
59
    The `apt.installed.{name}` state is set once the package is installed.
 
60
 
 
61
    If a package has already been installed it will not be reinstalled.
 
62
 
 
63
    If a package has already been queued it will not be requeued, and
 
64
    the install options will not be changed.
 
65
 
 
66
    Sets the apt.queued_installs state.
 
67
    """
 
68
    # Filter installed packages.
 
69
    store = unitdata.kv()
 
70
    queued_packages = store.getrange('apt.install_queue.', strip=True)
 
71
    packages = {package: options for package in packages
 
72
                if not (package in queued_packages or
 
73
                        reactive.helpers.is_state('apt.installed.' + package))}
 
74
    if packages:
 
75
        unitdata.kv().update(packages, prefix='apt.install_queue.')
 
76
        reactive.set_state('apt.queued_installs')
 
77
 
 
78
 
 
79
def installed():
 
80
    '''Return the set of deb packages completed install'''
 
81
    return set(state.split('.', 2)[2] for state in reactive.bus.get_states()
 
82
               if state.startswith('apt.installed.'))
 
83
 
 
84
 
 
85
def purge(packages):
 
86
    """Purge one or more deb packages from the system"""
 
87
    fetch.apt_purge(packages, fatal=True)
 
88
    store = unitdata.kv()
 
89
    store.unsetrange(packages, prefix='apt.install_queue.')
 
90
    for package in packages:
 
91
        reactive.remove_state('apt.installed.{}'.format(package))
 
92
 
 
93
 
 
94
def update():
 
95
    """Update the apt cache.
 
96
 
 
97
    Removes the apt.needs_update state.
 
98
    """
 
99
    status_set(None, 'Updating apt cache')
 
100
    fetch.apt_update(fatal=True)  # Friends don't let friends set fatal=False
 
101
    reactive.remove_state('apt.needs_update')
 
102
 
 
103
 
 
104
def install_queued():
 
105
    '''Installs queued deb packages.
 
106
 
 
107
    Removes the apt.queued_installs state and sets the apt.installed state.
 
108
 
 
109
    On failure, sets the unit's workload state to 'blocked'.
 
110
 
 
111
    Sets the apt.installed.{packagename} state for each installed package.
 
112
    Failed package installs remain queued.
 
113
    '''
 
114
    store = unitdata.kv()
 
115
    queue = sorted((options, package)
 
116
                   for package, options in store.getrange('apt.install_queue.',
 
117
                                                          strip=True).items())
 
118
 
 
119
    installed = set()
 
120
    for options, batch in itertools.groupby(queue, lambda x: x[0]):
 
121
        packages = [b[1] for b in batch]
 
122
        try:
 
123
            status_set(None, 'Installing {}'.format(','.join(packages)))
 
124
            fetch.apt_install(packages, options, fatal=True)
 
125
            store.unsetrange(packages, prefix='apt.install_queue.')
 
126
            installed.update(packages)
 
127
        except subprocess.CalledProcessError:
 
128
            status_set('blocked',
 
129
                       'Unable to install packages {}'
 
130
                       .format(','.join(packages)))
 
131
            return  # Without setting reactive state.
 
132
 
 
133
    for package in installed:
 
134
        reactive.set_state('apt.installed.{}'.format(package))
 
135
 
 
136
    reactive.remove_state('apt.queued_installs')
 
137
 
 
138
 
 
139
def ensure_package_status():
 
140
    '''Hold or unhold packages per the package_status configuration option.
 
141
 
 
142
    All packages installed using this module and handlers are affected.
 
143
 
 
144
    An mechanism may be added in the future to override this for a
 
145
    subset of installed packages.
 
146
    '''
 
147
    packages = installed()
 
148
    if not packages:
 
149
        return
 
150
    config = hookenv.config()
 
151
    package_status = config['package_status']
 
152
    changed = reactive.helpers.data_changed('apt.package_status',
 
153
                                            (package_status, sorted(packages)))
 
154
    if changed:
 
155
        if package_status == 'hold':
 
156
            hookenv.log('Holding packages {}'.format(','.join(packages)))
 
157
            fetch.apt_hold(packages)
 
158
        else:
 
159
            hookenv.log('Unholding packages {}'.format(','.join(packages)))
 
160
            fetch.apt_unhold(packages)
 
161
    reactive.remove_state('apt.needs_hold')
 
162
 
 
163
 
 
164
def status_set(state, message):
 
165
    """Set the unit's workload status.
 
166
 
 
167
    Set state == None to keep the same state and just change the message.
 
168
    """
 
169
    if state is None:
 
170
        state = hookenv.status_get()[0]
 
171
        if state == 'unknown':
 
172
            state = 'maintenance'  # Guess
 
173
    if state in ('error', 'blocked'):
 
174
        lvl = hookenv.WARNING
 
175
    else:
 
176
        lvl = hookenv.INFO
 
177
    hookenv.status_set(state, message)
 
178
    hookenv.log('{}: {}'.format(state, message), lvl)