Merge "Add MultiStrOps support to config_template"

This commit is contained in:
Jenkins 2016-02-12 09:58:28 +00:00 committed by Gerrit Code Review
commit 1e999b9256
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