# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import collections import json import logging import random import string import subprocess LOG = logging.getLogger(__name__) class DockerRunner(object): def __init__(self, managed_by, docker_cmd=None): self.managed_by = managed_by self.docker_cmd = docker_cmd or 'docker' @staticmethod def execute(cmd): LOG.debug('$ %s' % ' '.join(cmd)) subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) cmd_stdout, cmd_stderr = subproc.communicate() LOG.debug(cmd_stdout) LOG.debug(cmd_stderr) return (cmd_stdout.decode('utf-8'), cmd_stderr.decode('utf-8'), subproc.returncode) @staticmethod def execute_interactive(cmd): LOG.debug('$ %s' % ' '.join(cmd)) return subprocess.call(cmd) def current_config_ids(self): # List all config_id labels for managed containers cmd = [ self.docker_cmd, 'ps', '-a', '--filter', 'label=managed_by=%s' % self.managed_by, '--format', '{{.Label "config_id"}}' ] cmd_stdout, cmd_stderr, returncode = self.execute(cmd) if returncode != 0: return set() return set(cmd_stdout.split()) def containers_in_config(self, conf_id): cmd = [ self.docker_cmd, 'ps', '-q', '-a', '--filter', 'label=managed_by=%s' % self.managed_by, '--filter', 'label=config_id=%s' % conf_id ] cmd_stdout, cmd_stderr, returncode = self.execute(cmd) if returncode != 0: return [] return [c for c in cmd_stdout.split()] def remove_containers(self, conf_id): for container in self.containers_in_config(conf_id): self.remove_container(container) def remove_container(self, container): cmd = [self.docker_cmd, 'rm', '-f', container] cmd_stdout, cmd_stderr, returncode = self.execute(cmd) if returncode != 0: LOG.error('Error removing container: %s' % container) LOG.error(cmd_stderr) def container_names(self, conf_id=None): # list every container name, and its container_name label cmd = [ self.docker_cmd, 'ps', '-a', '--filter', 'label=managed_by=%s' % self.managed_by ] if conf_id: cmd.extend(( '--filter', 'label=config_id=%s' % conf_id )) cmd.extend(( '--format', '{{.Names}} {{.Label "container_name"}}' )) cmd_stdout, cmd_stderr, returncode = self.execute(cmd) if returncode != 0: return for line in cmd_stdout.split("\n"): if line: yield line.split() def rename_containers(self): current_containers = [] need_renaming = {} for entry in self.container_names(): current_containers.append(entry[0]) # ignore if container_name label not set if len(entry) < 2: continue # ignore if desired name is already actual name if entry[0] == entry[-1]: continue need_renaming[entry[0]] = entry[-1] for current, desired in sorted(need_renaming.items()): if desired in current_containers: LOG.info('Cannot rename "%s" since "%s" still exists' % ( current, desired)) else: LOG.info('Renaming "%s" to "%s"' % (current, desired)) self.rename_container(current, desired) current_containers.append(desired) def rename_container(self, container, name): cmd = [self.docker_cmd, 'rename', container, name] cmd_stdout, cmd_stderr, returncode = self.execute(cmd) if returncode != 0: LOG.error('Error renaming container: %s' % container) LOG.error(cmd_stderr) def inspect(self, name, format=None, type='container'): cmd = [self.docker_cmd, 'inspect', '--type', type] if format: cmd.append('--format') cmd.append(format) cmd.append(name) (cmd_stdout, cmd_stderr, returncode) = self.execute(cmd) if returncode != 0: return try: if format: return cmd_stdout else: return json.loads(cmd_stdout)[0] except Exception as e: LOG.error('Problem parsing docker inspect: %s' % e) def unique_container_name(self, container): container_name = container while self.inspect(container_name, format='exists'): suffix = ''.join(random.choice( string.ascii_lowercase + string.digits) for i in range(8)) container_name = '%s-%s' % (container, suffix) break return container_name def discover_container_name(self, container, cid): cmd = [ self.docker_cmd, 'ps', '-a', '--filter', 'label=container_name=%s' % container, '--filter', 'label=config_id=%s' % cid, '--format', '{{.Names}}' ] (cmd_stdout, cmd_stderr, returncode) = self.execute(cmd) if returncode != 0: return container names = cmd_stdout.split() if names: return names[0] return container def delete_missing_configs(self, config_ids): if not config_ids: config_ids = [] for conf_id in self.current_config_ids(): if conf_id not in config_ids: LOG.debug('%s no longer exists, deleting containers' % conf_id) self.remove_containers(conf_id) def list_configs(self): configs = collections.defaultdict(list) for conf_id in self.current_config_ids(): for container in self.containers_in_config(conf_id): configs[conf_id].append(self.inspect(container)) return configs