Add MultiStrOps support to config_template
This change adds the MultiStrOps variaable type to the config template parsing as well as supports multi string options when passing configuration overrides. This is made prossible by using the set type for options found within config overrides and creating a custom dictionary class that allows for multiple keys to be stored as a set. When the config_template encounters a set type it will process and reder value as a MultiStrOps. Set types are defined in yaml via the "?" entry. Example Overrides: things: - 1 - 2 multistrops_things: ? a ? b Example Rendered Config: things = 1,2 multistrops_things = a multistrops_things = b Change-Id: I2193ea2eb7f839c3151c2c96f9dfe86f141e5a15 Closes-Bug: #1542513 Signed-off-by: Kevin Carter <kevin.carter@rackspace.com>
This commit is contained in:
parent
fc411eaf89
commit
b42faff784
@ -12,11 +12,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import ConfigParser
|
||||
try:
|
||||
import ConfigParser
|
||||
except ImportError:
|
||||
import configparser as ConfigParser
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
|
||||
from ansible import errors
|
||||
@ -32,13 +35,210 @@ CONFIG_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
class MultiKeyDict(dict):
|
||||
"""Dictionary class which supports duplicate keys.
|
||||
|
||||
This class allows for an item to be added into a standard python dictionary
|
||||
however if a key is created more than once the dictionary will convert the
|
||||
singular value to a python set. This set type forces all values to be a
|
||||
string.
|
||||
|
||||
Example Usage:
|
||||
>>> z = MultiKeyDict()
|
||||
>>> z['a'] = 1
|
||||
>>> z['b'] = ['a', 'b', 'c']
|
||||
>>> z['c'] = {'a': 1}
|
||||
>>> print(z)
|
||||
... {'a': 1, 'b': ['a', 'b', 'c'], 'c': {'a': 1}}
|
||||
>>> z['a'] = 2
|
||||
>>> print(z)
|
||||
... {'a': set(['1', '2']), 'c': {'a': 1}, 'b': ['a', 'b', 'c']}
|
||||
"""
|
||||
def __setitem__(self, key, value):
|
||||
if key in self:
|
||||
if isinstance(self[key], set):
|
||||
items = self[key]
|
||||
items.add(str(value))
|
||||
super(MultiKeyDict, self).__setitem__(key, items)
|
||||
else:
|
||||
items = [str(value), str(self[key])]
|
||||
super(MultiKeyDict, self).__setitem__(key, set(items))
|
||||
else:
|
||||
return dict.__setitem__(self, key, value)
|
||||
|
||||
|
||||
class ConfigTemplateParser(ConfigParser.RawConfigParser):
|
||||
"""ConfigParser which supports multi key value.
|
||||
|
||||
The parser will use keys with multiple variables in a set as a multiple
|
||||
key value within a configuration file.
|
||||
|
||||
Default Configuration file:
|
||||
[DEFAULT]
|
||||
things =
|
||||
url1
|
||||
url2
|
||||
url3
|
||||
|
||||
other = 1,2,3
|
||||
|
||||
[section1]
|
||||
key = var1
|
||||
key = var2
|
||||
key = var3
|
||||
|
||||
Example Usage:
|
||||
>>> cp = ConfigTemplateParser(dict_type=MultiKeyDict)
|
||||
>>> cp.read('/tmp/test.ini')
|
||||
... ['/tmp/test.ini']
|
||||
>>> cp.get('DEFAULT', 'things')
|
||||
... \nurl1\nurl2\nurl3
|
||||
>>> cp.get('DEFAULT', 'other')
|
||||
... '1,2,3'
|
||||
>>> cp.set('DEFAULT', 'key1', 'var1')
|
||||
>>> cp.get('DEFAULT', 'key1')
|
||||
... 'var1'
|
||||
>>> cp.get('section1', 'key')
|
||||
... {'var1', 'var2', 'var3'}
|
||||
>>> cp.set('section1', 'key', 'var4')
|
||||
>>> cp.get('section1', 'key')
|
||||
... {'var1', 'var2', 'var3', 'var4'}
|
||||
>>> with open('/tmp/test2.ini', 'w') as f:
|
||||
... cp.write(f)
|
||||
|
||||
Output file:
|
||||
[DEFAULT]
|
||||
things =
|
||||
url1
|
||||
url2
|
||||
url3
|
||||
key1 = var1
|
||||
other = 1,2,3
|
||||
|
||||
[section1]
|
||||
key = var4
|
||||
key = var1
|
||||
key = var3
|
||||
key = var2
|
||||
"""
|
||||
def _write(self, fp, section, item, entry):
|
||||
if section:
|
||||
if (item is not None) or (self._optcre == self.OPTCRE):
|
||||
fp.write(entry)
|
||||
else:
|
||||
fp.write(entry)
|
||||
|
||||
def _write_check(self, fp, key, value, section=False):
|
||||
if isinstance(value, set):
|
||||
for item in value:
|
||||
item = str(item).replace('\n', '\n\t')
|
||||
entry = "%s = %s\n" % (key, item)
|
||||
self._write(fp, section, item, entry)
|
||||
else:
|
||||
if isinstance(value, list):
|
||||
_value = [str(i.replace('\n', '\n\t')) for i in value]
|
||||
entry = '%s = %s\n' % (key, ','.join(_value))
|
||||
else:
|
||||
entry = '%s = %s\n' % (key, str(value).replace('\n', '\n\t'))
|
||||
self._write(fp, section, value, entry)
|
||||
|
||||
def write(self, fp):
|
||||
if self._defaults:
|
||||
fp.write("[%s]\n" % 'DEFAULT')
|
||||
for key, value in self._defaults.items():
|
||||
self._write_check(fp, key=key, value=value)
|
||||
else:
|
||||
fp.write("\n")
|
||||
|
||||
for section in self._sections:
|
||||
fp.write("[%s]\n" % section)
|
||||
for key, value in self._sections[section].items():
|
||||
self._write_check(fp, key=key, value=value, section=True)
|
||||
else:
|
||||
fp.write("\n")
|
||||
|
||||
def _read(self, fp, fpname):
|
||||
cursect = None
|
||||
optname = None
|
||||
lineno = 0
|
||||
e = None
|
||||
while True:
|
||||
line = fp.readline()
|
||||
if not line:
|
||||
break
|
||||
lineno += 1
|
||||
if line.strip() == '' or line[0] in '#;':
|
||||
continue
|
||||
if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
|
||||
continue
|
||||
if line[0].isspace() and cursect is not None and optname:
|
||||
value = line.strip()
|
||||
if value:
|
||||
if isinstance(cursect[optname], set):
|
||||
_temp_item = list(cursect[optname])
|
||||
del cursect[optname]
|
||||
cursect[optname] = _temp_item
|
||||
elif isinstance(cursect[optname], (str, unicode)):
|
||||
_temp_item = [cursect[optname]]
|
||||
del cursect[optname]
|
||||
cursect[optname] = _temp_item
|
||||
cursect[optname].append(value)
|
||||
else:
|
||||
mo = self.SECTCRE.match(line)
|
||||
if mo:
|
||||
sectname = mo.group('header')
|
||||
if sectname in self._sections:
|
||||
cursect = self._sections[sectname]
|
||||
elif sectname == 'DEFAULT':
|
||||
cursect = self._defaults
|
||||
else:
|
||||
cursect = self._dict()
|
||||
self._sections[sectname] = cursect
|
||||
optname = None
|
||||
elif cursect is None:
|
||||
raise ConfigParser.MissingSectionHeaderError(
|
||||
fpname,
|
||||
lineno,
|
||||
line
|
||||
)
|
||||
else:
|
||||
mo = self._optcre.match(line)
|
||||
if mo:
|
||||
optname, vi, optval = mo.group('option', 'vi', 'value')
|
||||
optname = self.optionxform(optname.rstrip())
|
||||
if optval is not None:
|
||||
if vi in ('=', ':') and ';' in optval:
|
||||
pos = optval.find(';')
|
||||
if pos != -1 and optval[pos - 1].isspace():
|
||||
optval = optval[:pos]
|
||||
optval = optval.strip()
|
||||
if optval == '""':
|
||||
optval = ''
|
||||
cursect[optname] = optval
|
||||
else:
|
||||
if not e:
|
||||
e = ConfigParser.ParsingError(fpname)
|
||||
e.append(lineno, repr(line))
|
||||
if e:
|
||||
raise e
|
||||
all_sections = [self._defaults]
|
||||
all_sections.extend(self._sections.values())
|
||||
for options in all_sections:
|
||||
for name, val in options.items():
|
||||
if isinstance(val, list):
|
||||
_temp_item = '\n'.join(val)
|
||||
del options[name]
|
||||
options[name] = _temp_item
|
||||
|
||||
|
||||
class ActionModule(object):
|
||||
TRANSFERS_FILES = True
|
||||
|
||||
def __init__(self, runner):
|
||||
self.runner = runner
|
||||
|
||||
def grab_options(self, complex_args, module_args):
|
||||
@staticmethod
|
||||
def grab_options(complex_args, module_args):
|
||||
"""Grab passed options from Ansible complex and module args.
|
||||
|
||||
:param complex_args: ``dict``
|
||||
@ -53,14 +253,31 @@ class ActionModule(object):
|
||||
return options
|
||||
|
||||
@staticmethod
|
||||
def return_config_overrides_ini(config_overrides, resultant):
|
||||
def _option_write(config, section, key, value):
|
||||
config.remove_option(str(section), str(key))
|
||||
try:
|
||||
if not any(i for i in value.values()):
|
||||
value = set(value)
|
||||
except AttributeError:
|
||||
pass
|
||||
if isinstance(value, set):
|
||||
config.set(str(section), str(key), value)
|
||||
elif isinstance(value, list):
|
||||
config.set(str(section), str(key), ','.join(value))
|
||||
else:
|
||||
config.set(str(section), str(key), str(value))
|
||||
|
||||
def return_config_overrides_ini(self, config_overrides, resultant):
|
||||
"""Returns string value from a modified config file.
|
||||
|
||||
:param config_overrides: ``dict``
|
||||
:param resultant: ``str`` || ``unicode``
|
||||
:returns: ``str``
|
||||
"""
|
||||
config = ConfigParser.RawConfigParser(allow_no_value=True)
|
||||
config = ConfigTemplateParser(
|
||||
dict_type=MultiKeyDict,
|
||||
allow_no_value=True
|
||||
)
|
||||
config_object = io.BytesIO(resultant.encode('utf-8'))
|
||||
config.readfp(config_object)
|
||||
for section, items in config_overrides.items():
|
||||
@ -69,7 +286,7 @@ class ActionModule(object):
|
||||
if not isinstance(items, dict):
|
||||
if isinstance(items, list):
|
||||
items = ','.join(items)
|
||||
config.set('DEFAULT', str(section), str(items))
|
||||
self._option_write(config, 'DEFAULT', section, items)
|
||||
else:
|
||||
# Attempt to add a section to the config file passing if
|
||||
# an error is raised that is related to the section
|
||||
@ -79,9 +296,7 @@ class ActionModule(object):
|
||||
except (ConfigParser.DuplicateSectionError, ValueError):
|
||||
pass
|
||||
for key, value in items.items():
|
||||
if isinstance(value, list):
|
||||
value = ','.join(value)
|
||||
config.set(str(section), str(key), str(value))
|
||||
self._option_write(config, section, key, value)
|
||||
else:
|
||||
config_object.close()
|
||||
|
||||
@ -241,4 +456,3 @@ class ActionModule(object):
|
||||
inject=inject,
|
||||
complex_args=complex_args
|
||||
)
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
The ability to support MultiStrOps has been added to
|
||||
the config_template action plugin. This change updates
|
||||
the parser to use the ``set()`` type to determine if
|
||||
values within a given key are to be rendered as
|
||||
``MultiStrOps``. If an override is used in an INI
|
||||
config file the set type is defined using the standard
|
||||
yaml construct of "?" as the item marker.
|
||||
|
||||
::
|
||||
|
||||
# Example Override Entries
|
||||
Section:
|
||||
typical_list_things:
|
||||
- 1
|
||||
- 2
|
||||
multistrops_things:
|
||||
? a
|
||||
? b
|
||||
|
||||
::
|
||||
|
||||
# Example Rendered Config:
|
||||
[Section]
|
||||
typical_list_things = 1,2
|
||||
multistrops_things = a
|
||||
multistrops_things = b
|
||||
|
||||
fixes:
|
||||
- Resolves issue https://bugs.launchpad.net/openstack-ansible/+bug/1542513
|
@ -4,3 +4,4 @@ ansible>=1.9.1,<2.0.0
|
||||
# this is required for the docs build jobs
|
||||
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
|
||||
oslosphinx>=2.5.0 # Apache-2.0
|
||||
reno>=0.1.1 # Apache-2.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user