210 lines
7.4 KiB
Python
210 lines
7.4 KiB
Python
# Copyright 2015-2020 Canonical Ltd.
|
|
#
|
|
# This file is part of the Apt layer for Juju.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License version 3, as
|
|
# published by the Free Software Foundation.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranties of
|
|
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
|
|
# PURPOSE. See the GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
'''
|
|
charms.reactive helpers for dealing with deb packages.
|
|
|
|
Add apt package sources using add_source(). Queue deb packages for
|
|
installation with install(). Configure and work with your software
|
|
once the apt.installed.{packagename} flag is set.
|
|
'''
|
|
import itertools
|
|
import re
|
|
import subprocess
|
|
|
|
from charmhelpers import fetch
|
|
from charmhelpers.core import hookenv, unitdata
|
|
from charms import layer, reactive
|
|
from charms.layer import status
|
|
from charms.reactive import flags
|
|
|
|
|
|
__all__ = ['add_source', 'update', 'queue_install', 'install_queued', 'installed', 'purge', 'ensure_package_status']
|
|
|
|
|
|
def add_source(source, key=None):
|
|
'''Add an apt source.
|
|
|
|
Sets the apt.needs_update flag.
|
|
|
|
A source may be either a line that can be added directly to
|
|
sources.list(5), or in the form ppa:<user>/<ppa-name> for adding
|
|
Personal Package Archives, or a distribution component to enable.
|
|
|
|
The package signing key should be an ASCII armoured GPG key. While
|
|
GPG key ids are also supported, the retrieval mechanism is insecure.
|
|
There is no need to specify the package signing key for PPAs or for
|
|
the main Ubuntu archives.
|
|
'''
|
|
# Maybe we should remember which sources have been added already
|
|
# so we don't waste time re-adding them. Is this time significant?
|
|
fetch.add_source(source, key)
|
|
reactive.set_flag('apt.needs_update')
|
|
|
|
|
|
def queue_install(packages, options=None):
|
|
"""Queue one or more deb packages for install.
|
|
|
|
The `apt.installed.{name}` flag is set once the package is installed.
|
|
|
|
If a package has already been installed it will not be reinstalled.
|
|
|
|
If a package has already been queued it will not be requeued, and
|
|
the install options will not be changed.
|
|
|
|
Sets the apt.queued_installs flag.
|
|
"""
|
|
if isinstance(packages, str):
|
|
packages = [packages]
|
|
# Filter installed packages.
|
|
store = unitdata.kv()
|
|
queued_packages = store.getrange('apt.install_queue.', strip=True)
|
|
packages = {
|
|
package: options
|
|
for package in packages
|
|
if not (package in queued_packages or reactive.is_flag_set('apt.installed.' + package))
|
|
}
|
|
if packages:
|
|
unitdata.kv().update(packages, prefix='apt.install_queue.')
|
|
reactive.set_flag('apt.queued_installs')
|
|
|
|
|
|
def installed():
|
|
'''Return the set of deb packages completed install'''
|
|
return set(flag.split('.', 2)[2] for flag in flags.get_flags() if flag.startswith('apt.installed.'))
|
|
|
|
|
|
def purge(packages):
|
|
"""Purge one or more deb packages from the system"""
|
|
fetch.apt_purge(packages, fatal=True)
|
|
store = unitdata.kv()
|
|
store.unsetrange(packages, prefix='apt.install_queue.')
|
|
for package in packages:
|
|
reactive.clear_flag('apt.installed.{}'.format(package))
|
|
|
|
|
|
def update():
|
|
"""Update the apt cache.
|
|
|
|
Removes the apt.needs_update flag.
|
|
"""
|
|
status.maintenance('Updating apt cache')
|
|
fetch.apt_update(fatal=True) # Friends don't let friends set fatal=False
|
|
reactive.clear_flag('apt.needs_update')
|
|
|
|
|
|
def install_queued():
|
|
'''Installs queued deb packages.
|
|
|
|
Removes the apt.queued_installs flag and sets the apt.installed flag.
|
|
|
|
On failure, sets the unit's workload status to 'blocked' and returns
|
|
False. Package installs remain queued.
|
|
|
|
On success, sets the apt.installed.{packagename} flag for each
|
|
installed package and returns True.
|
|
'''
|
|
store = unitdata.kv()
|
|
queue = sorted((options, package) for package, options in store.getrange('apt.install_queue.', strip=True).items())
|
|
|
|
installed = set()
|
|
for options, batch in itertools.groupby(queue, lambda x: x[0]):
|
|
packages = [b[1] for b in batch]
|
|
try:
|
|
status.maintenance('Installing {}'.format(','.join(packages)))
|
|
fetch.apt_install(packages, options, fatal=True)
|
|
store.unsetrange(packages, prefix='apt.install_queue.')
|
|
installed.update(packages)
|
|
except subprocess.CalledProcessError:
|
|
status.blocked('Unable to install packages {}'.format(','.join(packages)))
|
|
return False # Without setting reactive flag.
|
|
|
|
for package in installed:
|
|
reactive.set_flag('apt.installed.{}'.format(package))
|
|
reactive.clear_flag('apt.queued_installs')
|
|
|
|
reset_application_version()
|
|
|
|
return True
|
|
|
|
|
|
def get_package_version(package, full_version=False):
|
|
'''Return the version of an installed package.
|
|
|
|
If `full_version` is True, returns the full Debian package version.
|
|
Otherwise, returns the shorter 'upstream' version number.
|
|
'''
|
|
# Don't use fetch.get_upstream_version, as it depends on python-apt
|
|
# and not available if the basic layer's use_site_packages option is off.
|
|
cmd = ['dpkg-query', '--show', r'--showformat=${Version}\n', package]
|
|
full = subprocess.check_output(cmd, universal_newlines=True).strip()
|
|
if not full_version:
|
|
# Attempt to strip off Debian style metadata from the end of the
|
|
# version number.
|
|
m = re.search(r'^([\d.a-z]+)', full, re.I)
|
|
if m is not None:
|
|
return m.group(1)
|
|
return full
|
|
|
|
|
|
def reset_application_version():
|
|
'''Set the Juju application version, per settings in layer.yaml'''
|
|
# Reset the application version. We call this after installing
|
|
# packages to initialize the version. We also call this every
|
|
# hook, incase the version has changed (eg. Landscape upgraded
|
|
# the package).
|
|
opts = layer.options().get('apt', {})
|
|
pkg = opts.get('version_package')
|
|
if pkg and pkg in installed():
|
|
ver = get_package_version(pkg, opts.get('full_version', False))
|
|
hookenv.application_version_set(ver)
|
|
|
|
|
|
def ensure_package_status():
|
|
'''Hold or unhold packages per the package_status configuration option.
|
|
|
|
All packages installed using this module and handlers are affected.
|
|
|
|
An mechanism may be added in the future to override this for a
|
|
subset of installed packages.
|
|
'''
|
|
packages = installed()
|
|
if not packages:
|
|
return
|
|
config = hookenv.config()
|
|
package_status = config.get('package_status') or ''
|
|
changed = reactive.data_changed('apt.package_status', (package_status, sorted(packages)))
|
|
if changed:
|
|
if package_status == 'hold':
|
|
hookenv.log('Holding packages {}'.format(','.join(packages)))
|
|
fetch.apt_hold(packages)
|
|
else:
|
|
hookenv.log('Unholding packages {}'.format(','.join(packages)))
|
|
fetch.apt_unhold(packages)
|
|
reactive.clear_flag('apt.needs_hold')
|
|
|
|
|
|
def status_set(state, message):
|
|
'''DEPRECATED, set the unit's workload status.
|
|
|
|
Set state == None to keep the same state and just change the message.
|
|
'''
|
|
if state is None:
|
|
state = hookenv.status_get()[0]
|
|
if state not in ('active', 'waiting', 'blocked'):
|
|
state = 'maintenance' # Guess
|
|
status.status_set(state, message)
|