Merge "Add MultiStrOps support to config_template"
This commit is contained in:
commit
1e999b9256
|
@ -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…
Reference in New Issue