Quote and escape extra vars passed to ansible

Arguments passed as extra variables to Ansible require double escaping.
This can be seen when running the following command:

kayobe physical network configure --interface-description-limit 'interface 42'

This gets passed to ansible-playbook as:

['-e', 'physical_network_interface_description_limit=interface 42']

Ansible for some reason loses the 42 from the description. This can be
worked around via quoting:

kayobe physical network configure --interface-description-limit '"interface 42"'

Which results in:

['-e', 'physical_network_interface_description_limit="interface 42"']

This change adds quoting and escaping of variables passed to
ansible-playbook in this way.

Note that this change does not modify any extra variables passed to
kayobe via the -e argument, instead keeping the behaviour of this
argument the same as that of the Ansible -e argument.

Change-Id: I51d5112b8f94998f948fe5b8cb9a927ee7ed8cec
Story: 2004379
Task: 27993
This commit is contained in:
Mark Goddard 2018-11-19 17:49:24 +00:00
parent 928efdca40
commit 196d28e766
7 changed files with 45 additions and 2 deletions

View File

@ -133,9 +133,13 @@ def build_args(parsed_args, playbooks,
cmd += ["-e", "@%s" % vars_file] cmd += ["-e", "@%s" % vars_file]
if parsed_args.extra_vars: if parsed_args.extra_vars:
for extra_var in parsed_args.extra_vars: for extra_var in parsed_args.extra_vars:
# Don't quote or escape variables passed via the kayobe -e CLI
# argument, to match Ansible's behaviour.
cmd += ["-e", extra_var] cmd += ["-e", extra_var]
if extra_vars: if extra_vars:
for extra_var_name, extra_var_value in extra_vars.items(): for extra_var_name, extra_var_value in extra_vars.items():
# Quote and escape variables originating within the python CLI.
extra_var_value = utils.quote_and_escape(extra_var_value)
cmd += ["-e", "%s=%s" % (extra_var_name, extra_var_value)] cmd += ["-e", "%s=%s" % (extra_var_name, extra_var_value)]
if parsed_args.become: if parsed_args.become:
cmd += ["--become"] cmd += ["--become"]

View File

@ -117,9 +117,13 @@ def build_args(parsed_args, command, inventory_filename, extra_vars=None,
os.path.join(parsed_args.kolla_config_path, "passwords.yml")] os.path.join(parsed_args.kolla_config_path, "passwords.yml")]
if parsed_args.kolla_extra_vars: if parsed_args.kolla_extra_vars:
for extra_var in parsed_args.kolla_extra_vars: for extra_var in parsed_args.kolla_extra_vars:
# Don't quote or escape variables passed via the kayobe -e CLI
# argument, to match Ansible's behaviour.
cmd += ["-e", extra_var] cmd += ["-e", extra_var]
if extra_vars: if extra_vars:
for extra_var_name, extra_var_value in extra_vars.items(): for extra_var_name, extra_var_value in extra_vars.items():
# Quote and escape variables originating within the python CLI.
extra_var_value = utils.quote_and_escape(extra_var_value)
cmd += ["-e", "%s=%s" % (extra_var_name, extra_var_value)] cmd += ["-e", "%s=%s" % (extra_var_name, extra_var_value)]
if parsed_args.kolla_limit or limit: if parsed_args.kolla_limit or limit:
limits = [l for l in [parsed_args.kolla_limit, limit] if l] limits = [l for l in [parsed_args.kolla_limit, limit] if l]

View File

@ -251,7 +251,7 @@ class TestCase(unittest.TestCase):
"-e", "@/etc/kayobe/vars-file1.yml", "-e", "@/etc/kayobe/vars-file1.yml",
"-e", "@/etc/kayobe/vars-file2.yaml", "-e", "@/etc/kayobe/vars-file2.yaml",
"-e", "ev_name1=ev_value1", "-e", "ev_name1=ev_value1",
"-e", "ev_name2=ev_value2", "-e", "ev_name2='ev_value2'",
"--check", "--check",
"--limit", "group1:host1:&group2:host2", "--limit", "group1:host1:&group2:host2",
"--tags", "tag1,tag2,tag3,tag4", "--tags", "tag1,tag2,tag3,tag4",

View File

@ -190,7 +190,7 @@ class TestCase(unittest.TestCase):
"-v", "-v",
"--inventory", "/etc/kolla/inventory/overcloud", "--inventory", "/etc/kolla/inventory/overcloud",
"-e", "ev_name1=ev_value1", "-e", "ev_name1=ev_value1",
"-e", "ev_name2=ev_value2", "-e", "ev_name2='ev_value2'",
"--tags", "tag1,tag2,tag3,tag4", "--tags", "tag1,tag2,tag3,tag4",
"--arg1", "--arg2", "--arg1", "--arg2",
] ]

View File

@ -96,3 +96,16 @@ key2: value2
mock_call.side_effect = subprocess.CalledProcessError(1, "command") mock_call.side_effect = subprocess.CalledProcessError(1, "command")
self.assertRaises(subprocess.CalledProcessError, utils.run_command, self.assertRaises(subprocess.CalledProcessError, utils.run_command,
["command", "to", "run"]) ["command", "to", "run"])
def test_quote_and_escape_no_whitespace(self):
self.assertEqual("'foo'", utils.quote_and_escape("foo"))
def test_quote_and_escape_whitespace(self):
self.assertEqual("'foo bar'", utils.quote_and_escape("foo bar"))
def test_quote_and_escape_whitespace_with_quotes(self):
self.assertEqual("'foo '\\''bar'\\'''",
utils.quote_and_escape("foo 'bar'"))
def test_quote_and_escape_non_string(self):
self.assertEqual(True, utils.quote_and_escape(True))

View File

@ -116,3 +116,19 @@ def run_command(cmd, quiet=False, check_output=False, **kwargs):
return subprocess.check_output(cmd, **kwargs) return subprocess.check_output(cmd, **kwargs)
else: else:
subprocess.check_call(cmd, **kwargs) subprocess.check_call(cmd, **kwargs)
def quote_and_escape(value):
"""Quote and escape a string.
Adds enclosing single quotes to the string passed, and escapes single
quotes within the string using backslashes. This is useful for passing
'extra vars' to Ansible. Without this, Ansible only uses the part of the
string up to the first whitespace.
:param value: the string to quote and escape.
:returns: the quoted and escaped string.
"""
if not isinstance(value, six.string_types):
return value
return "'" + value.replace("'", "'\\''") + "'"

View File

@ -0,0 +1,6 @@
---
fixes:
- |
Fixes an issue where CLI arguments containing whitespace that are passed to
Ansible needed to be quoted. See `Story 2004379
<https://storyboard.openstack.org/#!/story/2004379>`__ for details.