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'.
111
Sets the apt.installed.{packagename} state for each installed package.
112
Failed package installs remain queued.
114
store = unitdata.kv()
115
queue = sorted((options, package)
116
for package, options in store.getrange('apt.install_queue.',
120
for options, batch in itertools.groupby(queue, lambda x: x[0]):
121
packages = [b[1] for b in batch]
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.
133
for package in installed:
134
reactive.set_state('apt.installed.{}'.format(package))
136
reactive.remove_state('apt.queued_installs')
139
def ensure_package_status():
140
'''Hold or unhold packages per the package_status configuration option.
142
All packages installed using this module and handlers are affected.
144
An mechanism may be added in the future to override this for a
145
subset of installed packages.
147
packages = installed()
150
config = hookenv.config()
151
package_status = config['package_status']
152
changed = reactive.helpers.data_changed('apt.package_status',
153
(package_status, sorted(packages)))
155
if package_status == 'hold':
156
hookenv.log('Holding packages {}'.format(','.join(packages)))
157
fetch.apt_hold(packages)
159
hookenv.log('Unholding packages {}'.format(','.join(packages)))
160
fetch.apt_unhold(packages)
161
reactive.remove_state('apt.needs_hold')
164
def status_set(state, message):
165
"""Set the unit's workload status.
167
Set state == None to keep the same state and just change the message.
170
state = hookenv.status_get()[0]
171
if state == 'unknown':
172
state = 'maintenance' # Guess
173
if state in ('error', 'blocked'):
174
lvl = hookenv.WARNING
177
hookenv.status_set(state, message)
178
hookenv.log('{}: {}'.format(state, message), lvl)