tripleo-ansible/tripleo_ansible/ansible_plugins/modules/podman_image.py
Kevin Carter d7163861f2 Update project structure
This change updates the project structure to reflect the documented
ansible best practice when creating projects [0].

* setup.cfg has been updated to ensure this package installs all of the
  libs and playbooks into the correct on system locations.

With this change we should be able to begin collecting content throughout
the tripleo ecosystem.

[0] - https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#directory-layout

Change-Id: If3ca66befee3f82f462a8fc00984698e26cc7e9b
Signed-off-by: Kevin Carter <kecarter@redhat.com>
2019-06-05 14:27:22 +00:00

751 lines
24 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2018 OpenStack Foundation
# 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.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import json
import re
from ansible.module_utils.basic import AnsibleModule
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = """
---
module: Podman Image
author:
- Sam Doran (@samdoran)
version_added: '2.8'
short_description: Pull images for use by podman
notes: []
description:
- Build, pull, or push images using Podman.
options:
name:
description:
- Name of the image to pull, push, or delete. It may contain a tag using
the format C(image:tag).
required: True
tag:
description:
- Tag of the image to pull, push, or delete.
default: "latest"
pull:
description: Whether or not to pull the image.
default: True
push:
description: Whether or not to push an image.
default: False
path:
description: Path to directory containing the build file.
force:
description:
- Whether or not to force push or pull an image. When building, force
the build even if the image already exists.
state:
description:
- Whether an image should be present, absent, or built.
default: "present"
choices:
- present
- absent
- build
tls_verify:
description:
- Require HTTPS and validate certificates when pulling or pushing. Also
used during build if a pull or push is necessary.
default: True
aliases:
- tlsverify
auth_file:
description:
- Path to file containing authorization credentials to the remote
registry
aliases:
- authfile
build_args:
description: Arguments that control image build.
suboptions:
annotation:
description:
- Dictionory of key=value pairs to add to the image. Only
works with OCI images. Ignored for Docker containers.
type: "str"
force_rm:
description:
- Always remove intermediate containers after a build, even if
the build is unsuccessful.
type: bool
default: False
format:
descritption:
- Format of the built image.
choices:
- docker
- ock
deafault: "oci"
cache:
description:
- Whether or not to use cached layers when building an image
type: bool
default: True
rm:
description: Remove intermediate containers after a successful build
type: bool
default: True
push_args:
description: Arguments that control pushing images.
suboptions:
compress:
description:
- Compress tarball image layers when pushing to a directory using the
'dir' transport.
type: bool
format:
description:
- Manifest type to use when pushing an image using the
'dir' transport (default is manifest type of source)
choices:
- oci
- v2s1
- v2s2
remove_signatures:
description: Discard any pre-existing signatures in the image
type: bool
sign_by:
description:
- Path to a key file to use to sign the image.
dest:
description: Path or URL where image will be pushed.
transport:
description:
- Transport to use when pushing in image. If no transport is set,
will attempt to push to a remote registry.
choices:
- dir
- docker-archive
- docker-daemon
- oci-archive
- ostree
"""
EXAMPLES = """
- name: Pull an image
podman_image:
name: quay.io/bitnami/wildfly
- name: Remove an image
podman_image:
name: quay.io/bitnami/wildfly
state: absent
- name: Pull a specific version of an image
podman_image:
name: redis
tag: 4
- name: Build a basic OCI image
podman_image:
name: nginx
path: /path/to/build/dir
- name: Build a basic OCI image with advanced parameters
podman_image:
name: nginx
path: /path/to/build/dir
build_args:
cache: no
force_rm: yes
format: oci
annotation:
app: nginx
function: proxy
info: Load balancer for my cool app
- name: Build a Docker image
podman_image:
name: nginx
path: /path/to/build/dir
build_args:
format: docker
- name: Build and push an image using existing credentials
podman_image:
name: nginx
path: /path/to/build/dir
push: yes
push_args:
dest: quay.io/acme
- name: Build and push an image using an auth file
podman_image:
name: nginx
push: yes
auth_file: /etc/containers/auth.json
push_args:
dest: quay.io/acme
- name: Build and push an image using username and password
podman_image:
name: nginx
push: yes
username: bugs
password: "{{ vault_registry_password }}"
push_args:
dest: quay.io/acme
- name: Build and push an image to mulitple registries
podman_image:
name: "{{ item }}"
path: /path/to/build/dir
push: yes
auth_file: /etc/containers/auth.json
loop:
- quay.io/acme/nginx
- docker.io/acme/nginx
- name: Build and push an image to mulitple registries with separate parameters
podman_image:
name: "{{ item.name }}"
tag: "{{ item.tag }}"
path: /path/to/build/dir
push: yes
auth_file: /etc/containers/auth.json
push_args:
dest: "{{ item.dest }}"
loop:
- name: nginx
tag: 4
dest: docker.io/acme
- name: nginx
tag: 3
dest: docker.io/acme
"""
RETURN = """
image:
description:
- Image inspection results for the image that was pulled, pushed, or built.
returned: success
type: dict
sample:
{
"actions": [
"Built image myimage:latest from /root/build",
"Pushed image myimage:latest to docker.io/acme"
],
"changed": true,
"image": [
{
"Annotations": {
"app": "nginx",
"function": "proxy",
"info": "Load balancer for my cool app"
},
"Architecture": "amd64",
"Author": "",
"Comment": "",
"ContainerConfig": {
"Cmd": [
"/bin/bash"
],
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/s\
bin:/bin"
],
"Labels": {
"org.label-schema.build-date": "20181006",
"org.label-schema.license": "GPLv2",
"org.label-schema.name": "CentOS Base Image",
"org.label-schema.schema-version": "1.0",
"org.label-schema.vendor": "CentOS"
}
},
"Created": "2018-10-26T16:33:44.983831874Z",
"Digest": "sha256:f1a93c1ca2fd628f5aec74e67aab7b1876831f3e472c0180\
1ef29d02d8285ee7",
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/containers/storage/overlay/632dbad27\
7b565b77bdc9164d722ee7a10b692a7ca972b58d9d02e730f576517/diff:/var/lib/containe\
rs/storage/overlay/b21a503efb4ab61e34db1a5d4131de092449809ff010792fe523ea49831\
55c20/diff:/var/lib/containers/storage/overlay/f972d139738dfcd1519fd2461815651\
36ee25a8b54c358834c50af094bb262f/diff",
"MergedDir": "/var/lib/containers/storage/overlay/f41d3afa\
8b49b64e48fc37944f12a27d35271d666bcbde6dc9c2cca1bb88cdba/merged",
"UpperDir": "/var/lib/containers/storage/overlay/f41d3afa8\
b49b64e48fc37944f12a27d35271d666bcbde6dc9c2cca1bb88cdba/diff",
"WorkDir": "/var/lib/containers/storage/overlay/f41d3afa8b\
49b64e48fc37944f12a27d35271d666bcbde6dc9c2cca1bb88cdba/work"
},
"Name": "overlay"
},
"Id": "ba719f232d43f2f31c9c804859dd14ca2fbc0df80fa2bf3c43ca2728dcf\
5e29e",
"Labels": {
"org.label-schema.build-date": "20181006",
"org.label-schema.license": "GPLv2",
"org.label-schema.name": "CentOS Base Image",
"org.label-schema.schema-version": "1.0",
"org.label-schema.vendor": "CentOS"
},
"ManifestType": "application/vnd.oci.image.manifest.v1+json",
"Os": "linux",
"Parent": "",
"RepoDigests": [
"localhost/myimage@sha256:f1a93c1ca2fd628f5aec74e67aab7b187683\
1f3e472c01801ef29d02d8285ee7"
],
"RepoTags": [
"localhost/myimage:latest"
],
"RootFS": {
"Layers": [
"sha256:f972d139738dfcd1519fd2461815651336ee25a8b54c358834\
c50af094bb262f",
"sha256:ffcdc41a923a341055073f18c9acc5ad5dd1214ac2a1763272\
d37a5f0dbab7ea",
"sha256:703ad93ff2aa20b9c28203935a337a5ee4969d46b9de212ac6\
f17bacfbe21379",
"sha256:2054721e6c61d4f4ef13726126737038cc72cd7ae6c83da0ea\
1cd73fb8595140"
],
"Type": "layers"
},
"Size": 208823219,
"User": "",
"Version": "",
"VirtualSize": 208823219
}
]
}
"""
class PodmanImageManager(object):
def __init__(self, module, results):
super(PodmanImageManager, self).__init__()
self.module = module
self.results = results
self.name = self.module.params.get('name')
self.executable = \
self.module.get_bin_path(module.params.get('executable'),
required=True)
self.tag = self.module.params.get('tag')
self.pull = self.module.params.get('pull')
self.push = self.module.params.get('push')
self.path = self.module.params.get('path')
self.force = self.module.params.get('force')
self.state = self.module.params.get('state')
self.tls_verify = self.module.params.get('tls_verify')
self.auth_file = self.module.params.get('auth_file')
self.username = self.module.params.get('username')
self.password = self.module.params.get('password')
self.cert_dir = self.module.params.get('cert_dir')
self.build_args = self.module.params.get('build_args')
self.push_args = self.module.params.get('push_args')
repo, repo_tag = parse_repository_tag(self.name)
if repo_tag:
self.name = repo
self.tag = repo_tag
self.image_name = '{name}:{tag}'.format(name=self.name, tag=self.tag)
if self.state in ['present', 'build']:
self.present()
if self.state in ['absent']:
self.absent()
def _run(self, args, expected_rc=0, ignore_errors=False):
if not isinstance(self.executable, list):
command = [self.executable]
command.extend(args)
rc, out, err = self.module.run_command(command)
if not ignore_errors and rc != expected_rc:
self.module.fail_json(msg='Failed to run {command} {err}'.format(
command=command, err=err))
return rc, out, err
def _get_id_from_output(self, lines, startswith=None, contains=None,
split_on=' ', maxsplit=1):
layer_ids = []
for line in lines.splitlines():
_condition1 = (startswith and line.startswith(startswith))
_condition2 = (contains and contains in line)
if _condition1 or _condition2:
splitline = line.rsplit(split_on, maxsplit)
layer_ids.append(splitline[1])
return(layer_ids[-1])
def present(self):
image = self.find_image()
if not image or self.force:
if self.path:
# Build the image
self.results['actions'].append(
'Built image {image_name} from {path}'.format(
image_name=self.image_name, path=self.path))
self.results['changed'] = True
if not self.module.check_mode:
self.results['image'] = self.build_image()
else:
# Pull the image
self.results['actions'].append(
'Pulled image {image_name}'.format(
image_name=self.image_name))
self.results['changed'] = True
if not self.module.check_mode:
self.results['image'] = self.pull_image()
if self.push:
# Push the image
if '/' in self.image_name:
push_format_string = 'Pushed image {image_name}'
else:
push_format_string = 'Pushed image {image_name} to {dest}'
self.results['actions'].append(
push_format_string.format(
image_name=self.image_name,
dest=self.push_args['dest']))
self.results['changed'] = True
if not self.module.check_mode:
self.results['image'] = self.push_image()
def absent(self):
image = self.find_image()
if image:
self.results['actions'].append(
'Removed image {name}'.format(name=self.name))
self.results['changed'] = True
self.results['image']['state'] = 'Deleted'
if not self.module.check_mode:
self.remove_image()
def find_image(self, image_name=None):
if image_name is None:
image_name = self.image_name
args = ['image', 'ls', image_name, '--format', 'json']
rc, images, err = self._run(args, ignore_errors=True)
if len(images) > 0:
return json.loads(images)
else:
return None
def inspect_image(self, image_name=None):
if image_name is None:
image_name = self.image_name
args = ['inspect', image_name, '--format', 'json']
rc, image_data, err = self._run(args)
if len(image_data) > 0:
return json.loads(image_data)
else:
return None
def pull_image(self, image_name=None):
if image_name is None:
image_name = self.image_name
args = ['pull', image_name, '-q']
if self.auth_file:
args.extend(['--authfile', self.auth_file])
if self.tls_verify:
args.append('--tls-verify')
if self.cert_dir:
args.extend(['--cert-dir', self.cert_dir])
rc, out, err = self._run(args, ignore_errors=True)
if rc != 0:
self.module.fail_json(
msg='Failed to pull image {image_name}'.format(
image_name=image_name))
return self.inspect_image(out.strip())
def build_image(self):
args = ['build', '-q']
args.extend(['-t', self.image_name])
if self.tls_verify:
args.append('--tls-verify')
annotation = self.build_args.get('annotation')
if annotation:
for k, v in annotation.items():
args.extend(['--annotation', '{k}={v}'.format(k=k, v=v)])
if self.cert_dir:
args.extend(['--cert-dir', self.cert_dir])
if self.build_args.get('force_rm'):
args.append('--force-rm')
image_format = self.build_args.get('format')
if image_format:
args.extend(['--format', image_format])
if not self.build_args.get('cache'):
args.append('--no-cache')
if self.build_args.get('rm'):
args.append('--rm')
volume = self.build_args.get('volume')
if volume:
for v in volume:
args.extend(['--volume', v])
if self.auth_file:
args.extend(['--authfile', self.auth_file])
if self.username and self.password:
cred_string = '{user}:{password}'.format(user=self.username,
password=self.password)
args.extend(['--creds', cred_string])
args.append(self.path)
rc, out, err = self._run(args, ignore_errors=True)
if rc != 0:
self.module.fail_json(
msg="Failed to build image {image}: {out} {err}".format(
image=self.image_name,
out=out,
err=err))
last_id = self._get_id_from_output(out, startswith='-->')
return self.inspect_image(last_id)
def push_image(self):
args = ['push']
if self.tls_verify:
args.append('--tls-verify')
if self.cert_dir:
args.extend(['--cert-dir', self.cert_dir])
if self.username and self.password:
cred_string = '{user}:{password}'.format(user=self.username,
password=self.password)
args.extend(['--creds', cred_string])
if self.auth_file:
args.extend(['--authfile', self.auth_file])
if self.push_args.get('compress'):
args.append('--compress')
push_format = self.push_args.get('format')
if push_format:
args.extend(['--format', push_format])
if self.push_args.get('remove_signatures'):
args.append('--remove_signatures')
sign_by_key = self.push_args.get('sign_by')
if sign_by_key:
args.extend(['--sign-by', sign_by_key])
args.append(self.image_name)
# Build the destination argument
dest = self.push_args.get('dest')
dest_format_string = '{dest}/{image_name}'
regexp = re.compile(r'/{name}(:{tag})?'.format(
name=self.name,
tag=self.tag))
if not dest:
if '/' not in self.name:
self.module.fail_json(
msg="'push_args['dest']' is required when pushing images "
"that do not have the remote registry in the "
"image name")
# If the push destinaton contains the image name and/or the tag
# remove it and warn since it's not needed.
elif regexp.search(dest):
dest = regexp.sub('', dest)
self.module.warn(
"Image name and tag are automatically added to "
"push_args['dest']. Destination changed to {dest}".format(
dest=dest))
if dest and dest.endswith('/'):
dest = dest[:-1]
transport = self.push_args.get('transport')
if transport:
if not dest:
self.module.fail_json(
"'push_args['transport'] requires 'push_args['dest'] but "
"it was not provided.")
if transport == 'docker':
dest_format_string = '{transport}://{dest}'
elif transport == 'ostree':
dest_format_string = '{transport}:{name}@{dest}'
else:
dest_format_string = '{transport}:{dest}'
dest_string = dest_format_string.format(transport=transport,
name=self.name,
dest=dest,
image_name=self.image_name,)
# Only append the destination argument if the image name is not a URL
if '/' not in self.name:
args.append(dest_string)
rc, out, err = self._run(args, ignore_errors=True)
if rc != 0:
self.module.fail_json(
msg="Failed to push image {image_name}: {err}".format(
image_name=self.image_name,
err=err,
)
)
last_id = self._get_id_from_output(
out + err, contains=':', split_on=':')
return self.inspect_image(last_id)
def remove_image(self, image_name=None):
if image_name is None:
image_name = self.image_name
args = ['rmi', image_name]
if self.force:
args.append('--force')
rc, out, err = self._run(args, ignore_errors=True)
if rc != 0:
self.module.fail_json(
msg='Failed to remove image {image_name}. {err}'.format(
image_name=image_name, err=err))
return out
def parse_repository_tag(repo_name):
parts = repo_name.rsplit('@', 1)
if len(parts) == 2:
return tuple(parts)
parts = repo_name.rsplit(':', 1)
if len(parts) == 2 and '/' not in parts[1]:
return tuple(parts)
return repo_name, None
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
tag=dict(type='str', default='latest'),
pull=dict(type='bool', default=True),
push=dict(type='bool', default=False),
path=dict(type='str'),
force=dict(type='bool', default=False),
state=dict(
type='str',
default='present',
choices=['absent', 'present', 'build']
),
tls_verify=dict(type='bool', default=True, aliases=['tlsverify']),
executable=dict(type='str', default='podman'),
auth_file=dict(type='path', aliases=['authfile']),
username=dict(type='str'),
password=dict(type='str', no_log=True),
cert_dir=dict(type='path'),
build_args=dict(
type='dict',
aliases=['buildargs'],
default={},
options=dict(
annotation=dict(type='dict'),
force_rm=dict(type='bool'),
format=dict(
type='str',
choices=['oci', 'docker'],
default='oci'
),
cache=dict(type='bool', default=True),
rm=dict(type='bool', default=True),
volume=dict(type='list'),
),
),
push_args=dict(
type='dict',
default={},
options=dict(
compress=dict(type='bool'),
format=dict(type='str', choices=['oci', 'v2s1', 'v2s2']),
remove_signatures=dict(type='bool'),
sign_by=dict(type='str'),
dest=dict(type='str', aliases=['destination'],),
transport=dict(
type='str',
choices=[
'dir',
'docker-archive',
'docker-daemon',
'oci-archive',
'ostree',
]
),
),
),
),
supports_check_mode=True,
required_together=(
['username', 'password'],
),
mutually_exclusive=(
['authfile', 'username'],
['authfile', 'password'],
),
)
results = dict(
changed=False,
actions=[],
image={},
)
PodmanImageManager(module, results)
module.exit_json(**results)
if __name__ == '__main__':
main()