def get_conf() -> Namespace:
    """Returns the conf singleton."""
    return _CONF class Service(BaseRecord):
    """A Zoe Service."""

    CREATED_STATUS = 'created'
    TERMINATING_STATUS = "terminating"
    INACTIVE_STATUS = "inactive"
    ACTIVE_STATUS = "active"
    STARTING_STATUS = "starting"
    ERROR_STATUS = "error"
    RUNNABLE_STATUS = "runnable"

    BACKEND_UNDEFINED_STATUS = 'undefined'
    BACKEND_CREATE_STATUS = 'created'
    BACKEND_START_STATUS = 'started'
    BACKEND_DIE_STATUS = 'dead'
    BACKEND_DESTROY_STATUS = 'destroyed'
    BACKEND_OOM_STATUS = 'oom-killed'

    def __init__(self, d, sql_manager):
        super().__init__(d, sql_manager)

        self.name = d['name']
        self.status = d['status']
        self.error_message = d['error_message']
        self.execution_id = d['execution_id']
        self.description = d['description']
        self.service_group = d['service_group']
        self.backend_id = d['backend_id']
        self.backend_status = d['backend_status']
        self.backend_host = d['backend_host']
        self.restart_count = d['restart_count']

        self.ip_address = d['ip_address']
        if self.ip_address is not None and ('/32' in self.ip_address or '/128' in self.ip_address):
            self.ip_address = self.ip_address.split('/')[0]

        self.essential = d['essential']

        # Fields parsed from the JSON description
        self.image_name = self.description['image']
        self.is_monitor = self.description['monitor']
        self.startup_order = self.description['startup_order']
        self.environment = self.description['environment']
        self.command = self.description['command']
        self.resource_reservation = ResourceReservation(self.description['resources'])
        self.volumes = [VolumeDescriptionHostPath(v['name'], v['path'], v['read_only']) for v in self.description['volumes']]

        # Optional zapp fields
        try:
            self.work_dir = self.description['work_dir']
        except KeyError:
            self.work_dir = None

        try:
            self.labels = self.description['labels']
        except KeyError:
            self.labels = []

        try:
            self.network = self.description['network']
        except KeyError:
            self.network = None

    def serialize(self):
        """Generates a dictionary that can be serialized in JSON."""
        return {
            'id': self.id,
            'name': self.name,
            'status': self.status,
            'error_message': self.error_message,
            'execution_id': self.execution_id,
            'description': self.description,
            'service_group': self.service_group,
            'backend_id': self.backend_id,
            'ip_address': self.ip_address,
            'backend_status': self.backend_status,
            'backend_host': self.backend_host,
            'essential': self.essential,
            'proxy_address': self.proxy_address,
            'restart_count': self.restart_count
        }

    def __eq__(self, other):
        return self.id == other.id

    def set_terminating(self):
        """The service is being terminated."""
        self.sql_manager.services.update(self.id, status=self.TERMINATING_STATUS)
        self.status = self.TERMINATING_STATUS

    def set_inactive(self):
        """The service must not run."""
        self.sql_manager.services.update(self.id, status=self.INACTIVE_STATUS)
        self.status = self.INACTIVE_STATUS

    def set_starting(self):
        """The service is being started by the back-end."""
        self.sql_manager.services.update(self.id, status=self.STARTING_STATUS)
        self.status = self.STARTING_STATUS

    def set_runnable(self):
        """The service is elastic and can be started."""
        self.sql_manager.services.update(self.id, status=self.RUNNABLE_STATUS)
        self.status = self.RUNNABLE_STATUS

    def set_active(self, backend_id, ip_address, ports):
        """The service is running and has a valid backend_id."""
        self.sql_manager.services.update(self.id, status=self.ACTIVE_STATUS, backend_id=backend_id, error_message=None, ip_address=ip_address, backend_status=self.BACKEND_START_STATUS)
        self.error_message = None
        self.ip_address = ip_address
        self.backend_id = backend_id
        self.status = self.ACTIVE_STATUS
        self.backend_status = self.BACKEND_START_STATUS
        for port in self.ports:
            if port.internal_name in ports and ports[port.internal_name] is not None:
                port.activate(ip_address, ports[port.internal_name])
            else:
                port.reset()

    def set_error(self, error_message):
        """The service could not be created/started."""
        self.sql_manager.services.update(self.id, status=self.ERROR_STATUS, error_message=error_message)
        self.status = self.ERROR_STATUS
        self.error_message = error_message

    def set_backend_status(self, new_status):
        """The backend status of the service has changed."""
        log.debug("service {}, backend status updated to {}".format(self.id, new_status))
        self.backend_status = new_status
        if self.is_dead():
            for port in self.ports:
                port.reset()
            self.ip_address = None
            self.sql_manager.services.update(self.id, backend_status=new_status, ip_address=None)
            if new_status == self.BACKEND_DESTROY_STATUS:
                self.backend_id = None
                self.sql_manager.services.update(self.id, backend_status=new_status, backend_id=None, ip_address=None)
            else:
                self.sql_manager.services.update(self.id, backend_status=new_status, ip_address=None)
        else:
            self.sql_manager.services.update(self.id, backend_status=new_status)

    def assign_backend_host(self, backend_host):
        """Assign this service to a host in particular."""
        self.sql_manager.services.update(self.id, backend_host=backend_host)
        self.backend_host = backend_host

    @property
    def dns_name(self):
        """Getter for the DNS name of this service as it will be registered in Docker's DNS."""
        return "{}-{}-{}".format(self.name, self.execution_id, get_conf().deployment_name)

    @property
    def user_id(self):
        """Getter for the user_id, that is actually taken from the parent execution."""
        execution = self.sql_manager.executions.select(only_one=True, id=self.execution_id)
        return execution.user_id

    @property
    def ports(self):
        """Getter for the ports exposed by this service."""
        return self.sql_manager.ports.select(service_id=self.id)

    @property
    def proxy_address(self):
        """Get proxy address path"""
        if len(self.ports) > 0:
            return self.name + "-" + str(self.execution_id) + "-" + get_conf().deployment_name + "." + get_conf().proxy_path
        else:
            return None

    def is_dead(self):
        """Returns True if this service is not running."""
        return self.backend_status != self.BACKEND_START_STATUS

    @property
    def unique_name(self):
        """Returns a name for this service that is unique across multiple Zoe instances running on the same backend."""
        return self.name + '-' + str(self.execution_id) + '-' + get_conf().deployment_name

    @property
    def execution(self):
        """Return the parent execution."""
        return self.sql_manager.executions.select(only_one=True, id=self.execution_id)

    def restarted(self):
        """The service has restarted, keep track in the database."""
        self.restart_count += 1
        self.sql_manager.services.update(self.id, restart_count=self.restart_count) class DockerClient:
    """The client class that wraps the Docker API."""
    def __init__(self, docker_config: DockerHostConfig, mock_client=None) -> None:
        self.name = docker_config.name
        self.docker_config = docker_config
        if not docker_config.tls:
            tls = None
        else:
            tls = docker.tls.TLSConfig(client_cert=(docker_config.tls_cert, docker_config.tls_key), verify=docker_config.tls_ca)

        # Simplify testing
        if mock_client is not None:
            self.cli = mock_client
            return

        try:
            self.cli = docker.DockerClient(base_url=docker_config.address, version="auto", tls=tls)
        except docker.errors.DockerException as e:
            raise ZoeException("Cannot connect to Docker host {} at address {}: {}".format(docker_config.name, docker_config.address, str(e)))

    def info(self) -> Dict:
        """Retrieve engine statistics."""
        return self.cli.info()

    def spawn_container(self, service_instance: ServiceInstance) -> Dict[str, Any]:
        """Create and start a new container."""
        run_args = {
            'detach': True,
            'ports': {},
            'environment': {},
            'volumes': {},
            'working_dir': service_instance.work_dir,
            'mem_limit': 0,
            'mem_reservation': 0,
            'memswap_limit': 0,
            'name': service_instance.name,
            'network_disabled': False,
            'network_mode': service_instance.network,
            'image': service_instance.image_name,
            'command': service_instance.command,
            'hostname': service_instance.hostname,
            'labels': service_instance.labels,
            'cpu_period': 100000,
            'cpu_quota': 100000,
            'log_config': {
                "type": "json-file",
                "config": {}
            }
        }
        for port in service_instance.ports:
            run_args['ports'][str(port.number) + '/' + port.proto] = None

        for name, value in service_instance.environment:
            run_args['environment'][name] = value

        for volume in service_instance.volumes:
            if volume.type == "host_directory":
                assert isinstance(volume, VolumeDescriptionHostPath)
                run_args['volumes'][volume.path] = {'bind': volume.mount_point, 'mode': ("ro" if volume.readonly else "rw")}
            else:
                log.error('Docker backend does not support volume type {}'.format(volume.type))

        if service_instance.memory_limit is not None:
            run_args['mem_limit'] = service_instance.memory_limit.min
#            run_args['mem_reservation'] = service_instance.memory_limit.min
#            if service_instance.memory_limit.max == service_instance.memory_limit.min:
#                run_args['mem_reservation'] -= 1

        if service_instance.core_limit is not None:
            run_args['cpu_quota'] = int(100000 * service_instance.core_limit.min)

        if service_instance.shm_size > 0:
            run_args['shm_size'] = service_instance.shm_size

        if get_conf().gelf_address != '':
            run_args['log_config'] = {
                "type": "gelf",
                "config": {
                    'gelf-address': get_conf().gelf_address,
                    'labels': ",".join(service_instance.labels)
                }
            }

        cont = None
        try:
            cont = self.cli.containers.run(**run_args)
        except docker.errors.ImageNotFound:
            raise ZoeException(message='Image not found')
        except docker.errors.APIError as e:
            if cont is not None:
                cont.remove(force=True)
            if e.explanation == b'no resources available to schedule container':
                raise ZoeNotEnoughResourcesException(message=str(e))
            else:
                raise ZoeException(message=str(e))
        except Exception as e:
            if cont is not None:
                cont.remove(force=True)
            raise ZoeException(str(e))

        cont = self.cli.containers.get(cont.id)
        return self._container_summary(cont)

    def _container_summary(self, container: docker.models.containers.Container):
        """Translate a docker-specific container object into a simple dictionary."""
        info = {
            "id": container.id,
            "ip_address": {},
            "name": container.name,
            'labels': container.attrs['Config']['Labels'],
            'external_address': self.docker_config.external_address
        }  # type: Dict[str, Any]
        try:
            info['host'] = container.attrs['Node']['Name']
        except KeyError:
            info['host'] = 'N/A'

        if container.status == 'running' or container.status == 'restarting' or container.status == 'removing':
            info["state"] = Service.BACKEND_START_STATUS
            info["running"] = True
        elif container.status == 'paused' or container.status == 'exited' or container.status == 'dead':
            info["state"] = Service.BACKEND_DIE_STATUS
            info["running"] = False
        elif container.status == 'OOMKilled':
            info["state"] = Service.BACKEND_OOM_STATUS
            info["running"] = False
        elif container.status == 'created':
            info["state"] = Service.BACKEND_CREATE_STATUS
            info["running"] = False
        else:
            log.error('Unknown container status: {}'.format(container.status))
            info["state"] = Service.BACKEND_UNDEFINED_STATUS
            info["running"] = False

        info['ports'] = {}
        if 'Ports' in container.attrs['NetworkSettings'] and container.attrs['NetworkSettings']['Ports'] is not None:
            for port in container.attrs['NetworkSettings']['Ports']:
                if container.attrs['NetworkSettings']['Ports'][port] is not None:
                    info['ports'][port] = container.attrs['NetworkSettings']['Ports'][port][0]['HostPort']
                else:
                    info['ports'][port] = None

        info['cpu_period'] = container.attrs['HostConfig']['CpuPeriod']
        info['cpu_quota'] = container.attrs['HostConfig']['CpuQuota']
        info['memory_hard_limit'] = container.attrs['HostConfig']['Memory']
        info['memory_soft_limit'] = container.attrs['HostConfig']['MemoryReservation']

        return info

    def inspect_container(self, docker_id: str) -> Dict[str, Any]:
        """Retrieve information about a running container."""
        try:
            cont = self.cli.containers.get(docker_id)
        except Exception as e:
            raise ZoeException(str(e))
        return self._container_summary(cont)

    def terminate_container(self, docker_id: str, delete=False) -> None:
        """
        Terminate a container.

        :param docker_id: The container to terminate
        :type docker_id: str
        :param delete: If True, also delete the container files
        :type delete: bool
        :return: None
        """
        try:
            cont = self.cli.containers.get(docker_id)
        except docker.errors.NotFound:
            return

        try:
            if delete:
                cont.remove(force=True)
            else:
                cont.stop(timeout=5)
        except docker.errors.NotFound:
            pass
        except docker.errors.APIError as e:
            log.warning(str(e))

    def event_listener(self, callback: Callable[[str], bool]) -> None:
        """An infinite loop that listens for events from Swarm."""
        event_gen = self.cli.events(decode=True)
        while True:
            try:
                event = next(event_gen)
            except requests.exceptions.RequestException:
                log.warning('Docker closed event connection, retrying...')
                event_gen = self.cli.events(decode=True)
                continue

            try:
                res = callback(event)
            except Exception:
                log.exception('Uncaught exception in swarm event callback')
                log.warning('event was: {}'.format(event))
                continue
            if not res:
                break

    def list(self, only_label=None, status=None) -> List[dict]:
        """
        List running or defined containers.

        :param only_label: filter containers with only a certain label
        :param status: filter containers with only a certain status (one of restarting, running, paused, exited)
        :return: a list of containers
        """
        filters = {}
        if only_label is not None:
            filters['label'] = only_label
        if status is not None:
            filters['status'] = status
        try:
            ret = self.cli.containers.list(all=True, filters=filters)
        except docker.errors.APIError as ex:
            raise ZoeException(str(ex))
        except requests.exceptions.RequestException as ex:
            raise ZoeException(str(ex))
        conts = []
        for cont_info in ret:
            conts.append(self._container_summary(cont_info))

        return conts

    def stats(self, docker_id: str, stream: bool):
        """Retrieves container stats based on resource usage."""
        try:
            cont = self.cli.containers.get(docker_id)
        except docker.errors.NotFound:
            raise ZoeException('Container not found')
        except docker.errors.APIError as e:
            raise ZoeException('Docker API error: {}'.format(e))

        try:
            return cont.stats(stream=stream)
        except docker.errors.APIError as e:
            raise ZoeException('Docker API error: {}'.format(e))
        except requests.exceptions.ReadTimeout:
            raise ZoeException('Read timeout')
        except ValueError:
            raise ZoeException('Docker API decoding error')

    def logs(self, docker_id: str, stream: bool, follow=None):
        """
        Retrieves the logs of the selected container.

        :param docker_id:
        :param stream:
        :param follow:
        :return:
        """
        try:
            cont = self.cli.containers.get(docker_id)
        except (docker.errors.NotFound, docker.errors.APIError):
            return None

        try:
            return cont.logs(stdout=True, stderr=True, follow=follow, stream=stream, timestamps=True, tail='all')
        except docker.errors.APIError:
            return None

    def list_images(self):
        """Retrieve the list of images available on this node."""
        try:
            return self.cli.images.list()
        except (docker.errors.NotFound, docker.errors.APIError):
            return []

    def pull_image(self, image_name):
        """Pulls an image in the docker engine."""
        try:
            self.cli.images.pull(image_name)
        except docker.errors.APIError as e:
            log.error('Cannot download image {}: {}'.format(image_name, e))
            raise ZoeException('Cannot download image {}: {}'.format(image_name, e))

    def update(self, docker_id, cpu_quota=None, mem_reservation=None, mem_limit=None):
        """Update the resource reservation for a container."""
        kwargs = {}
        if cpu_quota is not None:
            kwargs['cpu_quota'] = cpu_quota
        if mem_reservation is not None:
            kwargs['mem_reservation'] = mem_reservation
        if mem_limit is not None:
            kwargs['mem_limit'] = mem_limit

        try:
            cont = self.cli.containers.get(docker_id)
        except (docker.errors.NotFound, docker.errors.APIError):
            return

        try:
            cont.update(**kwargs)
        except docker.errors.APIError:
            pass class DockerConfig:
    """A class that holds the configuration for the Docker Engine backend."""
    def __init__(self, config_file):
        self.conffile = config_file

    def read_config(self) -> List[DockerHostConfig]:
        """Parse the configuration file."""
        config = configparser.ConfigParser()
        config.read(self.conffile)
        hosts = []
        for section in config.sections():
            host = DockerHostConfig()
            host.name = section
            try:
                host.address = config[section]['docker_address']
                host.external_address = config[section]['external_address']
                host.tls = config.getboolean(section, 'use_tls')
                if host.tls:
                    host.tls_cert = config[section]['tls_cert']
                    host.tls_ca = config[section]['tls_ca']
                    host.tls_key = config[section]['tls_key']
            except KeyError as e:
                log.error('Error in Docker backend configuration, missing key {} in section {}'.format(e.args[0], section))
                continue

            if 'labels' in config[section]:  # labels are optional
                host.labels = set(config[section]['labels'].split(','))

            hosts.append(host)
        if len(hosts) == 0:
            log.error('Host list is empty, verify your docker backend configuration!')
        return hosts class DockerHostConfig:
    """A class that holds static information about a host."""
    def __init__(self):
        self.name = None
        self.address = None
        self.external_address = None
        self.tls = False
        self.tls_cert = None
        self.tls_key = None
        self.tls_ca = None
        self.labels = set() class DockerStateSynchronizer(threading.Thread):
    """The Docker Checker."""

    def __init__(self, state: SQLManager) -> None:
        super().__init__()
        self.setName('checker')
        self.stop = threading.Event()
        self.my_stop = threading.Event()
        self.state = state
        self.setDaemon(True)
        self.host_checkers = []
        self.host_stats = {}
        for docker_host in DockerConfig(get_conf().backend_docker_config_file).read_config():
            th = threading.Thread(target=self._host_subthread, args=(docker_host,), name='synchro_' + docker_host.name, daemon=True)
            th.start()
            self.host_checkers.append((th, docker_host))

        self.start()

    def _host_subthread(self, host_config: DockerHostConfig):  # pylint: disable=too-many-locals
        log.info("Synchro thread for host {} started".format(host_config.name))

        self.host_stats[host_config.name] = NodeStats(host_config.name)

        while True:
            time_start = time.time()
            try:
                my_engine = DockerClient(host_config)
                container_list = my_engine.list(only_label='zoe_deployment_name={}'.format(get_conf().deployment_name))
                running_container_list = my_engine.list(status='running')
                info = my_engine.info()
            except ZoeException as e:
                self.host_stats[host_config.name].status = 'offline'
                log.error(str(e))
                log.info('Node {} is offline'.format(host_config.name))
            else:
                if self.host_stats[host_config.name].status == 'offline':
                    log.info('Node {} is now online'.format(host_config.name))
                    self.host_stats[host_config.name].status = 'online'

                self.host_stats[host_config.name].container_count = len(running_container_list)
                self.host_stats[host_config.name].cores_total = info['NCPU']
                self.host_stats[host_config.name].memory_total = info['MemTotal']
                self.host_stats[host_config.name].labels = host_config.labels
                if info['Labels'] is not None:
                    self.host_stats[host_config.name].labels.union(set(info['Labels']))

                self.host_stats[host_config.name].memory_allocated = sum([cont['memory_hard_limit'] for cont in container_list if cont['memory_hard_limit'] != info['MemTotal']])
                self.host_stats[host_config.name].cores_allocated = sum([cont['cpu_quota'] / cont['cpu_period'] for cont in container_list if cont['cpu_period'] != 0])

                stats = {}
                tmp_memory_reserved = 0
                tmp_cores_reserved = 0
                for cont in container_list:
                    service = self.state.services.select(only_one=True, backend_host=host_config.name, backend_id=cont['id'])
                    if service is None:
                        log.warning('Container {} on host {} has no corresponding service'.format(cont['name'], host_config.name))
                        if cont['state'] == Service.BACKEND_DIE_STATUS:
                            log.warning('Terminating dead and orphan container {}'.format(cont['name']))
                            my_engine.terminate_container(cont['id'], delete=True)
                        continue
                    if service.status == service.TERMINATING_STATUS:
                        if service.backend_id is not None:
                            my_engine.terminate_container(service.backend_id, delete=True)
                        else:
                            service.set_inactive()

                    self._update_service_status(service, cont)
                    tmp_memory_reserved += service.resource_reservation.memory.min
                    tmp_cores_reserved += service.resource_reservation.cores.min
                    stats[service.id] = {
                        'core_limit': cont['cpu_quota'] / cont['cpu_period'],
                        'mem_limit': cont['memory_hard_limit']
                    }
                self.host_stats[host_config.name].memory_reserved = tmp_memory_reserved
                self.host_stats[host_config.name].cores_reserved = tmp_cores_reserved
                self.host_stats[host_config.name].service_stats = stats

                tmp_images = []
                for dk_image in my_engine.list_images():
                    image = {
                        'id': dk_image.attrs['Id'],
                        'size': dk_image.attrs['Size'],
                        'names': dk_image.tags
                    }
                    for name in image['names']:
                        if name[-7:] == ':latest':  # add an image with the name without 'latest' to fake Docker image lookup algorithm
                            image['names'].append(name[:-7])
                            break
                    tmp_images.append(image)
                self.host_stats[host_config.name].images = tmp_images
                self.host_stats[host_config.name].timestamp = time_start
                self.host_stats[host_config.name].valid = True

            sleep_time = CHECK_INTERVAL - (time.time() - time_start)
            if sleep_time <= 0:
                log.warning('synchro thread for host {} is late by {:.2f} seconds'.format(host_config.name, sleep_time * -1))
                sleep_time = 0
            if self.stop.wait(timeout=sleep_time):
                break

        log.info("Synchro thread for host {} stopped".format(host_config.name))

    def _update_service_status(self, service: Service, container):
        """Update the service status."""
        if service.backend_status != container['state']:
            old_status = service.backend_status
            service.set_backend_status(container['state'])
            log.debug('Updated service status, {} from {} to {}'.format(service.name, old_status, container['state']))

    def run(self):
        """The thread loop."""
        log.info("Checker thread started")
        while True:
            ret = self.my_stop.wait(timeout=CHECK_INTERVAL)
            if ret:
                break
            to_remove = []
            to_add = []
            for th, conf in self.host_checkers:
                if not th.is_alive():
                    log.warning('Thread {} has died, starting a new one.'.format(th.name))
                    to_remove.append((th, conf))
                    th = threading.Thread(target=self._host_subthread, args=(conf,), name='synchro_' + conf.name, daemon=True)
                    th.start()
                    to_add.append((th, conf))
            for dead_th in to_remove:
                self.host_checkers.remove(dead_th)
            for new_th in to_add:
                self.host_checkers.append(new_th)
        log.info("Checker thread stopped")

    def quit(self):
        """Stops the thread."""
        self.stop.set()
        for th, conf_ in self.host_checkers:
            th.join()
        self.my_stop.set()
        self.join() class ServiceInstance:
    """The ServiceInstance class, a Service that is going to be instantiated into a container."""
    def __init__(self, execution: Execution, service: Service, env_subst_dict):
        self.name = service.unique_name
        self.hostname = service.dns_name
        self.backend_host = service.backend_host

        if service.resource_reservation.memory.min is None:
            self.memory_limit = None
        else:
            self.memory_limit = service.resource_reservation.memory
            if self.memory_limit.max > get_conf().max_memory_limit * (1024 ** 3):
                self.memory_limit.max = get_conf().max_memory_limit * (1024 ** 3)

        if service.resource_reservation.cores.min is None:
            self.core_limit = None
        else:
            self.core_limit = service.resource_reservation.cores
            if self.core_limit.max > get_conf().max_core_limit:
                self.core_limit = get_conf().max_core_limit

        self.shm_size = service.resource_reservation.shm

        self.labels = {
            'zoe.execution.name': execution.name,
            'zoe.execution.id': str(execution.id),
            'zoe.service.name': service.name,
            'zoe.service.id': str(service.id),
            'zoe.owner': execution.owner.username,
            'zoe.deployment_name': get_conf().deployment_name,
            'zoe.type': 'service_{}'.format('essential' if service.essential else 'elastic'),
            'zoe.zapp_size': execution.size
        }
        if service.is_monitor:
            self.labels['zoe_monitor'] = 'true'
        else:
            self.labels['zoe_monitor'] = 'false'

        self.labels = zoe_master.backends.common.gen_labels(service, execution)
        self.environment = service.environment + zoe_master.backends.common.gen_environment(execution, service, env_subst_dict)
        self.volumes = zoe_master.backends.common.gen_volumes(service, execution)

        self.command = service.command

        self.work_dir = service.work_dir

        self.image_name = service.image_name

        self.ports = []
        for port in service.ports:
            self.ports.append(BackendPort(port.internal_number, port.protocol))

        if service.network is not None:
            self.network = service.network
        else:
            self.network = get_conf().overlay_network_name class ZoeStartExecutionRetryException(ZoeException):
    """Execution emitted in case the Execution failed to start for a transient error.""" class ZoeStartExecutionFatalException(ZoeException):
    """Execution emitted in case the Execution failed to start for a fatal error.""" class ZoeException(Exception):
    """
    A generic exception.
    """
    def __init__(self, message='Something happened'):
        super().__init__()
        self.message = message

    def __str__(self):
        return self.message class ZoeNotEnoughResourcesException(ZoeException):
    """Service failed to start due to not enough available resources.""" class ClusterStats(Stats):
    """Stats related to the whole cluster."""
    def __init__(self):
        super().__init__()
        self.nodes = []

    def serialize(self):
        """Convert the object into a dict."""
        return {
            'container_count': self.container_count,
            'memory_total': self.memory_total,
            'cores_total': self.cores_total,
            'memory_reserved': sum([n.memory_reserved for n in self.nodes]),
            'cores_reserved': sum([n.cores_reserved for n in self.nodes]),
            'memory_in_use': sum([n.memory_in_use for n in self.nodes]),
            'cores_in_use': sum([n.cores_in_use for n in self.nodes]),
            'nodes': [x.serialize() for x in self.nodes]
        }

    @property
    def memory_total(self) -> int:
        """Total memory installed in the whole platform."""
        return sum([node.memory_total for node in self.nodes])

    @property
    def cores_total(self) -> int:
        """Total number of cores installed."""
        return sum([node.cores_total for node in self.nodes])

    @property
    def memory_reserved(self) -> int:
        """Total memory reserved in the whole platform."""
        return sum([node.memory_reserved for node in self.nodes])

    @property
    def cores_reserved(self) -> int:
        """Total number of cores reserved."""
        return sum([node.cores_reserved for node in self.nodes])

    @property
    def memory_in_use(self) -> int:
        """Total memory in use in the whole platform."""
        return sum([node.memory_in_use for node in self.nodes])

    @property
    def cores_in_use(self) -> int:
        """Total number of cores in use."""
        return sum([node.cores_in_use for node in self.nodes])

    @property
    def container_count(self) -> int:
        """Total number of containers."""
        return sum([node.container_count for node in self.nodes])
import logging
import re
import time
import zoe_master.backends.base
from typing import Union
from zoe_lib.config import get_conf
from zoe_lib.state import Service
from zoe_master.backends.docker.api_client import DockerClient
from zoe_master.backends.docker.config import DockerConfig, DockerHostConfig  # pylint: disable=unused-import
from zoe_master.backends.docker.threads import DockerStateSynchronizer
from zoe_master.backends.service_instance import ServiceInstance
from zoe_master.exceptions import ZoeStartExecutionRetryException, ZoeStartExecutionFatalException, ZoeException, ZoeNotEnoughResourcesException
from zoe_master.stats import ClusterStats
# Copyright (c) 2017, Daniele Venzano
#
# 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.

"""Zoe backend implementation for one or more Docker Engines."""



log = logging.getLogger(__name__)

# This module-level variable holds the references to the synchro threads
_checker = None


class DockerEngineBackend(zoe_master.backends.base.BaseBackend):
    """Zoe backend implementation for old-style stand-alone Docker Swarm."""
    def __init__(self, opts):
        super().__init__(opts)
        self.docker_config = DockerConfig(get_conf().backend_docker_config_file).read_config()

    def _get_config(self, host) -> Union[DockerHostConfig, None]:
        for conf in self.docker_config:
            if conf.name == host:
                return conf
        return None

    @classmethod
    def init(cls, state):
        """Initializes Swarm backend starting the event monitoring thread."""
        global _checker
        _checker = DockerStateSynchronizer(state)

    @classmethod
    def shutdown(cls):
        """Performs a clean shutdown of the resources used by Swarm backend."""
        _checker.quit()

    def spawn_service(self, service_instance: ServiceInstance):
        """Spawn a service, translating a Zoe Service into a Docker container."""
        parsed_name = re.search(r'^(?:([^/]+)/)?(?:([^/]+)/)?([^@:/]+)(?:[@:](.+))?$', service_instance.image_name)
        if parsed_name.group(4) is None:

            raise ZoeStartExecutionFatalException('Image {} does not have a version tag'.format(service_instance.image_name))