container_agent/docker_client.py (125 lines of code) (raw):

# Copyright (c) 2014 Spotify AB. All Rights Reserved. # # 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 logging from json import loads from os import environ from subprocess import Popen, PIPE log = logging.getLogger(__name__) DEFAULT_DOCKER_HOST = 'unix:///var/run/docker.sock' DOCKER_HOST = environ.get('DOCKER_HOST', DEFAULT_DOCKER_HOST) DEFAULT_DOCKER_CLI = '/usr/bin/docker' DOCKER_CLI = environ.get('DOCKER_CLI') def escape(word): if ' ' in word: return "'%s'" % (word, ) else: return word class DockerClientError(Exception): pass class CliDockerClientError(DockerClientError): def __init__(self, command, code, out, err): super(CliDockerClientError, self).__init__() self.command = command self.code = code self.out = out self.err = err def __str__(self): return 'docker command failed: %s (%d) out=(%s) err=(%s)' % \ (self.command, self.code, self.out, self.err) class CliDockerClient(object): """ A docker client that uses the docker cli to communicate with the docker daemon.""" __detected_system_cli = None @staticmethod def __detect_system_cli(): if DOCKER_CLI is not None: return DOCKER_CLI default_msg = 'error detecting docker cli, using default (%s)' % \ (DEFAULT_DOCKER_CLI, ) try: p = Popen('which docker', stdout=PIPE, stderr=PIPE, shell=True) except: log.exception(default_msg) return DEFAULT_DOCKER_CLI else: out, err = p.communicate() if p.returncode == 0: location = out.strip() log.debug('detected docker cli: %s', location) return location else: log.warn(default_msg) return DEFAULT_DOCKER_CLI @classmethod def __system_cli(cls): if cls.__detected_system_cli is None: cls.__detected_system_cli = cls.__detect_system_cli() return cls.__detected_system_cli def __init__(self, docker=None, endpoint=None): """ Create a docker client. Args: docker: location of the docker cli. If None or not provided, the location will resolved in the order of precedence: 1. DOCKER_CLI environment variable 2. `which docker` 3. use /usr/bin/docker endpoint: The docker endpoint. If None or not provided, then environment variable DOCKER_HOST will be used. """ super(CliDockerClient, self).__init__() self.docker = docker if docker is not None else self.__system_cli() self.endpoint = endpoint if endpoint is not None else DOCKER_HOST def cli(self, *args): """Execute a docker cli command. Args: args: cli arguments Returns: returncode, stdout, stderr Example: cli('build', '-t', 'foobar', '.') """ command = (self.docker, '-H=%s' % self.endpoint) + tuple(args) log.debug('cli: %s', command) log.debug('cli: shell style: %s', ' '.join(escape(word) for word in command)) p = Popen(command, stdout=PIPE, stderr=PIPE) out, err = p.communicate() log.debug('%d %s %s', p.returncode, out, err) return p.returncode, out, err def cli_check(self, *args): """Execute a docker cli command. Raises DockerClientError if returncode != 0. Args: args: cli arguments Returns: stdout Example: cli('build', '-t', 'foobar', '.') """ code, out, err = self.cli(*args) if code: raise CliDockerClientError(args, code, out, err) return out def inspect_container(self, container): """Inspect a container. Args: container: container id or name Returns: list of dicts with info for matching containers. Example: inspect_container('d88916009fe2') inspect_container('mysql') """ log.debug('inspect_container %s', container) code, out, err = self.cli('inspect', container) return loads(out) def run(self, image=None, command=None, ports=None, name=None, volumes=None, env=None): """Start a new container. Args: image: container image to run command: command to run ports: ports to expose. list of (internal, external, proto) tuples. external and proto is optional. Returns: "docker run -d" stdout output Example: run(image='busybox', command=['nc', '-p', '4711', '-lle', 'cat'], ports=[(7, 4711, tcp), ('1.2.3.4', 7, 4711, tcp)], name='netcat-echo') """ log.debug('run_daemon %s', image) assert image ports = ports or [] command = command or [] args = [] if ports: args.extend(self.__port_arg(*port) for port in ports) if name: args.append('--name=%s' % (name, )) if volumes: args.extend('--volume=%s' % (volume, ) for volume in volumes) if env: args.extend('--env=%s' % (env, ) for env in env) args.append(image) args.extend(command) return self.cli_check('run', '-d', *args).strip() def start(self, container): """Start an existing container. Args: container: container id or name to start Returns: None Example: start('d88916009fe2') start('mysql') """ log.debug('start %s', container) self.cli_check('start', container) def stop(self, container): """Stop a running container. Args: container: container id or name to stop Returns: None Example: stop('d88916009fe2') stop('mysql') """ log.debug('stop %s', container) self.cli_check('stop', container) def kill(self, container): """Kill a running container. Args: container: container id or name to kill Returns: None Example: kill('d88916009fe2') kill('mysql') """ log.debug('kill %s', container) self.cli_check('kill', container) def destroy(self, container): """Remove a container. Args: container: container id or name to remove Returns: None Example: destroy('d88916009fe2') destroy('mysql') """ log.debug('destroy %s', container) self.cli_check('rm', container) def list_containers(self, needle=''): """List running containers. Note: When specifying a needle, 'docker ps' is invoked without the '-q' flag in order to be able to match on container names. Any string in 'docker ps' output that contains the needle is then returned and may as such not actually be a real container id or name. Args: needle: keyword to filter docker ps output on. Returns: A list of container id's and/or names. Example: list_containers() list_containers('deadbeef-namespace') """ if not needle: return self.cli_check('ps', '-q').splitlines() else: lines = self.cli_check('ps').splitlines()[1:] matches = [word for line in lines for word in line.split() if needle in word] log.debug('list_containers: needle=%s, matches=%s', needle, matches) return matches def __port_arg(self, ip, external, internal, proto): ips = ip and '%s:' % (ip, ) or '' es = external and '%d:' % (external, ) or '' ps = proto and '/%s' % (proto, ) or '' return '-p=%s%s%d%s' % (ips, es, internal, ps)