class ModelLocalContainerExecutionContext: """ Context manager for building and testing models Hides build and deploy process and provides model client """ def __init__(self, model_image): """ Create context :param model_image: docker image id :type model_image: str """ self._image_id = model_image self._docker_client = legion.containers.docker.build_docker_client(None) self._image = self._docker_client.images.get(self._image_id) self._model_id = self._image.labels[legion.containers.headers.DOMAIN_MODEL_ID] self._model_version = self._image.labels[legion.containers.headers.DOMAIN_MODEL_VERSION] self.container = None self.container_id = None self.model_port = None self.client = None def __enter__(self): """ Enter into context :return: self """ try: LOGGER.info('Deploying model {} v {} from image {}'.format(self._model_id, self._model_version, self._image_id)) additional_environment = { **build_environ_for_test_environments() } container_labels = legion.containers.docker.generate_docker_labels_for_container(self._image) ports = {'{}/tcp'.format(os.getenv(*legion.config.LEGION_PORT)): 0} self.container = self._docker_client.containers.run(self._image_id, stdout=True, stderr=True, detach=True, # in the background remove=True, # remove automatically after kill environment=additional_environment, labels=container_labels, ports=ports) self.container_id = self.container.id LOGGER.info('Model image has been deployed') wait = 3 LOGGER.info('Waiting {} sec'.format(wait)) time.sleep(wait) self.container = self._docker_client.containers.get(self.container_id) if self.container.status != 'running': print_docker_container_logs(self.container) raise Exception('Invalid container state: {}'.format(self.container.status)) LOGGER.info('OK') LOGGER.info('Detecting bound ports') ports_information = [item for sublist in self.container.attrs['NetworkSettings']['Ports'].values() for item in sublist] ports_information = [int(x['HostPort']) for x in ports_information] LOGGER.info('Detected ports: {}'.format(', '.join(str(port) for port in ports_information))) if len(ports_information) != 1: raise Exception('Should be only one bound port') self.model_port = ports_information[0] LOGGER.info('Model port: {}'.format(self.model_port)) LOGGER.info('Building client') url = 'http://{}:{}'.format('localhost', self.model_port) LOGGER.info('Target URI is {}'.format(url)) self.client = ModelClient(self._model_id, self._model_version, host=url) LOGGER.info('Getting model information') self.model_information = self.client.info() return self except Exception as build_exception: self.__exit__(build_exception) def __exit__(self, *args): """ Exit from context with cleaning fs temp directory, temporary container and image :param args: list of arguements :return: None """ if self.container: try: LOGGER.info('Finding container') container = self._docker_client.containers.get(self.container.id) print_docker_container_logs(container) try: LOGGER.info('Killing container') container.kill() except Exception as container_kill_exception: LOGGER.info('Cannot kill container: {}'.format(container_kill_exception)) except Exception as removing_exception: LOGGER.exception('Cannot remove container: {}'.format(removing_exception), exc_info=removing_exception) if args[0]: raise args[0]
class ModelTestDeployment: """ Context manager for building and testing models Hides build and deploy process and provides model client """ def __init__(self, model_id, model_version, model_builder, wheel): """ Create context :param model_id: id of model (uses for building model and model client) :type model_id: str :param model_version: version of model (passes to model builder) :param model_builder: str :param wheel: path to actual package wheel :type wheel: str """ self._model_id = model_id self._model_version = model_version self._model_builder = model_builder self._wheel = wheel self._docker_client = legion.containers.docker.build_docker_client( None) self._temp_directory = tempfile.mkdtemp() self._model_path = os.path.join(self._temp_directory, 'temp.model') self.image = None self.container = None self.container_id = None self.model_port = None self.client = None def __enter__(self): """ Enter into context :return: self """ try: print('Building model file {} v {}'.format(self._model_id, self._model_version)) legion.model.model_id.init(self._model_id) self._model_builder(self._model_path, self._model_version) print('Model file has been built') print('Building model image {} v {}'.format( self._model_id, self._model_version)) args = Namespace(model_file=self._model_path, model_id=None, base_docker_image=None, docker_network=None, python_package=self._wheel, python_package_version=None, python_repository=None, docker_image_tag=None, serving=deploy.VALID_SERVING_WORKERS[1], push_to_registry=None) self.image = deploy.build_model(args) print('Model image has been built') print('Deploying model {} v {}'.format(self._model_id, self._model_version)) additional_environment = { legion.config.REGISTER_ON_GRAFANA[0]: 'false', legion.config.REGISTER_ON_CONSUL[0]: 'false', } with patch_environ(additional_environment): args = Namespace(model_id=self._model_id, docker_image=None, docker_network=None, grafana_server=None, grafana_user=None, grafana_password=None, expose_model_port=0) self.container = deploy.deploy_model(args) self.container_id = self.container.id print('Model image has been deployed') wait = 3 print('Waiting {} sec'.format(wait)) time.sleep(wait) self.container = self._docker_client.containers.get( self.container_id) if self.container.status != 'running': try: logs = self.container.logs().decode('utf-8') print('--- CONTAINER LOGS ---') print(logs) except Exception: print('Cannot get logs of container') raise Exception('Invalid container state: {}'.format( self.container.status)) print('OK') print('Detecting bound ports') ports_information = [ item for sublist in self.container.attrs['NetworkSettings'] ['Ports'].values() for item in sublist ] ports_information = [int(x['HostPort']) for x in ports_information] print('Detected ports: {}'.format(', '.join( str(port) for port in ports_information))) if len(ports_information) != 1: raise Exception('Should be only one bound port') self.model_port = ports_information[0] print('Model port: {}'.format(self.model_port)) print('Building client') url = 'http://{}:{}'.format('localhost', self.model_port) print('Target URI is {}'.format(url)) self.client = ModelClient(self._model_id, url) print('Getting model information') self.model_information = self.client.info() return self except Exception as build_exception: self.__exit__(build_exception) def __exit__(self, *args): """ Exit from context with cleaning fs temp directory, temporary container and image :param args: list of arguements :return: None """ print('Removing temporary directory') remove_directory(self._temp_directory) if self.container: try: print('Finding container') container = self._docker_client.containers.get( self.container.id) print('Stopping container') container.stop() print('Removing container') container.remove() except Exception as removing_exception: print('Cannot remove container: {}'.format(removing_exception)) if self.image: try: print('Removing image') self._docker_client.images.remove(self.image.id) except Exception as removing_exception: print('Cannot remove image: {}'.format(removing_exception)) if args[0]: raise args[0]
def inspect(cluster_config, cluster_secrets, namespace=None): """ Get model deployments information :param cluster_config: cluster configuration :type cluster_config: dict :param cluster_secrets: secrets with credentials :type cluster_secrets: dict[str, str] :param namespace: :return: list[:py:class:`legion.containers.k8s.ModelDeploymentDescription`] """ deployments = find_all_models_deployments(namespace) models = [] edge_url = 'http://%s:%d' % (cluster_config['edge']['domain'], cluster_config['edge']['port']) for deployment in deployments: ready_replicas = deployment.status.ready_replicas if not ready_replicas: ready_replicas = 0 replicas = deployment.status.replicas if not replicas: replicas = 0 status = 'ok' if ready_replicas == 0: status = 'fail' elif replicas > ready_replicas > 0: status = 'warning' container_image = deployment.spec.template.spec.containers[0].image model_name = deployment.metadata.labels.get( normalize_name(legion.containers.headers.DOMAIN_MODEL_ID), '?') model_version = deployment.metadata.labels.get( normalize_name(legion.containers.headers.DOMAIN_MODEL_VERSION), '?') model_api_info = {'host': edge_url} try: model_client = ModelClient(model_name, host=edge_url) model_api_info['result'] = model_client.info() model_api_ok = True except Exception as model_api_exception: model_api_info['exception'] = str(model_api_exception) model_api_ok = False model_information = ModelDeploymentDescription( status=status, model=model_name, version=model_version, image=container_image, scale=replicas, ready_replicas=ready_replicas, namespace=deployment.metadata.namespace, deployment='deployment', model_api_ok=model_api_ok, model_api_info=model_api_info) models.append(model_information) return models