Charmed-Kubernetes/kubeapi-load-balancer/reactive/apt.py

159 lines
5.6 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 os.path
import subprocess
import re
from charmhelpers import fetch
from charmhelpers.core import hookenv
from charmhelpers.core.hookenv import DEBUG, ERROR, WARNING
from charms import layer
from charms.layer import status
from charms import reactive
from charms.reactive import when, when_not
import charms.apt
@when('apt.needs_update')
def update():
charms.apt.update()
@when('apt.queued_installs')
@when_not('apt.needs_update')
def install_queued():
charms.apt.install_queued()
@when_not('apt.queued_installs')
def ensure_package_status():
charms.apt.ensure_package_status()
def filter_installed_packages(packages):
# Don't use fetch.filter_installed_packages, 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=${Package}\n']
installed = set(subprocess.check_output(cmd, universal_newlines=True).split())
# list of packages that are not installed
not_installed = set(packages) - installed
# now we want to check for any regex in the installation of the packages
not_installed_iterable = not_installed.copy()
for pkg in not_installed_iterable:
# grab the pattern that we want to match against the packages
p = re.compile(pkg)
for pkg2 in installed:
matched = p.search(pkg2)
if matched:
not_installed.remove(pkg)
break
return not_installed
def clear_removed_package_flags():
"""On hook startup, clear install flags for removed packages."""
removed = filter_installed_packages(charms.apt.installed())
if removed:
hookenv.log('{} missing packages ({})'.format(len(removed), ','.join(removed)), WARNING)
for package in removed:
reactive.clear_flag('apt.installed.{}'.format(package))
def add_implicit_signing_keys():
"""Add keys specified in layer.yaml
The charm can ship trusted keys, avoiding the need to specify
them in config.yaml. We need to add them before we attempt
to add any custom sources, or apt will block under Bionic
if we attempt to add a source before the key becomes trusted.
"""
opts = layer.options()
if 'apt' not in opts or 'keys' not in opts['apt']:
return
keys = opts['apt']['keys']
for p in keys:
full_p = os.path.join(hookenv.charm_dir(), p)
if os.path.exists(full_p):
hookenv.log("Adding key {}".format(p), DEBUG)
subprocess.check_call(
['apt-key', 'add', full_p],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
else:
hookenv.log('Key {!r} does not exist'.format(full_p), ERROR)
def configure_sources():
"""Add user specified package sources from the service configuration.
See charmhelpers.fetch.configure_sources for details.
"""
config = hookenv.config()
# We don't have enums, so we need to validate this ourselves.
package_status = config.get('package_status') or ''
if package_status not in ('hold', 'install'):
status.blocked('Unknown package_status {}'.format(package_status))
# Die before further hooks are run. This isn't very nice, but
# there is no other way to inform the operator that they have
# invalid configuration.
raise SystemExit(0)
sources = config.get('install_sources') or ''
keys = config.get('install_keys') or ''
if reactive.helpers.data_changed('apt.configure_sources', (sources, keys)):
fetch.configure_sources(update=False, sources_var='install_sources', keys_var='install_keys')
reactive.set_flag('apt.needs_update')
# Clumsy 'config.get() or' per Bug #1641362
extra_packages = sorted((config.get('extra_packages') or '').split())
if extra_packages:
charms.apt.queue_install(extra_packages)
def queue_layer_packages():
"""Add packages listed in build-time layer options."""
# Both basic and apt layer. basic layer will have already installed
# its defined packages, but rescheduling it here gets the apt layer
# flag set and they will pinned as any other apt layer installed
# package.
opts = layer.options()
for section in ['basic', 'apt']:
if section in opts and 'packages' in opts[section]:
charms.apt.queue_install(opts[section]['packages'])
hookenv.atstart(hookenv.log, 'Initializing Apt Layer')
hookenv.atstart(clear_removed_package_flags)
hookenv.atstart(add_implicit_signing_keys)
hookenv.atstart(configure_sources)
hookenv.atstart(queue_layer_packages)
hookenv.atstart(charms.apt.reset_application_version)