Merge lp:~lamont/maas/bug-1630636b into lp:~maas-committers/maas/trunk

Proposed by LaMont Jones
Status: Merged
Approved by: LaMont Jones
Approved revision: no longer in the source branch.
Merged at revision: 5482
Proposed branch: lp:~lamont/maas/bug-1630636b
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 156 lines (+82/-11)
3 files modified
contrib/preseeds_v2/curtin_userdata (+1/-1)
src/maasserver/preseed.py (+3/-3)
src/maasserver/tests/test_preseed.py (+78/-7)
To merge this branch: bzr merge lp:~lamont/maas/bug-1630636b
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Blake Rouse (community) Needs Information
Andres Rodriguez (community) Needs Information
Review via email: mp+308300@code.launchpad.net

Commit message

Only escape single-quotes in the "turn off pxe boot" url given to the deploying node.

Description of the change

Instead of shell escaping the URL for "turning off pxe boot", just url-escape any single-quotes, since the url is (1) enclosed in single-quotes by the template, and (2) under the control of the MAAS admin, and running (as root) on the deploying node, which he already has full control over.

Because of the [] surrounding an ipv6 literal, pipes.quote() decides to wrap the entire thing in single quotes, which was breaking the template by causing the url to be outside of the quotes.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

Question inline:

review: Needs Information
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Code looks good, but I have the same question as Andres.

review: Needs Information
Revision history for this message
Gavin Panella (allenap) wrote :

I think this bug can be fixed imperfectly and more simply with http://paste.ubuntu.com/23314598/. The fix in this branch works but is also imperfect and a bit misleading. The test_get_curtin_config* tests also do not fail if I change quote_url to return the input unaltered.

A better fix might be http://paste.ubuntu.com/23314621/ (which works because YAML is a superset of JSON). Could you incorporate this approach into your branch, and change those tests so that they fail when the fix is absent? The preseeds are meant to be YAML, right? You could try loading them using yaml.safe_load to check that they're well formed.

review: Needs Fixing
Revision history for this message
LaMont Jones (lamont) wrote :

replies added. Will be pushing a branch with feedback incorporated.

Revision history for this message
Gavin Panella (allenap) :
Revision history for this message
Gavin Panella (allenap) wrote :

I think you've misunderstood what I suggested.

The point in the template where escape.shell was being used is a YAML array element. To correctly escape a value into that spot you can use json.dumps to produce something that conforms to YAML's escaping and encoding rules. Replacing single quotes with %27 is not the same.

I've put what I mean into lp:~allenap/maas/lamont--bug-1630636b which you can pull/merge into this branch. I've updated code and tests.

review: Needs Fixing
Revision history for this message
LaMont Jones (lamont) wrote :

Pulled Gavin's branch, made one small tweak to the test scenario generation.

Revision history for this message
Gavin Panella (allenap) wrote :

Looks good!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'contrib/preseeds_v2/curtin_userdata'
2--- contrib/preseeds_v2/curtin_userdata 2016-10-06 01:36:10 +0000
3+++ contrib/preseeds_v2/curtin_userdata 2016-10-13 16:33:14 +0000
4@@ -14,7 +14,7 @@
5 driver_04_load: ["sh", "-c", "depmod && modprobe {{driver['module']}}"]
6 {{endif}}
7 late_commands:
8- maas: [wget, '--no-proxy', '{{node_disable_pxe_url|escape.shell}}', '--post-data', '{{node_disable_pxe_data|escape.shell}}', '-O', '/dev/null']
9+ maas: [wget, '--no-proxy', {{node_disable_pxe_url|escape.json}}, '--post-data', {{node_disable_pxe_data|escape.json}}, '-O', '/dev/null']
10 {{if third_party_drivers and driver}}
11 driver_00_key_get: curtin in-target -- sh -c "/bin/echo -en '{{key_string}}' > /tmp/maas-{{driver['package']}}.gpg"
12 driver_02_key_add: ["curtin", "in-target", "--", "apt-key", "add", "/tmp/maas-{{driver['package']}}.gpg"]
13
14=== modified file 'src/maasserver/preseed.py'
15--- src/maasserver/preseed.py 2016-09-02 10:17:35 +0000
16+++ src/maasserver/preseed.py 2016-10-13 16:33:14 +0000
17@@ -15,8 +15,8 @@
18 ]
19
20 from collections import namedtuple
21+import json
22 import os.path
23-from pipes import quote
24 from urllib.parse import (
25 urlencode,
26 urlparse,
27@@ -590,8 +590,8 @@
28 """Return a singleton containing methods to escape various formats used in
29 the preseed templates.
30 """
31- Escape = namedtuple('Escape', 'shell')
32- return Escape(shell=quote)
33+ Escape = namedtuple('Escape', 'json')
34+ return Escape(json=json.dumps)
35
36
37 class PreseedTemplate(tempita.Template):
38
39=== modified file 'src/maasserver/tests/test_preseed.py'
40--- src/maasserver/tests/test_preseed.py 2016-09-27 20:58:10 +0000
41+++ src/maasserver/tests/test_preseed.py 2016-10-13 16:33:14 +0000
42@@ -6,8 +6,8 @@
43 __all__ = []
44
45 import http.client
46+import json
47 import os
48-from pipes import quote
49 from unittest.mock import sentinel
50 from urllib.parse import urlparse
51
52@@ -80,6 +80,7 @@
53 MockCalledOnceWith,
54 MockNotCalled,
55 )
56+from maastesting.testcase import MAASTestCase
57 from metadataserver.models import NodeKey
58 from provisioningserver.drivers.osystem.ubuntu import UbuntuOS
59 from provisioningserver.rpc.exceptions import NoConnectionsAvailable
60@@ -488,14 +489,21 @@
61 context['third_party_drivers'])
62
63
64-class TestPreseedTemplate(MAASServerTestCase):
65+class TestPreseedTemplate(MAASTestCase):
66 """Tests for class:`PreseedTemplate`."""
67
68- def test_escape_shell(self):
69- template = PreseedTemplate("{{var|escape.shell}}")
70- var = "$ ! ()"
71- observed = template.substitute(var=var)
72- self.assertEqual(quote(var), observed)
73+ scenarios = [
74+ (name, dict(var=var, expected=json.dumps(var))) for name, var in (
75+ ('plain', '$ ! ()'),
76+ ('quote', "$ ' ()"),
77+ ('double', '$ " ()'),
78+ )
79+ ]
80+
81+ def test_escape_json(self):
82+ template = PreseedTemplate("{{var|escape.json}}")
83+ observed = template.substitute(var=self.var)
84+ self.assertEqual(self.expected, observed)
85
86
87 class TestRenderPreseed(
88@@ -851,6 +859,69 @@
89 ]
90 ))
91
92+ def test_get_curtin_config_with_ipv4_rack_url(self):
93+ primary_rack = self.rpc_rack_controller
94+ primary_rack.url = 'http://192.168.1.1:5240/'
95+ primary_rack.save()
96+ node = factory.make_Node_with_Interface_on_Subnet(
97+ primary_rack=primary_rack)
98+ self.configure_get_boot_images_for_node(node, 'xinstall')
99+ config = get_curtin_config(node)
100+ yaml_conf = yaml.safe_load(config)
101+ self.assertEqual('reboot', yaml_conf['power_state']['mode'])
102+ self.assertEqual(
103+ "%smetadata/latest/by-id/%s/" % (
104+ primary_rack.url, node.system_id),
105+ yaml_conf['late_commands']['maas'][2])
106+ self.assertTrue('debconf_selections' in yaml_conf)
107+
108+ def test_get_curtin_config_with_ipv6_rack_url(self):
109+ primary_rack = self.rpc_rack_controller
110+ primary_rack.url = 'http://[2001:db8::1]:5240/'
111+ primary_rack.save()
112+ node = factory.make_Node_with_Interface_on_Subnet(
113+ primary_rack=primary_rack)
114+ self.configure_get_boot_images_for_node(node, 'xinstall')
115+ config = get_curtin_config(node)
116+ yaml_conf = yaml.safe_load(config)
117+ self.assertEqual('reboot', yaml_conf['power_state']['mode'])
118+ self.assertEqual(
119+ "%smetadata/latest/by-id/%s/" % (
120+ primary_rack.url, node.system_id),
121+ yaml_conf['late_commands']['maas'][2])
122+ self.assertTrue('debconf_selections' in yaml_conf)
123+
124+ def test_get_curtin_config_with_name_rack_url(self):
125+ primary_rack = self.rpc_rack_controller
126+ primary_rack.url = 'http://%s:5240/' % factory.make_name('host')
127+ primary_rack.save()
128+ node = factory.make_Node_with_Interface_on_Subnet(
129+ primary_rack=primary_rack)
130+ self.configure_get_boot_images_for_node(node, 'xinstall')
131+ config = get_curtin_config(node)
132+ yaml_conf = yaml.safe_load(config)
133+ self.assertEqual('reboot', yaml_conf['power_state']['mode'])
134+ self.assertEqual(
135+ "%smetadata/latest/by-id/%s/" % (
136+ primary_rack.url, node.system_id),
137+ yaml_conf['late_commands']['maas'][2])
138+ self.assertTrue('debconf_selections' in yaml_conf)
139+
140+ def test_get_curtin_config_with_quote_rack_url(self):
141+ primary_rack = self.rpc_rack_controller
142+ primary_rack.url = 'http://%s:5240/' % factory.make_name("ho'st")
143+ primary_rack.save()
144+ node = factory.make_Node_with_Interface_on_Subnet(
145+ primary_rack=primary_rack)
146+ self.configure_get_boot_images_for_node(node, 'xinstall')
147+ config = get_curtin_config(node)
148+ yaml_conf = yaml.safe_load(config)
149+ self.assertEqual('reboot', yaml_conf['power_state']['mode'])
150+ self.assertEqual(
151+ "%smetadata/latest/by-id/%s/" % (primary_rack.url, node.system_id),
152+ yaml_conf['late_commands']['maas'][2])
153+ self.assertTrue('debconf_selections' in yaml_conf)
154+
155 def make_fastpath_node(self, main_arch=None):
156 """Return a `Node`, with FPI enabled, and the given main architecture.
157