class Runner: """ This class is in charge of loading test suites and runs them on different environments """ STOP_TIMEOUT = 3 def __init__(self, cfg): from docker.client import Client from docker.utils import kwargs_from_env self.config = cfg docker_kwargs = kwargs_from_env() docker_kwargs['tls'].assert_hostname = False self.docker = Client(**docker_kwargs) def run(self, build, *tests): """ Run all the test suites passed in as parameters on the given build This method will start a container of the build, run the tests and stop it """ from docker.utils import create_host_config print("Running tests on {}".format(build.name)) ports = self.config['environment']['ports'] host = self.config['global'].get('docker_host', os.getenv('DOCKER_HOST').split('/')[-1].split(':')[0]) container = self.docker.create_container( image=build.docker_tag, command='/bin/bash -c "nc -l 8080"', ports=ports, host_config=create_host_config(port_bindings=dict(zip(ports, [None] * len(ports)))) ).get('Id') self.docker.start(container) info = self.docker.inspect_container(container) port_bindings = {port: bind[0]['HostPort'] for port, bind in info['NetworkSettings']['Ports'].items()} for test in tests: test.run(host, port_bindings, build.context) self.docker.stop(container, timeout=self.STOP_TIMEOUT) log_file_path = os.path.join(self.config['global'].get('logs_dir', '/tmp'), '{}.log'.format(build.name)) with open(log_file_path, 'wb') as logs: logs.write(self.docker.logs(container, stdout=True, stderr=True, stream=False)) print("Container logs wrote to {}".format(log_file_path))
class Docker: dockerconf = KOOPLEX.get('docker', {}) def __init__(self): base_url = self.dockerconf.get('base_url', '') self.client = Client(base_url=base_url) logger.debug("Client init") self.check = None def list_imagenames(self): logger.debug("Listing image names") pattern_imagenamefilter = KOOPLEX.get('docker', {}).get( 'pattern_imagename_filter', r'^image-%(\w+):\w$') for image in self.client.images(all=True): if image['RepoTags'] is None: continue for tag in image['RepoTags']: if re.match(pattern_imagenamefilter, tag): _, imagename, _ = re.split(pattern_imagenamefilter, tag) logger.debug("Found image: %s" % imagename) yield imagename def list_volumenames(self): logger.debug("Listing volume names") volumes = self.client.volumes() for volume in volumes['Volumes']: yield volume['Name'] def create_volume(self, volume): volume_dir = None #self.dockerconf.get('volume_dir', '') if volume_dir: self.client.create_volume(name=volume.name, driver='local', driver_opts={ 'device': '%s/%s/' % (volume_dir, volume.name), 'o': 'bind', 'type': 'none' }) else: self.client.create_volume(name=volume.name, ) logger.debug("Volume %s created" % volume.name) return True #self.get_container(container) def delete_volume(self, volume): self.client.remove_volume(name=volume.name) logger.debug("Volume %s deleted" % volume.name) def get_container(self, container): for item in self.client.containers(all=True): # docker API prepends '/' in front of container names if '/' + container.name in item['Names']: logger.debug("Get container %s" % container.name) return item return None def create_container(self, container): volumes = [] # the list of mount points in the container binds = {} # a mapping dictionary of the container mounts for volume in container.volumes: logger.debug("container %s, volume %s" % (container, volume)) mp = volume.mountpoint volumes.append(mp) binds[volume.name] = { 'bind': mp, 'mode': volume.mode(container.user) } logger.debug("container %s binds %s" % (container, binds)) host_config = self.client.create_host_config( binds=binds, privileged=True, mem_limit='2g', memswap_limit='170m', mem_swappiness=0, # oom_kill_disable = True, cpu_shares=2, ) network = self.dockerconf.get('network', 'host') networking_config = {'EndpointsConfig': {network: {}}} ports = self.dockerconf.get('container_ports', [8000, 9000]) imagename = container.image.imagename if container.image else self.dockerconf.get( 'default_image', 'basic') args = { 'name': container.name, 'image': imagename, 'detach': True, 'hostname': container.name, 'host_config': host_config, 'networking_config': networking_config, 'environment': container.environment, 'volumes': volumes, 'ports': ports, } self.client.create_container(**args) logger.debug("Container created") self.managemount(container) #FIXME: check if not called twice return self.get_container(container) def _writefile(self, container_name, path, filename, content): import tarfile import time from io import BytesIO tarstream = BytesIO() tar = tarfile.TarFile(fileobj=tarstream, mode='w') tarinfo = tarfile.TarInfo(name=filename) tarinfo.size = len(content) tarinfo.mtime = time.time() tar.addfile(tarinfo, BytesIO(content)) tar.close() tarstream.seek(0) try: status = self.client.put_archive(container=container_name, path=path, data=tarstream) logger.info("container %s put_archive %s/%s returns %s" % (container_name, path, filename, status)) except Exception as e: logger.error("container %s put_archive %s/%s fails -- %s" % (container_name, path, filename, e)) def managemount(self, container): from kooplex.lib.fs_dirname import Dirname path, filename = os.path.split( self.dockerconf.get('mountconf', '/tmp/mount.conf')) mapper = [] for v in container.volumes: mapper.extend([ "%s:%s" % (v.volumetype, d) for d in Dirname.containervolume_listfolders(container, v) ]) #NOTE: mounter uses read to process the mapper configuration, thus we need to make sure '\n' terminates the config mapper file mapper.append('') logger.debug("container %s map %s" % (container, mapper)) file_data = "\n".join(mapper).encode('utf8') self._writefile(container.name, path, filename, file_data) def trigger_impersonator(self, vcproject): #FIXME: dont call it 1-by-1 from kooplex.lib.fs_dirname import Dirname container_name = self.dockerconf.get('impersonator', 'impersonator') path, filename = os.path.split( self.dockerconf.get('gitcommandconf', '/tmp/gitcommand.conf')) cmdmaps = [] token = vcproject.token fn_clonesh = os.path.join(Dirname.vcpcache(vcproject), "clone.sh") fn_key = os.path.join(Dirname.userhome(vcproject.token.user), '.ssh', token.fn_rsa) cmdmaps.append( "%s:%s:%s:%s" % (token.user.username, fn_key, token.repository.domain, fn_clonesh)) cmdmaps.append('') file_data = "\n".join(cmdmaps).encode('utf8') self._writefile(container_name, path, filename, file_data) def run_container(self, container): docker_container_info = self.get_container(container) if docker_container_info is None: logger.debug("Container did not exist, Creating new one") docker_container_info = self.create_container(container) container_state = docker_container_info['Status'] if container_state == 'Created' or container_state.startswith( 'Exited'): logger.debug("Starting container") self.start_container(container) def refresh_container_state(self, container): docker_container_info = self.get_container(container) container_state = docker_container_info['State'] logger.debug("Container state %s" % container_state) container.last_message = str(container_state) container.last_message_at = now() container.save() def start_container(self, container): self.client.start(container.name) # we need to retrieve the container state after starting it docker_container_info = self.get_container(container) container_state = docker_container_info['State'] logger.debug("Container state %s" % container_state) container.last_message = str(container_state) container.last_message_at = now() assert container_state == 'running', "Container failed to start: %s" % docker_container_info def stop_container(self, container): try: self.client.stop(container.name) container.last_message = 'Container stopped' except Exception as e: logger.warn("docker container not found by API -- %s" % e) container.last_message = str(e) def remove_container(self, container): try: self.client.remove_container(container.name) container.last_message = 'Container removed' container.last_message_at = now() except Exception as e: logger.warn("docker container not found by API -- %s" % e) container.last_message = str(e) container.last_message_at = now() logger.debug("Container removed %s" % container.name) #FIXME: az execute2 lesz az igazi... def execute(self, container, command): logger.info("execution: %s in %s" % (command, container)) execution = self.client.exec_create(container=container.name, cmd=shlex.split(command)) return self.client.exec_start(execution, detach=False) def execute2(self, container, command): logger.info("execution: %s in %s" % (command, container)) execution = self.client.exec_create(container=container.name, cmd=shlex.split(command)) response = self.client.exec_start(exec_id=execution['Id'], stream=False) check = self.client.exec_inspect(exec_id=execution['Id']) self.check = check if check['ExitCode'] != 0: logger.error('Execution %s in %s failed -- %s' % (command, container, check)) return response.decode()
class Docker: dockerconf = KOOPLEX.get('docker', {}) def __init__(self): base_url = self.dockerconf.get('base_url', '') self.client = Client(base_url = base_url) logger.debug("Client init") self.check = None def list_imagenames(self): logger.debug("Listing image names") pattern_imagenamefilter = KOOPLEX.get('docker', {}).get('pattern_imagename_filter', r'^image-%(\w+):\w$') for image in self.client.images(all = True): if image['RepoTags'] is None: continue for tag in image['RepoTags']: if re.match(pattern_imagenamefilter, tag): _, imagename, _ = re.split(pattern_imagenamefilter, tag) logger.debug("Found image: %s" % imagename) yield imagename def list_volumenames(self): logger.debug("Listing volume names") volumes = self.client.volumes() for volume in volumes['Volumes']: yield volume['Name'] def get_container(self, container): for item in self.client.containers(all = True): # docker API prepends '/' in front of container names if '/' + container.name in item['Names']: logger.debug("Get container %s" % container.name) return item return None def create_container(self, container): volumes = [] # the list of mount points in the container binds = {} # a mapping dictionary of the container mounts for volume in container.volumes: logger.debug("container %s, volume %s" % (container, volume)) mp = volume.mountpoint volumes.append(mp) binds[volume.name] = { 'bind': mp, 'mode': volume.mode(container.user) } logger.debug("container %s binds %s" % (container, binds)) host_config = self.client.create_host_config( binds = binds, privileged = True, mem_limit = '2g', memswap_limit = '170m', mem_swappiness = 0, # oom_kill_disable = True, cpu_shares = 2, ) network = self.dockerconf.get('network', 'host') networking_config = { 'EndpointsConfig': { network: {} } } ports = self.dockerconf.get('container_ports', [ 8000, 9000 ]) imagename = container.image.imagename if container.image else self.dockerconf.get('default_image', 'basic') args = { 'name': container.name, 'image': imagename, 'detach': True, 'hostname': container.name, 'host_config': host_config, 'networking_config': networking_config, 'environment': container.environment, 'volumes': volumes, 'ports': ports, } self.client.create_container(**args) logger.debug("Container created") self.managemount(container) #FIXME: check if not called twice return self.get_container(container) def _writefile(self, container_name, path, filename, content): import tarfile import time from io import BytesIO tarstream = BytesIO() tar = tarfile.TarFile(fileobj = tarstream, mode = 'w') tarinfo = tarfile.TarInfo(name = filename) tarinfo.size = len(content) tarinfo.mtime = time.time() tar.addfile(tarinfo, BytesIO(content)) tar.close() tarstream.seek(0) try: status = self.client.put_archive(container = container_name, path = path, data = tarstream) logger.info("container %s put_archive %s/%s returns %s" % (container_name, path, filename, status)) except Exception as e: logger.error("container %s put_archive %s/%s fails -- %s" % (container_name, path, filename, e)) def managemount(self, container): from kooplex.lib.fs_dirname import Dirname path, filename = os.path.split(self.dockerconf.get('mountconf', '/tmp/mount.conf')) mapper = [] for v in container.volumes: mapper.extend([ "%s:%s" % (v.volumetype, d) for d in Dirname.containervolume_listfolders(container, v) ]) #NOTE: mounter uses read to process the mapper configuration, thus we need to make sure '\n' terminates the config mapper file mapper.append('') logger.debug("container %s map %s" % (container, mapper)) file_data = "\n".join(mapper).encode('utf8') self._writefile(container.name, path, filename, file_data) def trigger_impersonator(self, vcproject): #FIXME: dont call it 1-by-1 from kooplex.lib.fs_dirname import Dirname container_name = self.dockerconf.get('impersonator', 'impersonator') path, filename = os.path.split(self.dockerconf.get('gitcommandconf', '/tmp/gitcommand.conf')) cmdmaps = [] token = vcproject.token fn_clonesh = os.path.join(Dirname.vcpcache(vcproject), "clone.sh") fn_key = os.path.join(Dirname.userhome(vcproject.token.user), '.ssh', token.fn_rsa) cmdmaps.append("%s:%s:%s:%s" % (token.user.username, fn_key, token.repository.domain, fn_clonesh)) cmdmaps.append('') file_data = "\n".join(cmdmaps).encode('utf8') self._writefile(container_name, path, filename, file_data) def run_container(self, container): docker_container_info = self.get_container(container) if docker_container_info is None: logger.debug("Container did not exist, Creating new one") docker_container_info = self.create_container(container) container_state = docker_container_info['Status'] if container_state == 'Created' or container_state.startswith('Exited'): logger.debug("Starting container") self.start_container(container) def refresh_container_state(self, container): docker_container_info = self.get_container(container) container_state = docker_container_info['State'] logger.debug("Container state %s" % container_state) container.last_message = str(container_state) container.last_message_at = now() container.save() def start_container(self, container): self.client.start(container.name) # we need to retrieve the container state after starting it docker_container_info = self.get_container(container) container_state = docker_container_info['State'] logger.debug("Container state %s" % container_state) container.last_message = str(container_state) container.last_message_at = now() assert container_state == 'running', "Container failed to start: %s" % docker_container_info def stop_container(self, container): try: self.client.stop(container.name) container.last_message = 'Container stopped' except Exception as e: logger.warn("docker container not found by API -- %s" % e) container.last_message = str(e) def remove_container(self, container): try: self.client.remove_container(container.name) container.last_message = 'Container removed' container.last_message_at = now() except Exception as e: logger.warn("docker container not found by API -- %s" % e) container.last_message = str(e) container.last_message_at = now() logger.debug("Container removed %s" % container.name) #FIXME: az execute2 lesz az igazi... def execute(self, container, command): logger.info("execution: %s in %s" % (command, container)) execution = self.client.exec_create(container = container.name, cmd = shlex.split(command)) return self.client.exec_start(execution, detach = False) def execute2(self, container, command): logger.info("execution: %s in %s" % (command, container)) execution = self.client.exec_create(container = container.name, cmd = shlex.split(command)) response = self.client.exec_start(exec_id = execution['Id'], stream = False) check = self.client.exec_inspect(exec_id = execution['Id']) self.check = check if check['ExitCode'] != 0: logger.error('Execution %s in %s failed -- %s' % (command, container, check)) return response.decode()
def delete_instance(docker_api_endpoint, service_name, container_id): c = DockerClient(docker_api_endpoint) c.stop(container_id) c.remove_container(container_id)
class DockerPyClient(DockerClient): def __init__(self, remote, username=None, password=None, email=None): super(DockerPyClient,self).__init__() self.client = Client(base_url=remote, version='1.15') self.log = logging.getLogger(__name__) self.log.debug('password %s, remote = %s, username=%s', password, remote, username) if username: self.client.login(username=username, password=password, email=email) def docker_images(self, filters=None): return self.client.images(filters=filters) def __id(self, ioc): if ioc and 'Id' in ioc: return ioc['Id'] return None def docker_containers(self): return [{ 'Id': cont['Id'], 'Tag': cont['Image'], 'Image': self.__id(self.image(cont['Image'])), 'Names': cont['Names'], 'Ports': cont['Ports'], 'Created': cont['Created'], 'Command': cont['Command'], 'Status': cont['Status'], 'Running': cont['Status'].startswith('Up ') or cont['Status'].startswith('Restarting ') } for cont in self.client.containers(all=True)] def docker_pull(self, image): (repository, tag) = self.tag(image) existing = self.image(image) for line in self.client.pull(repository=repository, stream=True, insecure_registry=True): parsed = json.loads(line) self.log.debug('parsed %s' % parsed) if 'error' in parsed: raise Exception(parsed['error']) # Check if image updated self.flush_images() newer = self.image(image) if not existing or (newer['Id'] != existing['Id']): return True return False def docker_run(self, entry): volumes = ['/var/log/ext'] kwargs = { 'image': entry['image'], 'volumes': volumes, 'detach': True, 'environment': { 'DOCKER_IMAGE': entry['image'] } } if 'name' in entry: kwargs['name'] = entry['name'] if 'env' in entry: kwargs['environment'].update(entry['env']) if 'cpu' in entry: kwargs['cpu_shares'] = entry['cpu'] if 'memory' in entry: kwargs['mem_limit'] = entry['memory'] if 'entrypoint' in entry: kwargs['entrypoint'] = entry['entrypoint'] if 'command' in entry: kwargs['command'] = entry['command'] if 'volumes' in entry: volumes.extend([vol['containerPath'] for vol in entry['volumes'] if 'containerPath' in vol]) volsFrom = [vol['from'] for vol in entry['volumes'] if 'from' in vol] if len(volsFrom): kwargs['volumes_from'] = volsFrom if 'portMappings' in entry: kwargs['ports'] = [p['containerPort'] for p in entry['portMappings']] container = self.client.create_container(**kwargs) self.docker_start(container['Id'], entry) return container['Id'] def docker_start(self, container, entry=None): logsBound = False binds = {} restart_policy = 'on-failure' kwargs = { 'container': container, 'binds': binds } if entry is not None: if 'network' in entry: kwargs['network_mode'] = entry['network'] if 'privileged' in entry: kwargs['privileged'] = entry['privileged'] if 'volumes' in entry: volsFrom = [] for vol in entry['volumes']: if 'from' in vol: volsFrom.append(vol['from']) continue if not 'containerPath' in vol: self.log.warn('No container mount point specified, skipping volume') continue if not 'hostPath' in vol: # Just a local volume, no bindings continue binds[vol['hostPath']] = { 'bind': vol['containerPath'], 'ro': 'mode' in vol and vol['mode'].lower() == 'ro' } if vol['containerPath'] == '/var/log/ext': logsBound = True if len(volsFrom): kwargs['volumes_from'] = volsFrom if 'portMappings' in entry: portBinds = {} for pm in entry['portMappings']: portBinds[pm['containerPort']] = pm['hostPort'] if 'hostPort' in pm else None kwargs['port_bindings'] = portBinds if 'links' in entry: kwargs['links'] = entry['links'] if 'restart' in entry: restart_policy = entry['restart'] kwargs['restart_policy'] = { 'MaximumRetryCount': 0, 'Name': restart_policy } if not logsBound: binds['/var/log/ext/%s' % container] = { 'bind': '/var/log/ext', 'ro': False } self.client.start(**kwargs); def docker_signal(self, container, sig='HUP'): self.client.kill(container, sig) def docker_restart(self, container): self.client.restart(container) def docker_stop(self, container): self.client.stop(container) def docker_rm(self, container): self.client.remove_container(container) def docker_rmi(self, image): # Force removal, sometimes conflicts result from truncated pulls when # dockerup container upgrades/dies self.client.remove_image(image, force=True)
class DockerPyClient(DockerClient): def __init__(self, remote, username=None, password=None, email=None): super(DockerPyClient,self).__init__() self.client = Client(base_url=remote, version='1.15') if username: self.client.login(username=username, password=password, email=email) def docker_images(self, filters=None): return self.client.images(filters=filters) def __id(self, ioc): if ioc and 'Id' in ioc: return ioc['Id'] return None def docker_containers(self): return [{ 'Id': cont['Id'], 'Tag': cont['Image'], 'Image': self.__id(self.image(cont['Image'])), 'Names': cont['Names'], 'Ports': cont['Ports'], 'Created': cont['Created'], 'Command': cont['Command'], 'Status': cont['Status'], 'Running': cont['Status'].startswith('Up ') or cont['Status'].startswith('Restarting ') } for cont in self.client.containers(all=True)] def docker_pull(self, image): (repository, tag) = self.tag(image) existing = self.image(image) for line in self.client.pull(repository=repository, stream=True, insecure_registry=True): parsed = json.loads(line) if 'error' in parsed: raise Exception(parsed['error']) # Check if image updated self.flush_images() newer = self.image(image) if not existing or (newer['Id'] != existing['Id']): return True return False def docker_run(self, entry): volumes = ['/var/log/ext'] kwargs = { 'image': entry['image'], 'volumes': volumes, 'detach': True, 'environment': { 'DOCKER_IMAGE': entry['image'] } } if 'name' in entry: kwargs['name'] = entry['name'] if 'env' in entry: kwargs['environment'].update(entry['env']) if 'cpu' in entry: kwargs['cpu_shares'] = entry['cpu'] if 'memory' in entry: kwargs['mem_limit'] = entry['memory'] if 'entrypoint' in entry: kwargs['entrypoint'] = entry['entrypoint'] if 'command' in entry: kwargs['command'] = entry['command'] if 'volumes' in entry: volumes.extend([vol['containerPath'] for vol in entry['volumes'] if 'containerPath' in vol]) volsFrom = [vol['from'] for vol in entry['volumes'] if 'from' in vol] if len(volsFrom): kwargs['volumes_from'] = volsFrom if 'portMappings' in entry: kwargs['ports'] = [p['containerPort'] for p in entry['portMappings']] container = self.client.create_container(**kwargs) self.docker_start(container['Id'], entry) return container['Id'] def docker_start(self, container, entry=None): logsBound = False binds = {} restart_policy = 'on-failure' kwargs = { 'container': container, 'binds': binds } if entry is not None: if 'network' in entry: kwargs['network_mode'] = entry['network'] if 'privileged' in entry: kwargs['privileged'] = entry['privileged'] if 'volumes' in entry: volsFrom = [] for vol in entry['volumes']: if 'from' in vol: volsFrom.append(vol['from']) continue if not 'containerPath' in vol: self.log.warn('No container mount point specified, skipping volume') continue if not 'hostPath' in vol: # Just a local volume, no bindings continue binds[vol['hostPath']] = { 'bind': vol['containerPath'], 'ro': 'mode' in vol and vol['mode'].lower() == 'ro' } if vol['containerPath'] == '/var/log/ext': logsBound = True if len(volsFrom): kwargs['volumes_from'] = volsFrom if 'portMappings' in entry: portBinds = {} for pm in entry['portMappings']: portBinds[pm['containerPort']] = pm['hostPort'] if 'hostPort' in pm else None kwargs['port_bindings'] = portBinds if 'links' in entry: kwargs['links'] = entry['links'] if 'restart' in entry: restart_policy = entry['restart'] kwargs['restart_policy'] = { 'MaximumRetryCount': 0, 'Name': restart_policy } if not logsBound: binds['/var/log/ext/%s' % container] = { 'bind': '/var/log/ext', 'ro': False } self.client.start(**kwargs); def docker_signal(self, container, sig='HUP'): self.client.kill(container, sig) def docker_restart(self, container): self.client.restart(container) def docker_stop(self, container): self.client.stop(container) def docker_rm(self, container): self.client.remove_container(container) def docker_rmi(self, image): # Force removal, sometimes conflicts result from truncated pulls when # dockerup container upgrades/dies self.client.remove_image(image, force=True)