Source code for certdeploy.client.update

"""Functions that update system services."""

import subprocess

import docker

from . import log
from .config import ClientConfig
from .config.service import (
    DockerContainer,
    DockerService,
    RCService,
    Script,
    SystemdUnit,
)
from .errors import (
    DockerContainerError,
    DockerContainerNotFound,
    DockerServiceError,
    DockerServiceNotFound,
    RCServiceError,
    ScriptError,
    SystemdError,
    UpdateError,
)


[docs] def update_docker_container(spec: DockerContainer, client_config: ClientConfig): """Update a docker container. Arguments: spec: The update service specifications. client_config: The CertDeploy client config. Raises: DockerContainerError: When there is a docker error while restarting the container. DockerContainerNotFound: When the specified container cannot be found. """ log.debug('Updating %s', spec) api = docker.DockerClient(base_url=client_config.docker_url) matches = api.containers.list(filters=spec.filters) if not matches: err = DockerContainerNotFound(spec) if client_config.fail_fast: raise err log.error(err) return for container in matches: try: container.restart(timeout=spec.timeout) except docker.errors.APIError as err: error = DockerContainerError( spec, err, str(container.attrs.get('Names')), ) if client_config.fail_fast: raise error from err log.error(error, exc_info=err) else: log.info( 'Docker container updated: names=%s, filters=%s', container.attrs.get('Names'), spec.filters, )
[docs] def update_docker_service(spec: DockerService, client_config: ClientConfig): """Force update a docker service. Arguments: spec: The update service specifications. client_config: The CertDeploy client config. Raises: DockerServiceError: When there is a docker error while force updating the service. DockerServiceNotFound: When the specified service cannot be found. """ log.debug('Updating %s', spec) api = docker.DockerClient(base_url=client_config.docker_url) if spec.name: try: matches = [api.services.get(spec.name)] except docker.errors.NotFound: matches = [] else: matches = api.services.list(filters=spec.filters) if not matches: # Borrowing the formatting from the exception err = DockerServiceNotFound(spec) if client_config.fail_fast: raise err log.error(err) return for service in matches: try: warnings = service.force_update() except docker.errors.APIError as err: # Borrowing the formatting from the exception error = DockerServiceError(spec, service_name=service.name) if client_config.fail_fast: raise error from err log.error(error, exc_info=err) else: for warn in warnings.get('Warnings') or []: log.warning('Got warning from the Docker API: %s', warn) log.info('Docker service updated: name=%s', service.name)
[docs] def update_rc_service(spec: RCService, client_config: ClientConfig): """Update an init system service. Arguments: spec: The update service specifications. client_config: The CertDeploy client config. Raises: RCServiceError: When the `service` encounters an `OSError`, doesn't finish in a timely manner (according to `script.timeout`), or exits non-zero. """ log.debug('Updating %s', spec) cmd = [client_config.rc_service_exec, spec.name, spec.action] try: proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) proc.wait(timeout=spec.timeout) except (OSError, subprocess.TimeoutExpired) as err: # Regular errors from the `service` call shouldn't halt the whole # set of updates unless fail_fast is True. # Borrowing the exception's formatting. error = RCServiceError(spec, err) if client_config.fail_fast: raise error from err log.error(error, exc_info=err) return stdout = proc.stdout.read().decode() log.debug( 'RC service command %s returned=%s. Got combined ' 'stdout/stderr: \n%s', # nofmt proc.returncode, cmd, stdout, ) if proc.returncode != 0: # Borrowing the exception's formatting. err = RCServiceError(spec, stdout=stdout) if client_config.fail_fast: raise err log.error(err) log.info('RC service %s %sed.', spec.name, spec.action)
[docs] def update_script(script: Script, client_config: ClientConfig): """Update the system with a script. Arguments: script: The update service specifications. client_config: The CertDeploy client config. Raises: ScriptError: When the script encounters an `OSError`, doesn't finish in a timely manner (according to `script.timeout`), or exits non-zero. """ log.debug('Updating %s', script) try: proc = subprocess.Popen( [script.script_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) proc.wait(timeout=script.timeout) except (OSError, subprocess.TimeoutExpired) as err: # Regular errors from the script shouldn't halt the whole # set of updates unless fail_fast is True. # Borrowing the exception's formatting. error = ScriptError(script, err) if client_config.fail_fast: raise error from err log.error(error, exc_info=err) return stdout = proc.stdout.read().decode() log.debug( 'Script %s returned=%s, combined stdout/stderr: \n%s', script.script_path, proc.returncode, stdout, ) if proc.returncode != 0: # Borrowing the exception's formatting err = ScriptError(script, proc=proc, stdout=stdout) if client_config.fail_fast: raise err log.error(err) else: log.info('Script %s returned=%s', script.script_path, proc.returncode)
[docs] def update_systemd_unit(unit: SystemdUnit, client_config: ClientConfig): """Update a Systemd unit. Arguments: unit: The update service specifications. client_config: The CertDeploy client config. Raises: SystemdError: When the systemctl encounters an `OSError`, doesn't finish in a timely manner (according to `script.timeout`), or exits non-zero. """ log.debug('Updating %s', unit) cmd = [client_config.systemd_exec, unit.action, unit.name] try: proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) proc.wait(timeout=unit.timeout) except (OSError, subprocess.TimeoutExpired) as err: # Regular errors from the systemctl call shouldn't halt the whole # set of updates unless fail_fast is True. # Borrowing the exception's formatting. error = SystemdError(unit, err) if client_config.fail_fast: raise error from err log.error(error, exc_info=err) return stdout = proc.stdout.read().decode() log.debug( 'Systemctl command %s returned=%s. Got combined ' 'stdout/stderr: \n%s', proc.returncode, cmd, stdout, ) if proc.returncode != 0: # Borrowing the exception's formatting. err = SystemdError(unit, stdout=stdout) if client_config.fail_fast: raise err log.error(err) log.info('Systemd unit %s %sed.', unit.name, unit.action)
# An index of Service types to update functions _UPDATER_MAP = { DockerContainer: update_docker_container, DockerService: update_docker_service, RCService: update_rc_service, Script: update_script, SystemdUnit: update_systemd_unit, }
[docs] def update_services(config: ClientConfig): """Update all services in `config.services`. Arguments: config: The CertDeploy client config. """ for service in config.services: try: _UPDATER_MAP[type(service)](service, config) except UpdateError as err: # Don't halt on UpdateError unless fail_fast is True # ConfigError and any unexpected errors should halt if config.fail_fast: raise err from err