def _update_service(service_name, service_config): if url and namespace: # Reference previously pushed image image_id = self.get_latest_image_id_for_service(service_name) if not image_id: raise exceptions.AnsibleContainerConductorException( u"Unable to get image ID for service {}. Did you forget to run " u"`ansible-container build`?".format(service_name) ) image_tag = tag or self.get_build_stamp_for_image(image_id) if repository_prefix: image_name = "{}-{}".format(repository_prefix, service_name) elif repository_prefix is None: image_name = "{}-{}".format(self.project_name, service_name) else: image_name = service_name repository = "{}/{}".format(namespace, image_name) image_name = "{}:{}".format(repository, image_tag) pull_url = pull_from_url if pull_from_url else url service_config['image'] = "{}/{}".format(pull_url.rstrip('/'), image_name) else: # We're using a local image, so check that the image was built image = self.get_latest_image_for_service(service_name) if image is None: raise exceptions.AnsibleContainerConductorException( u"No image found for service {}, make sure you've run `ansible-container " u"build`".format(service_name) ) service_config['image'] = image.tags[0]
def login(self, username, password, email, url, config_path): """ If username and password are provided, authenticate with the registry. Otherwise, check the config file for existing authentication data. """ if username and password: try: self.client.login(username=username, password=password, email=email, registry=url, reauth=True) except docker_errors.APIError as exc: raise exceptions.AnsibleContainerConductorException( u"Error logging into registry: {}".format(exc)) except Exception: raise self._update_config_file(username, password, email, url, config_path) username, password = self._get_registry_auth(url, config_path) if not username: raise exceptions.AnsibleContainerConductorException( u'Please provide login credentials for registry {}.'.format( url)) return username, password
def get_build_stamp_for_image(self, image_id): build_stamp = None try: image = self.client.images.get(image_id) except docker_errors.ImageNotFound: raise exceptions.AnsibleContainerConductorException( "Unable to find image {}".format(image_id) ) if image and image.tags: build_stamp = [tag for tag in image.tags if not tag.endswith(':latest')][0].split(':')[-1] return build_stamp
def await_conductor_command(self, command, config, base_path, params, save_container=False): conductor_id = self.run_conductor(command, config, base_path, params) try: while self.service_is_running('conductor'): time.sleep(0.1) finally: exit_code = self.service_exit_code('conductor') msg = 'Preserving as requested.' if save_container else 'Cleaning up.' logger.info('Conductor terminated. {}'.format(msg), save_container=save_container, conductor_id=conductor_id, command_rc=exit_code) if not save_container: self.delete_container(conductor_id, remove_volumes=True) if exit_code: raise exceptions.AnsibleContainerConductorException( u'Conductor exited with status %s' % exit_code )
def _update_config_file(username, password, email, url, config_path): """Update the config file with the authorization.""" try: # read the existing config config = json.load(open(config_path, "r")) except ValueError: config = dict() if not config.get('auths'): config['auths'] = dict() if not config['auths'].get(url): config['auths'][url] = dict() encoded_credentials = dict(auth=base64.b64encode(username + b':' + password), email=email) config['auths'][url] = encoded_credentials try: json.dump(config, open(config_path, "w"), indent=5, sort_keys=True) except Exception as exc: raise exceptions.AnsibleContainerConductorException( u"Failed to write registry config to {0} - {1}".format( config_path, exc))
def generate_orchestration_playbook(self, url=None, namespace=None, local_images=True, **kwargs): """ Generate an Ansible playbook to orchestrate services. :param url: registry URL where images will be pulled from :param namespace: registry namespace :param local_images: bypass pulling images, and use local copies :return: playbook dict """ states = ['start', 'restart', 'stop', 'destroy'] service_def = {} for service_name, service in self.services.items(): service_definition = {} if service.get('roles'): image = self.get_latest_image_for_service(service_name) if image is None: raise exceptions.AnsibleContainerConductorException( u"No image found for service {}, make sure you've run `ansible-container " u"build`".format(service_name) ) service_definition[u'image'] = image.tags[0] else: try: image = self.client.images.get(service['from']) except docker.errors.ImageNotFound: image = None logger.warning(u"Image {} for service {} not found. " u"An attempt will be made to pull it.".format(service['from'], service_name)) if image: service_definition[u'image'] = image.tags[0] else: service_definition[u'image'] = service['from'] for extra in self.COMPOSE_WHITELIST: if extra in service: service_definition[extra] = service[extra] logger.debug(u'Adding new service to definition', service=service_name, definition=service_definition) service_def[service_name] = service_definition tasks = [] for desired_state in states: task_params = { u'project_name': self.project_name, u'definition': { u'version': u'2', u'services': service_def, } } if self.volumes: task_params[u'definition'][u'volumes'] = dict(self.volumes) if desired_state in {'restart', 'start', 'stop'}: task_params[u'state'] = u'present' if desired_state == 'restart': task_params[u'restarted'] = True if desired_state == 'stop': task_params[u'stopped'] = True elif desired_state == 'destroy': task_params[u'state'] = u'absent' task_params[u'remove_volumes'] = u'yes' tasks.append({u'docker_service': task_params, u'tags': [desired_state]}) playbook = [{ u'hosts': u'localhost', u'gather_facts': False, u'tasks': tasks, }] for service in list(self.services.keys()) + ['conductor']: image_name = self.image_name_for_service(service) for image in self.client.images.list(name=image_name): logger.debug('Found image for service', tags=image.tags, id=image.short_id) for tag in image.tags: logger.debug('Adding task to destroy image', tag=tag) playbook[0][u'tasks'].append({ u'docker_image': { u'name': tag, u'state': u'absent', u'force': u'yes' }, u'tags': u'destroy' }) logger.debug(u'Created playbook to run project', playbook=playbook) return playbook
def run_conductor(self, command, config, base_path, params, engine_name=None, volumes=None): image_id = self.get_latest_image_id_for_service('conductor') if image_id is None: raise exceptions.AnsibleContainerConductorException( u"Conductor container can't be found. Run " u"`ansible-container build` first") serialized_params = base64.b64encode(json.dumps(params).encode("utf-8")).decode() serialized_config = base64.b64encode(json.dumps(ordereddict_to_list(config)).encode("utf-8")).decode() if not volumes: volumes = {} if params.get('with_volumes'): for volume in params.get('with_volumes'): volume_parts = volume.split(':') volume_parts[0] = os.path.normpath(os.path.abspath(os.path.expanduser(volume_parts[0]))) volumes[volume_parts[0]] = { 'bind': volume_parts[1] if len(volume_parts) > 1 else volume_parts[0], 'mode': volume_parts[2] if len(volume_parts) > 2 else 'rw' } permissions = 'ro' if command != 'install' else 'rw' volumes[base_path] = {'bind': '/src', 'mode': permissions} if params.get('deployment_output_path'): deployment_path = params['deployment_output_path'] volumes[deployment_path] = {'bind': deployment_path, 'mode': 'rw'} roles_path = None if params.get('roles_path'): # User specified --roles-path roles_path = os.path.normpath(os.path.abspath(os.path.expanduser(params.get('roles_path')))) volumes[roles_path] = {'bind': roles_path, 'mode': 'ro'} environ = {} if os.environ.get('DOCKER_HOST'): environ['DOCKER_HOST'] = os.environ['DOCKER_HOST'] if os.environ.get('DOCKER_CERT_PATH'): environ['DOCKER_CERT_PATH'] = '/etc/docker' volumes[os.environ['DOCKER_CERT_PATH']] = {'bind': '/etc/docker', 'mode': 'ro'} if os.environ.get('DOCKER_TLS_VERIFY'): environ['DOCKER_TLS_VERIFY'] = os.environ['DOCKER_TLS_VERIFY'] else: environ['DOCKER_HOST'] = 'unix:///var/run/docker.sock' volumes['/var/run/docker.sock'] = {'bind': '/var/run/docker.sock', 'mode': 'rw'} if params.get('with_variables'): for var in params['with_variables']: key, value = var.split('=', 1) environ[key] = value if roles_path: environ['ANSIBLE_ROLES_PATH'] = "%s:/src/roles:/etc/ansible/roles" % roles_path else: environ['ANSIBLE_ROLES_PATH'] = '/src/roles:/etc/ansible/roles' if params.get('devel'): conductor_path = os.path.dirname(container.__file__) logger.debug(u"Binding Ansible Container code at %s into conductor " u"container", conductor_path) volumes[conductor_path] = {'bind': '/_ansible/container', 'mode': 'rw'} if command in ('login', 'push') and params.get('config_path'): config_path = params.get('config_path') volumes[config_path] = {'bind': config_path, 'mode': 'rw'} if not engine_name: engine_name = __name__.rsplit('.', 2)[-2] run_kwargs = dict( name=self.container_name_for_service('conductor'), command=['conductor', command, '--project-name', self.project_name, '--engine', engine_name, '--params', serialized_params, '--config', serialized_config, '--encoding', 'b64json'], detach=True, user='******', volumes=volumes, environment=environ, working_dir='/src', cap_add=['SYS_ADMIN'] ) # Anytime a playbook is executed, /src is bind mounted to a tmpdir, and that seems to # require privileged=True run_kwargs['privileged'] = True logger.debug('Docker run:', image=image_id, params=run_kwargs) try: container_obj = self.client.containers.run( image_id, **run_kwargs ) except docker_errors.APIError as exc: if exc.response.status_code == StatusCodes.CONFLICT: raise exceptions.AnsibleContainerConductorException( u"Can't start conductor container, another conductor for " u"this project already exists or wasn't cleaned up.") six.reraise(*sys.exc_info()) else: log_iter = container_obj.logs(stdout=True, stderr=True, stream=True) mux = logmux.LogMultiplexer() mux.add_iterator(log_iter, plainLogger) return container_obj.id
def run_conductor(self, command, config, base_path, params, engine_name=None, volumes=None): image_id = self.get_latest_image_id_for_service('conductor') if image_id is None: raise exceptions.AnsibleContainerConductorException( u"Conductor container can't be found. Run " u"`ansible-container build` first") config['settings'] = dict(pwd=base_path, **config.get('settings', {})) serialized_params = base64.b64encode( json.dumps(params).encode("utf-8")).decode() serialized_config = base64.b64encode( json.dumps(config).encode("utf-8")).decode() if not volumes: volumes = {} permissions = 'ro' if command != 'install' else 'rw' volumes[base_path] = {'bind': '/src', 'mode': permissions} if params.get('deployment_output_path'): deployment_path = params['deployment_output_path'] volumes[deployment_path] = {'bind': deployment_path, 'mode': 'rw'} environ = {} if os.environ.get('DOCKER_HOST'): environ['DOCKER_HOST'] = os.environ['DOCKER_HOST'] if os.environ.get('DOCKER_CERT_PATH'): environ['DOCKER_CERT_PATH'] = '/etc/docker' volumes[os.environ['DOCKER_CERT_PATH']] = { 'bind': '/etc/docker', 'mode': 'ro' } if os.environ.get('DOCKER_TLS_VERIFY'): environ['DOCKER_TLS_VERIFY'] = os.environ['DOCKER_TLS_VERIFY'] else: environ['DOCKER_HOST'] = 'unix:///var/run/docker.sock' volumes['/var/run/docker.sock'] = { 'bind': '/var/run/docker.sock', 'mode': 'rw' } environ['ANSIBLE_ROLES_PATH'] = '/src/roles:/etc/ansible/roles' if params.get('devel'): conductor_path = os.path.dirname(container.__file__) logger.debug( u"Binding Ansible Container code at %s into conductor " u"container", conductor_path) volumes[conductor_path] = { 'bind': '/_ansible/container', 'mode': 'rw' } if command in ('login', 'push') and params.get('config_path'): config_path = params.get('config_path') volumes[config_path] = {'bind': config_path, 'mode': 'rw'} if not engine_name: engine_name = __name__.rsplit('.', 2)[-2] run_kwargs = dict(name=self.container_name_for_service('conductor'), command=[ 'conductor', command, '--project-name', self.project_name, '--engine', engine_name, '--params', serialized_params, '--config', serialized_config, '--encoding', 'b64json' ], detach=True, user='******', volumes=volumes, environment=environ, working_dir='/src', cap_add=['SYS_ADMIN']) logger.debug('Docker run:', image=image_id, params=run_kwargs) try: container_obj = self.client.containers.run(image_id, **run_kwargs) except docker_errors.APIError as exc: if exc.response.status_code == StatusCodes.CONFLICT: raise exceptions.AnsibleContainerConductorException( u"Can't start conductor container, another conductor for " u"this project already exists or wasn't cleaned up.") six.reraise(*sys.exc_info()) else: log_iter = container_obj.logs(stdout=True, stderr=True, stream=True) mux = logmux.LogMultiplexer() mux.add_iterator(log_iter, plainLogger) return container_obj.id