1
# Copyright 2015-2016 Canonical Ltd.
3
# This file is part of the Apt layer for Juju.
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.
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.
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/>.
18
charms.reactive helpers for dealing with deb packages.
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.
27
from charmhelpers import fetch
28
from charmhelpers.core import hookenv, unitdata
29
from charms import reactive
32
__all__ = ['add_source', 'update', 'queue_install', 'install_queued',
33
'installed', 'purge', 'ensure_package_status']
36
def add_source(source, key=None):
39
Sets the apt.needs_update state.
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.
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.
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')
56
def queue_install(packages, options=None):
57
"""Queue one or more deb packages for install.
59
The `apt.installed.{name}` state is set once the package is installed.
61
If a package has already been installed it will not be reinstalled.
63
If a package has already been queued it will not be requeued, and
64
the install options will not be changed.
66
Sets the apt.queued_installs state.
68
# Filter installed packages.
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))}
75
unitdata.kv().update(packages, prefix='apt.install_queue.')
76
reactive.set_state('apt.queued_installs')
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.'))
86
"""Purge one or more deb packages from the system"""
87
fetch.apt_purge(packages, fatal=True)
89
store.unsetrange(packages, prefix='apt.install_queue.')
90
for package in packages:
91
reactive.remove_state('apt.installed.{}'.format(package))
95
"""Update the apt cache.
97
Removes the apt.needs_update state.
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')
104
def install_queued():
105
'''Installs queued deb packages.
107
Removes the apt.queued_installs state and sets the apt.installed state.
109
On failure, sets the unit's workload state to 'blocked' and returns
110
False. Package installs remain queued.
112
On success, sets the apt.installed.{packagename} state for each
113
installed package and returns True.
115
store = unitdata.kv()
116
queue = sorted((options, package)
117
for package, options in store.getrange('apt.install_queue.',
121
for options, batch in itertools.groupby(queue, lambda x: x[0]):
122
packages = [b[1] for b in batch]
124
status_set(None, 'Installing {}'.format(','.join(packages)))
125
fetch.apt_install(packages, options, fatal=True)
126
store.unsetrange(packages, prefix='apt.install_queue.')
127
installed.update(packages)
128
except subprocess.CalledProcessError:
129
status_set('blocked',
130
'Unable to install packages {}'
131
.format(','.join(packages)))
132
return False # Without setting reactive state.
134
for package in installed:
135
reactive.set_state('apt.installed.{}'.format(package))
137
reactive.remove_state('apt.queued_installs')
141
def ensure_package_status():
142
'''Hold or unhold packages per the package_status configuration option.
144
All packages installed using this module and handlers are affected.
146
An mechanism may be added in the future to override this for a
147
subset of installed packages.
149
packages = installed()
152
config = hookenv.config()
153
package_status = config['package_status']
154
changed = reactive.helpers.data_changed('apt.package_status',
155
(package_status, sorted(packages)))
157
if package_status == 'hold':
158
hookenv.log('Holding packages {}'.format(','.join(packages)))
159
fetch.apt_hold(packages)
161
hookenv.log('Unholding packages {}'.format(','.join(packages)))
162
fetch.apt_unhold(packages)
163
reactive.remove_state('apt.needs_hold')
166
def status_set(state, message):
167
"""Set the unit's workload status.
169
Set state == None to keep the same state and just change the message.
172
state = hookenv.status_get()[0]
173
if state == 'unknown':
174
state = 'maintenance' # Guess
175
if state in ('error', 'blocked'):
176
lvl = hookenv.WARNING
179
hookenv.status_set(state, message)
180
hookenv.log('{}: {}'.format(state, message), lvl)