kolla-ansible/ansible/action_plugins/merge_configs.py
Maksim Malchuk 8972241dc9 Fix merge action plugins verbose output
This change fixes the output 'module_args' information of the plugins
'merge_configs' and 'merge_yaml' when Ansible is executed in maximum
verbose mode. Now all the plugin options are displayed instead of
standard 'copy' plugin options only.

Also, this change contains fixes already applied in the Kayobe
project to improve and synchronize the code of the plugins between
projects.

Change-Id: Ie2d9a0501fe29bfd854eb31258f282b197855948
Signed-off-by: Maksim Malchuk <maksim.malchuk@gmail.com>
2023-04-02 00:33:40 +03:00

224 lines
7.0 KiB
Python

# Copyright 2015 Sam Yaple
# Copyright 2017 99Cloud Inc.
#
# 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.
import collections
import os
import shutil
import tempfile
from ansible import constants
from ansible.plugins import action
from io import StringIO
from oslo_config import iniparser
_ORPHAN_SECTION = 'TEMPORARY_ORPHAN_VARIABLE_SECTION'
DOCUMENTATION = '''
---
module: merge_configs
short_description: Merge ini-style configs
description:
- ConfigParser is used to merge several ini-style configs into one
options:
dest:
description:
- The destination file name
required: True
type: str
sources:
description:
- A list of files on the destination node to merge together
default: None
required: True
type: str
whitespace:
description:
- Whether whitespace characters should be used around equal signs
default: True
required: False
type: bool
author: Sam Yaple
'''
EXAMPLES = '''
Merge multiple configs:
- hosts: database
tasks:
- name: Merge configs
merge_configs:
sources:
- "/tmp/config_1.cnf"
- "/tmp/config_2.cnf"
- "/tmp/config_3.cnf"
dest:
- "/etc/mysql/my.cnf"
'''
class OverrideConfigParser(iniparser.BaseParser):
def __init__(self, whitespace=True):
self._cur_sections = collections.OrderedDict()
self._sections = collections.OrderedDict()
self._cur_section = None
self._whitespace = ' ' if whitespace else ''
def assignment(self, key, value):
if self._cur_section is None:
self.new_section(_ORPHAN_SECTION)
cur_value = self._cur_section.get(key)
if len(value) == 1 and value[0] == '':
value = []
if not cur_value:
self._cur_section[key] = [value]
else:
self._cur_section[key].append(value)
def parse(self, lineiter):
self._cur_sections = collections.OrderedDict()
self._cur_section = None
super(OverrideConfigParser, self).parse(lineiter)
# merge _cur_sections into _sections
for section, values in self._cur_sections.items():
if section not in self._sections:
self._sections[section] = collections.OrderedDict()
for key, value in values.items():
self._sections[section][key] = value
def new_section(self, section):
cur_section = self._cur_sections.get(section)
if not cur_section:
cur_section = collections.OrderedDict()
self._cur_sections[section] = cur_section
self._cur_section = cur_section
return cur_section
def write(self, fp):
def write_key_value(key, values):
for v in values:
if not v:
fp.write('{key}{ws}=\n'.format(
key=key, ws=self._whitespace))
for index, value in enumerate(v):
if index == 0:
fp.write('{key}{ws}={ws}{value}\n'.format(
key=key,
ws=self._whitespace,
value=value))
else:
# We want additional values to be written out under the
# first value with the same indentation, like this:
# key = value1
# value2
indent_size = len(key) + len(self._whitespace) * 2 + 1
ws_indent = ' ' * indent_size
fp.write('{ws_indent}{value}\n'.format(
ws_indent=ws_indent,
value=value))
def write_section(section):
for key, values in section.items():
write_key_value(key, values)
for section in self._sections:
if section != _ORPHAN_SECTION:
fp.write('[{}]\n'.format(section))
write_section(self._sections[section])
fp.write('\n')
class ActionModule(action.ActionBase):
TRANSFERS_FILES = True
def read_config(self, source, config):
# Only use config if present
if os.access(source, os.R_OK):
with open(source, 'r') as f:
template_data = f.read()
# set search path to mimic 'template' module behavior
searchpath = [
self._loader._basedir,
os.path.join(self._loader._basedir, 'templates'),
os.path.dirname(source),
]
self._templar.environment.loader.searchpath = searchpath
result = self._templar.template(template_data)
fakefile = StringIO(result)
config.parse(fakefile)
fakefile.close()
def run(self, tmp=None, task_vars=None):
result = super(ActionModule, self).run(tmp, task_vars)
del tmp # not used
sources = self._task.args.get('sources', None)
whitespace = self._task.args.get('whitespace', True)
if not isinstance(sources, list):
sources = [sources]
config = OverrideConfigParser(whitespace=whitespace)
for source in sources:
self.read_config(source, config)
# Dump configparser to string via an emulated file
fakefile = StringIO()
config.write(fakefile)
full_source = fakefile.getvalue()
fakefile.close()
local_tempdir = tempfile.mkdtemp(dir=constants.DEFAULT_LOCAL_TMP)
try:
result_file = os.path.join(local_tempdir, 'source')
with open(result_file, 'w') as f:
f.write(full_source)
new_task = self._task.copy()
new_task.args.pop('sources', None)
new_task.args.pop('whitespace', None)
new_task.args.update(
dict(
src=result_file
)
)
copy_action = self._shared_loader_obj.action_loader.get(
'copy',
task=new_task,
connection=self._connection,
play_context=self._play_context,
loader=self._loader,
templar=self._templar,
shared_loader_obj=self._shared_loader_obj)
copy_result = copy_action.run(task_vars=task_vars)
copy_result['invocation']['module_args'].update({
'src': result_file, 'sources': sources,
'whitespace': whitespace})
result.update(copy_result)
finally:
shutil.rmtree(local_tempdir)
return result