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:
Kevin Carter 2016-02-09 23:54:16 -06:00
parent fc411eaf89
commit b42faff784
No known key found for this signature in database
GPG Key ID: 69FEFFC5E2D9273F
3 changed files with 257 additions and 10 deletions

View File

@ -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
)

View File

@ -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

View File

@ -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