Merge "Create action plugin to support artifact push"

This commit is contained in:
Zuul 2021-03-24 21:52:17 +00:00 committed by Gerrit Code Review
commit 74c1226b6d
1 changed files with 298 additions and 0 deletions

View File

@ -0,0 +1,298 @@
#!/usr/bin/python
# Copyright 2020 Red Hat, Inc.
# 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.
__metaclass__ = type
from ansible.errors import AnsibleActionFail
from ansible.plugins.action import ActionBase
from ansible.utils.display import Display
import os
import requests
import subprocess
import tempfile
ARTIFACTS_ANCHOR = '/var/lib/tripleo/artifacts'
DISPLAY = Display()
ANSIBLE_METADATA = {
'metadata_version': '1.0',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: tripleo_push_artifacts
short_description: Push RPM/tar.gz artifact files from a local path
version_added: "2.9"
author: "Kevin Carter (@cloudnull)"
description:
- Takes a set of fully qualified paths as inputs, pushes the content
and deploys them on the remote system.
- When installing multiple RPMs all of them will be installed using
a single transaction with DNF. This improves performance, while
maintaining the package ordering.
options:
artifact_paths:
description:
- List of artifact full paths
required: true
type: list
artifact_urls:
description:
- List of artifact full paths
required: true
type: list
'''
RETURN = """
"""
EXAMPLES = """
- name: Push artifacts
tripleo_push_artifacts:
artifact_paths:
- /var/lib/tripleo/artifacts/container1/foo.rpm
- /var/lib/tripleo/artifacts/container2/foo.tar.gz
artifact_urls:
- https://example.tld/packages/package.rpm
"""
class ActionModule(ActionBase):
"""Batch process artifacts."""
def _run_module(self, module_name, module_args):
"""Execute an ansible module."""
DISPLAY.vv('Running module name: {}'.format(module_name))
results = self._execute_module(
module_name=module_name,
module_args=module_args,
task_vars=self.task_vars_meta
)
DISPLAY.vv('Result {}'.format(results))
if results.get('changed', False):
self.changed = True
if results.get('failed', False):
raise AnsibleActionFail(
'Module {} failed. Message: {}'.format(
module_name,
results.get('msg')
)
)
return results
def _get_filetype(self, filename):
"""Get file type information."""
try:
r = subprocess.run(
"file -b {}".format(filename),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
except Exception as e:
raise Exception('Unable to determine file type: %s' & e)
else:
if 'RPM' in r.stdout:
return 'rpm'
elif 'gzip compressed data' in r.stdout:
return 'targz'
raise AnsibleActionFail(
'Filename {} is an unknown type'.format(filename)
)
def _get_url(self, url):
"""Run file download operation."""
path_path = os.path.join(tempfile.gettempdir(), 'artifacts')
os.makedirs(path_path)
package_name = os.path.join(
path_path,
os.path.basename(url)
)
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(package_name, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return package_name
def _transfer_files(self, filename, destination):
"""Run file transfer operation."""
# Access to protected method is unavoidable in Ansible
# NOTE(cloudnull): Access to private method is unavoidable in Ansible
transferred_file = self._transfer_file(
local_path=filename,
remote_path=self._connection._shell.join_path(self.tmp, 'source')
)
self._run_module(
module_name='copy',
module_args=dict(
src=transferred_file,
dest=destination,
_original_basename=os.path.basename(filename),
follow=True,
)
)
def deploy_rpm(self, filename):
"""Sync RPM to remote host."""
DISPLAY.vv('Running package deployment')
package_path = os.path.join(
ARTIFACTS_ANCHOR,
os.path.basename(filename)
)
self._run_module(
module_name='file',
module_args=dict(
path=os.path.dirname(package_path),
state='directory'
)
)
self._transfer_files(filename=filename, destination=package_path)
return package_path
def install_rpms(self, rpms):
"""Run RPM installation."""
DISPLAY.vv('Running package install for: {}'.format(rpms))
self._run_module(
module_name='dnf',
module_args=dict(
name=rpms
)
)
for rpm in rpms:
self._run_module(
module_name='file',
module_args=dict(
path=rpm,
state='absent'
)
)
self.installed_artifacts.extend(
[os.path.basename(i) for i in rpms]
)
def deploy_targz(self, filename):
"""Run unarchive deployment."""
DISPLAY.vv('Running archive deployment')
package_path = os.path.join(
ARTIFACTS_ANCHOR,
os.path.basename(filename)
)
self._transfer_files(filename=filename, destination=package_path)
results = self._low_level_execute_command(
"tar xvz -C / -f {}".format(package_path),
executable='/bin/bash'
)
DISPLAY.vv('Result {}'.format(results))
if results['rc'] > 0:
DISPLAY.error(msg='Failed command: {}'.format(results))
raise AnsibleActionFail(
'Unable to perform unarchive {}.'.format(package_path)
)
self._run_module(
module_name='file',
module_args=dict(
path=package_path,
state='absent'
)
)
self.installed_artifacts.append(os.path.basename(filename))
def _run(self, task_vars=None):
"""Run the artifact push batcher.
All pushed artifacts will be deployed to the inventory target.
"""
self.changed = False
self.installed_artifacts = list()
if task_vars is None:
task_vars = dict()
result = super(ActionModule, self).run(task_vars=task_vars)
self.task_vars_meta = task_vars
# parse args
download_artifacts = self._task.args.get('artifact_urls', list())
local_artifacts = self._task.args.get('artifact_paths', list())
if not local_artifacts or not download_artifacts:
raise AnsibleActionFail(
'Neither artifact_paths or artifact_urls has any value.'
' Check configuration and try again.'
)
for artifact in download_artifacts:
local_artifacts.append(
self._get_url(url=artifact)
)
rpms = list()
for artifact in local_artifacts:
filetype = self._get_filetype(filename=artifact)
DISPLAY.vv(
'Artifact type: {}, file: {}'.format(
filetype,
artifact
)
)
if filetype == 'rpm':
pushed_artifact = self.deploy_rpm(filename=artifact)
rpms.append(pushed_artifact)
elif filetype == 'targz':
self.deploy_targz(filename=artifact)
if rpms:
self.install_rpms(rpms=rpms)
result['changed'] = self.changed
result['installed_artifacts'] = self.installed_artifacts
return result
def run(self, tmp=None, task_vars=None):
"""Begin action plugin execution."""
del tmp # tmp no longer has any effect
try:
self.tmp = self._make_tmp_path(
remote_user=self._play_context.remote_user
)
return self._run(task_vars=task_vars)
finally:
self._remove_tmp_path(self.tmp)