Merge "Action module for bulk host prep"

This commit is contained in:
Zuul 2021-08-17 01:25:18 +00:00 committed by Gerrit Code Review
commit 76f354c60a
2 changed files with 338 additions and 0 deletions

View File

@ -0,0 +1,14 @@
==========================
Module - tripleo_host_prep
==========================
This module provides for the following ansible plugin:
* tripleo_host_prep
.. ansibleautoplugin::
:module: tripleo_ansible/ansible_plugins/action/tripleo_host_prep.py
:documentation: true
:examples: true

View File

@ -0,0 +1,324 @@
#!/usr/bin/env python3
# Copyright 2021 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
import os
import tempfile
import yaml
from ansible.errors import AnsibleActionFail
from ansible.plugins.action import ActionBase
from ansible.utils.display import Display
DISPLAY = Display()
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = """
---
module: tripleo_host_prep
author:
- Alex Schultz <aschultz@redhat.com>
version_added: '2.9'
short_description: Apply host prep data to a host
notes: []
description:
- This module processes a complex hash provided to it that expresses
users, groups, files, directories and some selinux related options that
should applied to the host. This module leverages the existing ansible
modules to apply the data. users (ansible.builtin.user),
groups (ansible.builtin.group), files (ansible.builtin.copy),
directories (ansible.builtin.file), seboolean (ansible.posix.seboolean),
sefcontext (community.general.sefcontext). All options exposed by these
modules are available.
options:
host_prep_data:
description:
- Dictionary containing users, groups, files, directories, etc to apply.
required: True
type: dict
debug:
description:
- Whether or not debug is enabled.
default: False
required: False
type: bool
"""
EXAMPLES = """
- name: Apply host prep
tripleo_host_prep:
host_prep_data:
service_a:
users:
"foo":
uid: 1233
group: foobar
groups:
"foobar":
gid: 1233
files:
"/var/tmp/foo/bar":
content: |
data
mode: "0644"
directories:
"/var/tmp/foo":
mode: "0700"
seboolean:
"virt_sandbox_use_netlink":
persistent: yes
state: yes
sefcontext:
"/var/tmp/foo(/.*)?":
setype: container_file_t
service_b:
directories:
"/var/tmp/bar":
mode: "0750"
files:
"/var/tmp/bar/baz":
content: "fizz"
mode: "0600"
owner: root
"""
RETURN = """
"""
class ActionModule(ActionBase):
"""Tripleo host prep module
"""
TRANSFERS_FILES = True
_VALID_ARGS = yaml.safe_load(DOCUMENTATION)['options']
class PrepTaskFailure(Exception):
"""exception to stop processing"""
def _get_args(self):
missing = []
args = {}
for option, vals in self._VALID_ARGS.items():
if 'default' not in vals:
if self._task.args.get(option, None) is None:
missing.append(option)
continue
args[option] = self._task.args.get(option)
else:
args[option] = self._task.args.get(option, vals['default'])
if missing:
raise AnsibleActionFail('Missing required parameters: {}'.format(
', '.join(missing)))
return args
def _get_data_type(self, data_type):
data = {}
for svc in self.host_prep_data.items():
items = svc[1].get(data_type, {})
for item in items:
if item in data:
if data[item] != items[item]:
DISPLAY.warning(f'{item} defined multiple times with '
'different settings. The first '
'instance will be used.')
else:
DISPLAY.debug(f'{item} already handled, skipping')
continue
data[item] = items.get(item)
return data
def _handle_result(self, result):
if result.get('changed', False):
self.changed = True
if result.get('failed', False):
self.fail_result = result
raise self.PrepTaskFailure()
def apply_groups(self, task_vars):
"""Apply groups to a system"""
group_data = self._get_data_type('groups')
for group in group_data:
# create group
args = group_data[group] or {}
args.setdefault('name', group)
group_result = self._execute_module(
module_name='group',
module_args=args,
task_vars=task_vars
)
self._handle_result(group_result)
def apply_users(self, task_vars):
"""Apply users to a system"""
user_data = self._get_data_type('users')
for user in user_data:
# create user
args = user_data[user] or {}
args.setdefault('name', user)
user_result = self._execute_module(
module_name='user',
module_args=args,
task_vars=task_vars
)
self._handle_result(user_result)
def apply_dirs(self, task_vars):
"""Create directories on a system"""
dir_data = self._get_data_type('directories')
for dirname in dir_data:
# create dir
args = dir_data[dirname] or {}
args.setdefault('path', dirname)
args.setdefault('state', 'directory')
dir_result = self._execute_module(
module_name='file',
module_args=args,
task_vars=task_vars
)
self._handle_result(dir_result)
def apply_files(self, task_vars):
"""Copy file or file data to a remote system"""
file_data = self._get_data_type('files')
for filename in file_data:
# create file
args = file_data[filename] or {}
args.setdefault('dest', filename)
tempfile_path = None
if 'content' in args:
# copy content to the remote system
tempfile_path = self._transfer_data(
remote_path=self._connection._shell.join_path(
self.remote_tmp,
next(tempfile._get_candidate_names())),
data=args.pop('content')
)
elif not args.get('remote_src', False) and 'src' in args:
# copy the local src to the remote system
tempfile_path = self._transfer_file(
local_path=args['src'],
remote_path=self._connection._shell.join_path(
self.remote_tmp,
next(tempfile._get_candidate_names()))
)
if tempfile_path:
args['src'] = tempfile_path
# since we already handled the copy, tell copy module it
# is a remote src location
args['remote_src'] = True
try:
# the copy module always assumes remote host, the action
# plugin version does the copy action.
file_result = self._execute_module(
module_name='copy',
module_args=args,
task_vars=task_vars
)
self._handle_result(file_result)
finally:
# do temp file cleanup
if tempfile_path:
try:
# delete remote temp
self._execute_module(
module_name='file',
module_args={'path': tempfile_path,
'state': 'absent'},
task_vars=task_vars
)
finally:
# delete local if exists
if os.path.exists(tempfile_path):
os.remove(tempfile_path)
def apply_seboolean(self, task_vars):
"""Apply a list of sebooleans"""
sebool_data = self._get_data_type('seboolean')
for sebool in sebool_data:
# manage seboolean
args = sebool_data[sebool] or {}
args.setdefault('name', sebool)
sebool_result = self._execute_module(
module_name='ansible.posix.seboolean',
module_args=args,
task_vars=task_vars
)
self._handle_result(sebool_result)
def apply_sefcontext(self, task_vars):
"""Apply a list of sefcontexts"""
sefctx_data = self._get_data_type('sefcontext')
for sefctx in sefctx_data:
# manage sefctx
args = sefctx_data[sefctx] or {}
args.setdefault('target', sefctx)
sefctx_result = self._execute_module(
module_name='community.general.sefcontext',
module_args=args,
task_vars=task_vars
)
self._handle_result(sefctx_result)
def run(self, tmp=None, task_vars=None):
self._supports_check_mode = True
self.changed = False
if task_vars is None:
task_vars = dict()
result = super(ActionModule, self).run(tmp, task_vars)
del tmp
# parse args
args = self._get_args()
self.fail_result = None
self.host_prep_data = args['host_prep_data']
self.debug = args['debug']
try:
# create a remote temp for our usage with the files call
self.remote_tmp = self._make_tmp_path(
remote_user=self._play_context.remote_user
)
# Apply the data in a specific order
self.apply_groups(task_vars)
# users need groups
self.apply_users(task_vars)
# directories needs users/groups
self.apply_dirs(task_vars)
# files need directories/users/groups
self.apply_files(task_vars)
# selinux bits can be applied last
self.apply_seboolean(task_vars)
self.apply_sefcontext(task_vars)
# update result with changed flag
result['changed'] = self.changed
except self.PrepTaskFailure:
result = self.fail_result
finally:
if self.remote_tmp:
self._remove_tmp_path(self.remote_tmp)
return result