paunch/paunch/runner.py

228 lines
7.9 KiB
Python

# 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 random
import string
import subprocess
from paunch.utils import common
class DockerRunner(object):
def __init__(self, managed_by, docker_cmd=None, log=None):
self.managed_by = managed_by
self.docker_cmd = docker_cmd or 'docker'
# Leverage pre-configured logger
self.log = log or common.configure_logging(__name__)
@staticmethod
def execute(cmd, log=None):
if not log:
log = common.configure_logging(__name__)
log.debug('$ %s' % ' '.join(cmd))
subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
cmd_stdout, cmd_stderr = subproc.communicate()
if subproc.returncode != 0:
log.error('Error executing %s: returned %s' % (cmd,
subproc.returncode))
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=None):
if not log:
log = common.configure_logging(__name__)
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, self.log)
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, self.log)
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):
self.execute([self.docker_cmd, 'stop', container], self.log)
cmd = [self.docker_cmd, 'rm', container]
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
if returncode != 0:
self.log.error('Error removing container: %s' % container)
self.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, self.log)
if returncode != 0:
return []
result = []
for line in cmd_stdout.split("\n"):
if line:
result.append(line.split())
return result
def rename_containers(self):
current_containers = []
need_renaming = {}
renamed = False
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:
self.log.info('Cannot rename "%s" since "%s" '
'still exists' % (current, desired))
else:
self.log.info('Renaming "%s" to "%s"' % (
current, desired))
self.rename_container(current, desired)
renamed = True
current_containers.append(desired)
return renamed
def rename_container(self, container, name):
cmd = [self.docker_cmd, 'rename', container, name]
cmd_stdout, cmd_stderr, returncode = self.execute(cmd, self.log)
if returncode != 0:
self.log.error('Error renaming container: %s' % container)
self.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, self.log)
if returncode != 0:
return
try:
if format:
return cmd_stdout
else:
return json.loads(cmd_stdout)[0]
except Exception as e:
self.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, self.log)
if returncode == 0:
names = cmd_stdout.split()
if names:
return names[0]
self.log.warning('Did not find container with "%s" - retrying without '
'config_id' % cmd)
cmd = [
self.docker_cmd,
'ps',
'-a',
'--filter',
'label=container_name=%s' % container,
'--format',
'{{.Names}}'
]
(cmd_stdout, cmd_stderr, returncode) = self.execute(cmd, self.log)
if returncode == 0:
names = cmd_stdout.split()
if names:
return names[0]
self.log.warning('Did not find container with "%s"' % cmd)
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:
self.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