Skip to content

Commit

Permalink
Import merge_configs and merge_yaml from Kolla Ansible
Browse files Browse the repository at this point in the history
These action plugins will be useful for generating configuration files
on the Ansible control host.

Change-Id: I172c8e81936c93c8d6ce4e53c83083a44aa52e6b
  • Loading branch information
markgoddard authored and priteau committed Sep 29, 2021
1 parent f09faa4 commit a04b5d6
Show file tree
Hide file tree
Showing 7 changed files with 820 additions and 0 deletions.
19 changes: 19 additions & 0 deletions ansible/action_plugins/merge_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) 2021 StackHPC Ltd.
#
# 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 kayobe.plugins.action.merge_configs

ActionModule = kayobe.plugins.action.merge_configs.ActionModule
19 changes: 19 additions & 0 deletions ansible/action_plugins/merge_yaml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) 2021 StackHPC Ltd.
#
# 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 kayobe.plugins.action.merge_yaml

ActionModule = kayobe.plugins.action.merge_yaml.ActionModule
225 changes: 225 additions & 0 deletions kayobe/plugins/action/merge_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
#!/usr/bin/env 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.

# This file has been adapted from the merge_configs action plugin in Kolla
# Ansible.
# https://opendev.org/openstack/kolla-ansible/src/branch/master/ansible/action_plugins/merge_configs.py

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)

if not isinstance(sources, list):
sources = [sources]

config = OverrideConfigParser(
whitespace=self._task.args.get('whitespace', True))

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)
result.update(copy_action.run(task_vars=task_vars))
finally:
shutil.rmtree(local_tempdir)
return result
Loading

0 comments on commit a04b5d6

Please sign in to comment.