Merge lp:~racb/maas/arch-detect into lp:~maas-committers/maas/trunk
- arch-detect
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Robie Basak |
Approved revision: | 1132 |
Merged at revision: | 1142 |
Proposed branch: | lp:~racb/maas/arch-detect |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
371 lines (+175/-45) 4 files modified
src/maasserver/api.py (+66/-12) src/maasserver/tests/test_api.py (+36/-25) src/provisioningserver/tests/test_tftp.py (+30/-3) src/provisioningserver/tftp.py (+43/-5) |
To merge this branch: | bzr merge lp:~racb/maas/arch-detect |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email:
|
Commit message
Add architecture detection
Rather than always responding to pxelinux.
This allows multiple architectures to be served the correct pxelinux.cfg configuration for enlistment without any needed intervention.
See bug 1041092 for details on pxelinux.
Description of the change

Gavin Panella (allenap) wrote : | # |
> ... I preferred to
> use them, because they behave differently, and 2to3 translates them
> anyway, but we, Red Squad, in days of old, agreed this. ...
There's a "not" missing from the start of the second line there, but
you probably guessed that.
- 1128. By Robie Basak
-
Use conditional expression for conciseness
- 1129. By Robie Basak
-
Improve TFTP fallback filename comments
- 1130. By Robie Basak
-
Use non-capturing regexp groups
- 1131. By Robie Basak
-
Fix formatting
- 1132. By Robie Basak
-
Use dict.items() instead of dict.iteritems()

Robie Basak (racb) wrote : | # |
[1] Done
[2] Done
[3] Clarified separator in corresponding comments
[4]
> Why is "arm" being sent instead of "armhf"? Should we normalise on just "arm"?
Outside userspace, the "hf" part of armhf has no meaning as there is no defined userspace ABI at that point. So before userspace, armel and armhf are identical. But MAAS does care about userspace. albeit only armhf userspace for ARM, because it needs to know which userspace to deploy. So pxelinux emulators should be agnostic of el/hf yet MAAS must be aware of it. So we must map it.
[5] Done
[6] Done
[7] Done
Thanks Gavin!
Preview Diff
1 | === modified file 'src/maasserver/api.py' | |||
2 | --- src/maasserver/api.py 2012-10-03 02:28:04 +0000 | |||
3 | +++ src/maasserver/api.py 2012-10-03 08:57:21 +0000 | |||
4 | @@ -1496,33 +1496,87 @@ | |||
5 | 1496 | return "poweroff" | 1496 | return "poweroff" |
6 | 1497 | 1497 | ||
7 | 1498 | 1498 | ||
8 | 1499 | def get_node_from_mac_string(mac_string): | ||
9 | 1500 | """Get a Node object from a MAC address string. | ||
10 | 1501 | |||
11 | 1502 | Returns a Node object or None if no node with the given MAC address exists. | ||
12 | 1503 | |||
13 | 1504 | :param mac_string: MAC address string in the form "12-34-56-78-9a-bc" | ||
14 | 1505 | :return: Node object or None | ||
15 | 1506 | """ | ||
16 | 1507 | if mac_string is None: | ||
17 | 1508 | return None | ||
18 | 1509 | macaddress = get_one(MACAddress.objects.filter(mac_address=mac_string)) | ||
19 | 1510 | return macaddress.node if macaddress else None | ||
20 | 1511 | |||
21 | 1512 | |||
22 | 1499 | def pxeconfig(request): | 1513 | def pxeconfig(request): |
23 | 1500 | """Get the PXE configuration given a node's details. | 1514 | """Get the PXE configuration given a node's details. |
24 | 1501 | 1515 | ||
25 | 1502 | Returns a JSON object corresponding to a | 1516 | Returns a JSON object corresponding to a |
26 | 1503 | :class:`provisioningserver.kernel_opts.KernelParameters` instance. | 1517 | :class:`provisioningserver.kernel_opts.KernelParameters` instance. |
27 | 1504 | 1518 | ||
28 | 1519 | This is now fairly decoupled from pxelinux's TFTP filename encoding | ||
29 | 1520 | mechanism, with one notable exception. Call this function with (mac, arch, | ||
30 | 1521 | subarch) and it will do the right thing. If details it needs are missing | ||
31 | 1522 | (ie. arch/subarch missing when the MAC is supplied but unknown), then it | ||
32 | 1523 | will as an exception return an HTTP NO_CONTENT (204) in the expectation | ||
33 | 1524 | that this will be translated to a TFTP file not found and pxelinux (or an | ||
34 | 1525 | emulator) will fall back to default-<arch>-<subarch> (in the case of an | ||
35 | 1526 | alternate architecture emulator) or just straight to default (in the case | ||
36 | 1527 | of native pxelinux on i386 or amd64). See bug 1041092 for details and | ||
37 | 1528 | discussion. | ||
38 | 1529 | |||
39 | 1505 | :param mac: MAC address to produce a boot configuration for. | 1530 | :param mac: MAC address to produce a boot configuration for. |
40 | 1531 | :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not | ||
41 | 1532 | 'armhf'). | ||
42 | 1533 | :param subarch: Subarchitecture name (in the pxelinux namespace). | ||
43 | 1506 | """ | 1534 | """ |
45 | 1507 | mac = get_mandatory_param(request.GET, 'mac') | 1535 | node = get_node_from_mac_string(request.GET.get('mac', None)) |
46 | 1508 | 1536 | ||
58 | 1509 | macaddress = get_one(MACAddress.objects.filter(mac_address=mac)) | 1537 | if node: |
48 | 1510 | if macaddress is None: | ||
49 | 1511 | # Default to i386 as a works-for-all solution. This will not support | ||
50 | 1512 | # non-x86 architectures, but for now this assumption holds. | ||
51 | 1513 | node = None | ||
52 | 1514 | arch, subarch = ARCHITECTURE.i386.split('/') | ||
53 | 1515 | preseed_url = compose_enlistment_preseed_url() | ||
54 | 1516 | hostname = 'maas-enlist' | ||
55 | 1517 | domain = Config.objects.get_config('enlistment_domain') | ||
56 | 1518 | else: | ||
57 | 1519 | node = macaddress.node | ||
59 | 1520 | arch, subarch = node.architecture.split('/') | 1538 | arch, subarch = node.architecture.split('/') |
60 | 1521 | preseed_url = compose_preseed_url(node) | 1539 | preseed_url = compose_preseed_url(node) |
61 | 1522 | # The node's hostname may include a domain, but we ignore that | 1540 | # The node's hostname may include a domain, but we ignore that |
62 | 1523 | # and use the one from the nodegroup instead. | 1541 | # and use the one from the nodegroup instead. |
63 | 1524 | hostname = node.hostname.split('.', 1)[0] | 1542 | hostname = node.hostname.split('.', 1)[0] |
64 | 1525 | domain = node.nodegroup.name | 1543 | domain = node.nodegroup.name |
65 | 1544 | else: | ||
66 | 1545 | try: | ||
67 | 1546 | pxelinux_arch = request.GET['arch'] | ||
68 | 1547 | except KeyError: | ||
69 | 1548 | if 'mac' in request.GET: | ||
70 | 1549 | # Request was pxelinux.cfg/01-<mac>, so attempt fall back | ||
71 | 1550 | # to pxelinux.cfg/default-<arch>-<subarch> for arch detection. | ||
72 | 1551 | return HttpResponse(status=httplib.NO_CONTENT) | ||
73 | 1552 | else: | ||
74 | 1553 | # Request has already fallen back, so if arch is still not | ||
75 | 1554 | # provided then use i386. | ||
76 | 1555 | arch = ARCHITECTURE.i386.split('/')[0] | ||
77 | 1556 | else: | ||
78 | 1557 | # Map from pxelinux namespace architecture names to MAAS namespace | ||
79 | 1558 | # architecture names. If this gets bigger, an external lookup table | ||
80 | 1559 | # would make sense. But here is fine for something as trivial as it | ||
81 | 1560 | # is right now. | ||
82 | 1561 | if pxelinux_arch == 'arm': | ||
83 | 1562 | arch = 'armhf' | ||
84 | 1563 | else: | ||
85 | 1564 | arch = pxelinux_arch | ||
86 | 1565 | |||
87 | 1566 | # Use subarch if supplied; otherwise assume 'generic'. | ||
88 | 1567 | try: | ||
89 | 1568 | pxelinux_subarch = request.GET['subarch'] | ||
90 | 1569 | except KeyError: | ||
91 | 1570 | subarch = 'generic' | ||
92 | 1571 | else: | ||
93 | 1572 | # Map from pxelinux namespace subarchitecture names to MAAS | ||
94 | 1573 | # namespace subarchitecture names. Right now this happens to be a | ||
95 | 1574 | # 1-1 mapping. | ||
96 | 1575 | subarch = pxelinux_subarch | ||
97 | 1576 | |||
98 | 1577 | preseed_url = compose_enlistment_preseed_url() | ||
99 | 1578 | hostname = 'maas-enlist' | ||
100 | 1579 | domain = Config.objects.get_config('enlistment_domain') | ||
101 | 1526 | 1580 | ||
102 | 1527 | if node is None or node.status == NODE_STATUS.COMMISSIONING: | 1581 | if node is None or node.status == NODE_STATUS.COMMISSIONING: |
103 | 1528 | series = Config.objects.get_config('commissioning_distro_series') | 1582 | series = Config.objects.get_config('commissioning_distro_series') |
104 | 1529 | 1583 | ||
105 | === modified file 'src/maasserver/tests/test_api.py' | |||
106 | --- src/maasserver/tests/test_api.py 2012-10-03 02:28:04 +0000 | |||
107 | +++ src/maasserver/tests/test_api.py 2012-10-03 08:57:21 +0000 | |||
108 | @@ -2715,21 +2715,22 @@ | |||
109 | 2715 | 2715 | ||
110 | 2716 | class TestPXEConfigAPI(AnonAPITestCase): | 2716 | class TestPXEConfigAPI(AnonAPITestCase): |
111 | 2717 | 2717 | ||
113 | 2718 | def get_params(self): | 2718 | def get_mac_params(self): |
114 | 2719 | return {'mac': factory.make_mac_address().mac_address} | 2719 | return {'mac': factory.make_mac_address().mac_address} |
115 | 2720 | 2720 | ||
118 | 2721 | def get_optional_params(self): | 2721 | def get_default_params(self): |
119 | 2722 | return ['mac'] | 2722 | return dict() |
120 | 2723 | 2723 | ||
121 | 2724 | def get_pxeconfig(self, params=None): | 2724 | def get_pxeconfig(self, params=None): |
122 | 2725 | """Make a request to `pxeconfig`, and return its response dict.""" | 2725 | """Make a request to `pxeconfig`, and return its response dict.""" |
123 | 2726 | if params is None: | 2726 | if params is None: |
125 | 2727 | params = self.get_params() | 2727 | params = self.get_default_params() |
126 | 2728 | response = self.client.get(reverse('pxeconfig'), params) | 2728 | response = self.client.get(reverse('pxeconfig'), params) |
127 | 2729 | return json.loads(response.content) | 2729 | return json.loads(response.content) |
128 | 2730 | 2730 | ||
129 | 2731 | def test_pxeconfig_returns_json(self): | 2731 | def test_pxeconfig_returns_json(self): |
131 | 2732 | response = self.client.get(reverse('pxeconfig'), self.get_params()) | 2732 | response = self.client.get( |
132 | 2733 | reverse('pxeconfig'), self.get_default_params()) | ||
133 | 2733 | self.assertThat( | 2734 | self.assertThat( |
134 | 2734 | ( | 2735 | ( |
135 | 2735 | response.status_code, | 2736 | response.status_code, |
136 | @@ -2751,7 +2752,28 @@ | |||
137 | 2751 | self.get_pxeconfig(), | 2752 | self.get_pxeconfig(), |
138 | 2752 | ContainsAll(KernelParameters._fields)) | 2753 | ContainsAll(KernelParameters._fields)) |
139 | 2753 | 2754 | ||
141 | 2754 | def test_pxeconfig_defaults_to_i386_when_node_unknown(self): | 2755 | def test_pxeconfig_returns_data_for_known_node(self): |
142 | 2756 | params = self.get_mac_params() | ||
143 | 2757 | node = MACAddress.objects.get(mac_address=params['mac']).node | ||
144 | 2758 | response = self.client.get(reverse('pxeconfig'), params) | ||
145 | 2759 | self.assertEqual(httplib.OK, response.status_code) | ||
146 | 2760 | |||
147 | 2761 | def test_pxeconfig_returns_no_content_for_unknown_node(self): | ||
148 | 2762 | params = dict(mac=factory.getRandomMACAddress(delimiter=b'-')) | ||
149 | 2763 | response = self.client.get(reverse('pxeconfig'), params) | ||
150 | 2764 | self.assertEqual(httplib.NO_CONTENT, response.status_code) | ||
151 | 2765 | |||
152 | 2766 | def test_pxeconfig_returns_data_for_detailed_but_unknown_node(self): | ||
153 | 2767 | architecture = factory.getRandomEnum(ARCHITECTURE) | ||
154 | 2768 | arch, subarch = architecture.split('/') | ||
155 | 2769 | params = dict( | ||
156 | 2770 | mac=factory.getRandomMACAddress(delimiter=b'-'), | ||
157 | 2771 | arch=arch, | ||
158 | 2772 | subarch=subarch) | ||
159 | 2773 | response = self.client.get(reverse('pxeconfig'), params) | ||
160 | 2774 | self.assertEqual(httplib.OK, response.status_code) | ||
161 | 2775 | |||
162 | 2776 | def test_pxeconfig_defaults_to_i386_for_default(self): | ||
163 | 2755 | # As a lowest-common-denominator, i386 is chosen when the node is not | 2777 | # As a lowest-common-denominator, i386 is chosen when the node is not |
164 | 2756 | # yet known to MAAS. | 2778 | # yet known to MAAS. |
165 | 2757 | expected_arch = tuple(ARCHITECTURE.i386.split('/')) | 2779 | expected_arch = tuple(ARCHITECTURE.i386.split('/')) |
166 | @@ -2760,16 +2782,12 @@ | |||
167 | 2760 | self.assertEqual(expected_arch, observed_arch) | 2782 | self.assertEqual(expected_arch, observed_arch) |
168 | 2761 | 2783 | ||
169 | 2762 | def test_pxeconfig_uses_fixed_hostname_for_enlisting_node(self): | 2784 | def test_pxeconfig_uses_fixed_hostname_for_enlisting_node(self): |
174 | 2763 | new_mac = factory.getRandomMACAddress() | 2785 | self.assertEqual('maas-enlist', self.get_pxeconfig().get('hostname')) |
171 | 2764 | self.assertEqual( | ||
172 | 2765 | 'maas-enlist', | ||
173 | 2766 | self.get_pxeconfig({'mac': new_mac}).get('hostname')) | ||
175 | 2767 | 2786 | ||
176 | 2768 | def test_pxeconfig_uses_enlistment_domain_for_enlisting_node(self): | 2787 | def test_pxeconfig_uses_enlistment_domain_for_enlisting_node(self): |
177 | 2769 | new_mac = factory.getRandomMACAddress() | ||
178 | 2770 | self.assertEqual( | 2788 | self.assertEqual( |
179 | 2771 | Config.objects.get_config('enlistment_domain'), | 2789 | Config.objects.get_config('enlistment_domain'), |
181 | 2772 | self.get_pxeconfig({'mac': new_mac}).get('domain')) | 2790 | self.get_pxeconfig().get('domain')) |
182 | 2773 | 2791 | ||
183 | 2774 | def test_pxeconfig_splits_domain_from_node_hostname(self): | 2792 | def test_pxeconfig_splits_domain_from_node_hostname(self): |
184 | 2775 | host = factory.make_name('host') | 2793 | host = factory.make_name('host') |
185 | @@ -2800,24 +2818,16 @@ | |||
186 | 2800 | kernel_opts, 'get_ephemeral_name', | 2818 | kernel_opts, 'get_ephemeral_name', |
187 | 2801 | FakeMethod(result=factory.getRandomString())) | 2819 | FakeMethod(result=factory.getRandomString())) |
188 | 2802 | 2820 | ||
200 | 2803 | def test_pxeconfig_requires_mac_address(self): | 2821 | def test_pxeconfig_has_enlistment_preseed_url_for_default(self): |
201 | 2804 | # The `mac` parameter is mandatory. | 2822 | self.silence_get_ephemeral_name() |
202 | 2805 | self.silence_get_ephemeral_name() | 2823 | params = self.get_default_params() |
192 | 2806 | self.assertEqual( | ||
193 | 2807 | httplib.BAD_REQUEST, | ||
194 | 2808 | self.get_without_param("mac").status_code) | ||
195 | 2809 | |||
196 | 2810 | def test_pxeconfig_has_enlistment_preseed_url_for_unknown_node(self): | ||
197 | 2811 | self.silence_get_ephemeral_name() | ||
198 | 2812 | params = self.get_params() | ||
199 | 2813 | params['mac'] = factory.getRandomMACAddress() | ||
203 | 2814 | response = self.client.get(reverse('pxeconfig'), params) | 2824 | response = self.client.get(reverse('pxeconfig'), params) |
204 | 2815 | self.assertEqual( | 2825 | self.assertEqual( |
205 | 2816 | compose_enlistment_preseed_url(), | 2826 | compose_enlistment_preseed_url(), |
206 | 2817 | json.loads(response.content)["preseed_url"]) | 2827 | json.loads(response.content)["preseed_url"]) |
207 | 2818 | 2828 | ||
208 | 2819 | def test_pxeconfig_has_preseed_url_for_known_node(self): | 2829 | def test_pxeconfig_has_preseed_url_for_known_node(self): |
210 | 2820 | params = self.get_params() | 2830 | params = self.get_mac_params() |
211 | 2821 | node = MACAddress.objects.get(mac_address=params['mac']).node | 2831 | node = MACAddress.objects.get(mac_address=params['mac']).node |
212 | 2822 | response = self.client.get(reverse('pxeconfig'), params) | 2832 | response = self.client.get(reverse('pxeconfig'), params) |
213 | 2823 | self.assertEqual( | 2833 | self.assertEqual( |
214 | @@ -2852,7 +2862,8 @@ | |||
215 | 2852 | def test_pxeconfig_uses_boot_purpose(self): | 2862 | def test_pxeconfig_uses_boot_purpose(self): |
216 | 2853 | fake_boot_purpose = factory.make_name("purpose") | 2863 | fake_boot_purpose = factory.make_name("purpose") |
217 | 2854 | self.patch(api, "get_boot_purpose", lambda node: fake_boot_purpose) | 2864 | self.patch(api, "get_boot_purpose", lambda node: fake_boot_purpose) |
219 | 2855 | response = self.client.get(reverse('pxeconfig'), self.get_params()) | 2865 | response = self.client.get(reverse('pxeconfig'), |
220 | 2866 | self.get_default_params()) | ||
221 | 2856 | self.assertEqual( | 2867 | self.assertEqual( |
222 | 2857 | fake_boot_purpose, | 2868 | fake_boot_purpose, |
223 | 2858 | json.loads(response.content)["purpose"]) | 2869 | json.loads(response.content)["purpose"]) |
224 | 2859 | 2870 | ||
225 | === modified file 'src/provisioningserver/tests/test_tftp.py' | |||
226 | --- src/provisioningserver/tests/test_tftp.py 2012-08-30 10:37:26 +0000 | |||
227 | +++ src/provisioningserver/tests/test_tftp.py 2012-10-03 08:57:21 +0000 | |||
228 | @@ -70,7 +70,9 @@ | |||
229 | 70 | The path is intended to match `re_config_file`, and the components are | 70 | The path is intended to match `re_config_file`, and the components are |
230 | 71 | the expected groups from a match. | 71 | the expected groups from a match. |
231 | 72 | """ | 72 | """ |
233 | 73 | components = {"mac": factory.getRandomMACAddress(b"-")} | 73 | components = {"mac": factory.getRandomMACAddress(b"-"), |
234 | 74 | "arch": None, | ||
235 | 75 | "subarch": None} | ||
236 | 74 | config_path = compose_config_path(components["mac"]) | 76 | config_path = compose_config_path(components["mac"]) |
237 | 75 | return config_path, components | 77 | return config_path, components |
238 | 76 | 78 | ||
239 | @@ -112,13 +114,15 @@ | |||
240 | 112 | mac = 'aa-bb-cc-dd-ee-ff' | 114 | mac = 'aa-bb-cc-dd-ee-ff' |
241 | 113 | match = TFTPBackend.re_config_file.match('pxelinux.cfg/01-%s' % mac) | 115 | match = TFTPBackend.re_config_file.match('pxelinux.cfg/01-%s' % mac) |
242 | 114 | self.assertIsNotNone(match) | 116 | self.assertIsNotNone(match) |
244 | 115 | self.assertEqual({'mac': mac}, match.groupdict()) | 117 | self.assertEqual({'mac': mac, 'arch': None, 'subarch': None}, |
245 | 118 | match.groupdict()) | ||
246 | 116 | 119 | ||
247 | 117 | def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self): | 120 | def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self): |
248 | 118 | mac = 'aa-bb-cc-dd-ee-ff' | 121 | mac = 'aa-bb-cc-dd-ee-ff' |
249 | 119 | match = TFTPBackend.re_config_file.match('/pxelinux.cfg/01-%s' % mac) | 122 | match = TFTPBackend.re_config_file.match('/pxelinux.cfg/01-%s' % mac) |
250 | 120 | self.assertIsNotNone(match) | 123 | self.assertIsNotNone(match) |
252 | 121 | self.assertEqual({'mac': mac}, match.groupdict()) | 124 | self.assertEqual({'mac': mac, 'arch': None, 'subarch': None}, |
253 | 125 | match.groupdict()) | ||
254 | 122 | 126 | ||
255 | 123 | def test_re_config_file_does_not_match_non_config_file(self): | 127 | def test_re_config_file_does_not_match_non_config_file(self): |
256 | 124 | self.assertIsNone( | 128 | self.assertIsNone( |
257 | @@ -132,6 +136,29 @@ | |||
258 | 132 | self.assertIsNone( | 136 | self.assertIsNone( |
259 | 133 | TFTPBackend.re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff')) | 137 | TFTPBackend.re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff')) |
260 | 134 | 138 | ||
261 | 139 | def test_re_config_file_with_default(self): | ||
262 | 140 | match = TFTPBackend.re_config_file.match('pxelinux.cfg/default') | ||
263 | 141 | self.assertIsNotNone(match) | ||
264 | 142 | self.assertEqual({'mac': None, 'arch': None, 'subarch': None}, | ||
265 | 143 | match.groupdict()) | ||
266 | 144 | |||
267 | 145 | def test_re_config_file_with_default_arch(self): | ||
268 | 146 | arch = factory.make_name('arch', sep='') | ||
269 | 147 | match = TFTPBackend.re_config_file.match('pxelinux.cfg/default.%s' % | ||
270 | 148 | arch) | ||
271 | 149 | self.assertIsNotNone(match) | ||
272 | 150 | self.assertEqual({'mac': None, 'arch': arch, 'subarch': None}, | ||
273 | 151 | match.groupdict()) | ||
274 | 152 | |||
275 | 153 | def test_re_config_file_with_default_arch_and_subarch(self): | ||
276 | 154 | arch = factory.make_name('arch', sep='') | ||
277 | 155 | subarch = factory.make_name('subarch', sep='') | ||
278 | 156 | match = TFTPBackend.re_config_file.match( | ||
279 | 157 | 'pxelinux.cfg/default.%s-%s' % (arch, subarch)) | ||
280 | 158 | self.assertIsNotNone(match) | ||
281 | 159 | self.assertEqual({'mac': None, 'arch': arch, 'subarch': subarch}, | ||
282 | 160 | match.groupdict()) | ||
283 | 161 | |||
284 | 135 | 162 | ||
285 | 136 | class TestTFTPBackend(TestCase): | 163 | class TestTFTPBackend(TestCase): |
286 | 137 | """Tests for `provisioningserver.tftp.TFTPBackend`.""" | 164 | """Tests for `provisioningserver.tftp.TFTPBackend`.""" |
287 | 138 | 165 | ||
288 | === modified file 'src/provisioningserver/tftp.py' | |||
289 | --- src/provisioningserver/tftp.py 2012-09-12 19:56:23 +0000 | |||
290 | +++ src/provisioningserver/tftp.py 2012-10-03 08:57:21 +0000 | |||
291 | @@ -14,6 +14,7 @@ | |||
292 | 14 | "TFTPBackend", | 14 | "TFTPBackend", |
293 | 15 | ] | 15 | ] |
294 | 16 | 16 | ||
295 | 17 | import httplib | ||
296 | 17 | from io import BytesIO | 18 | from io import BytesIO |
297 | 18 | from itertools import repeat | 19 | from itertools import repeat |
298 | 19 | import json | 20 | import json |
299 | @@ -32,7 +33,9 @@ | |||
300 | 32 | FilesystemSynchronousBackend, | 33 | FilesystemSynchronousBackend, |
301 | 33 | IReader, | 34 | IReader, |
302 | 34 | ) | 35 | ) |
303 | 36 | from tftp.errors import FileNotFound | ||
304 | 35 | from twisted.web.client import getPage | 37 | from twisted.web.client import getPage |
305 | 38 | import twisted.web.error | ||
306 | 36 | from zope.interface import implementer | 39 | from zope.interface import implementer |
307 | 37 | 40 | ||
308 | 38 | 41 | ||
309 | @@ -89,9 +92,18 @@ | |||
310 | 89 | ^/* | 92 | ^/* |
311 | 90 | pxelinux[.]cfg # PXELINUX expects this. | 93 | pxelinux[.]cfg # PXELINUX expects this. |
312 | 91 | / | 94 | / |
316 | 92 | {htype:02x} # ARP HTYPE. | 95 | (?: # either a MAC |
317 | 93 | - | 96 | {htype:02x} # ARP HTYPE. |
318 | 94 | (?P<mac>{re_mac_address.pattern}) # Capture MAC. | 97 | - |
319 | 98 | (?P<mac>{re_mac_address.pattern}) # Capture MAC. | ||
320 | 99 | | # or "default" | ||
321 | 100 | default | ||
322 | 101 | (?: # perhaps with specified arch, with a separator of either '-' | ||
323 | 102 | # or '.', since the spec was changed and both are unambiguous | ||
324 | 103 | [.-](?P<arch>\w+) # arch | ||
325 | 104 | (?:-(?P<subarch>\w+))? # optional subarch | ||
326 | 105 | )? | ||
327 | 106 | ) | ||
328 | 95 | $ | 107 | $ |
329 | 96 | '''.format( | 108 | '''.format( |
330 | 97 | htype=ARP_HTYPE.ETHERNET, | 109 | htype=ARP_HTYPE.ETHERNET, |
331 | @@ -162,6 +174,25 @@ | |||
332 | 162 | d.addCallback(BytesReader) | 174 | d.addCallback(BytesReader) |
333 | 163 | return d | 175 | return d |
334 | 164 | 176 | ||
335 | 177 | @staticmethod | ||
336 | 178 | def get_page_errback(failure, file_name): | ||
337 | 179 | failure.trap(twisted.web.error.Error) | ||
338 | 180 | # This twisted.web.error.Error.status object ends up being a | ||
339 | 181 | # string for some reason, but the constants we can compare against | ||
340 | 182 | # (both in httplib and twisted.web.http) are ints. | ||
341 | 183 | try: | ||
342 | 184 | status_int = int(failure.value.status) | ||
343 | 185 | except ValueError: | ||
344 | 186 | # Assume that it's some other error and propagate it | ||
345 | 187 | return failure | ||
346 | 188 | |||
347 | 189 | if status_int == httplib.NO_CONTENT: | ||
348 | 190 | # Convert HTTP No Content to a TFTP file not found | ||
349 | 191 | raise FileNotFound(file_name) | ||
350 | 192 | else: | ||
351 | 193 | # Otherwise propogate the unknown error | ||
352 | 194 | return failure | ||
353 | 195 | |||
354 | 165 | @deferred | 196 | @deferred |
355 | 166 | def get_reader(self, file_name): | 197 | def get_reader(self, file_name): |
356 | 167 | """See `IBackend.get_reader()`. | 198 | """See `IBackend.get_reader()`. |
357 | @@ -174,5 +205,12 @@ | |||
358 | 174 | if config_file_match is None: | 205 | if config_file_match is None: |
359 | 175 | return super(TFTPBackend, self).get_reader(file_name) | 206 | return super(TFTPBackend, self).get_reader(file_name) |
360 | 176 | else: | 207 | else: |
363 | 177 | params = config_file_match.groupdict() | 208 | # Do not include any element that has not matched (ie. is None) |
364 | 178 | return self.get_config_reader(params) | 209 | params = { |
365 | 210 | key: value | ||
366 | 211 | for key, value in config_file_match.groupdict().items() | ||
367 | 212 | if value is not None | ||
368 | 213 | } | ||
369 | 214 | d = self.get_config_reader(params) | ||
370 | 215 | d.addErrback(self.get_page_errback, file_name) | ||
371 | 216 | return d |
Several minor comments, but they're mostly about style; the approach
is good, afaict.
[1]
+ if macaddress:
+ return macaddress.node
+ else:
+ return None
Don't know how you feel about conditional expressions, but this might
be as clear but more concise as:
return macaddress.node if macaddress else None
[2]
+ :param arch: Architecture name (in the pxelinux namespace, eg. 'arm' not
+ 'armhf').
Typically we align follow-on lines 4-spaces in from the first
line. Yay, another trivial nitpick :)
[3]
+ # Request was pxelinux. cfg/01- <mac>, so attempt fall back cfg/default- <arch> for arch detection.
+ # to pxelinux.
Is it default-<arch> or default.<arch>?
Update: the regex says either.
[4]
+ if pxelinux_arch == 'arm':
+ arch = 'armhf'
+ else:
+ arch = pxelinux_arch
One step in the direction of a lookup-table, and more concise:
arch = {"arm": "armhf" }.get(pxelinux_ arch, pxelinux_arch)
Er, but it's not very pretty. Never mind. Why is "arm" being sent
instead of "armhf"? Should we normalise on just "arm"?
[5]
+ response = self.client. get(reverse( 'pxeconfig' ), default_ params( ))
+ self.get_
We have followed the style of wrapping 4 spaces in most places:
response = self.client.get(
reverse( 'pxeconfig' ), self.get_ default_ params( ))
Also, when we need to break onto a newline, we break at the opening
brace, so not like this:
response = self.client. get(reverse( 'pxeconfig' ),
self. get_default_ params( ))
Too much code on Launchpad ended up in the rightmost 20 columns
(there, like here, we chose to use a right margin) so we changed the
convention.
Hardly needs saying, but Emacs' python-mode does this all perfectly,
right out of the box ;)
[6]
+ ( # either a MAC
Make this a non-capturing group, i.e. (?:...)
+ {htype:02x} # ARP HTYPE. {re_mac_ address. pattern} ) # Capture MAC.
+ -
+ (?P<mac>
+ | # or "default"
+ default
+ ( # perhaps with specified arch
Here too?
+ [.-](?P<arch>\w+) # arch \w+))? # optional subarch
+ (-(?P<subarch>
And here?
+ )?
[7]
+ params = {k: v file_match. groupdict( ).iteritems( )
+ for k, v in config_
+ if v is not None}
We have tended to avoid single-letter variable names (sorry, again,
this is Launchpad engineering heritage showing its face, along with my
pedantry). Also, we decided to use the non-iter methods on dicts, I
think because they're spelled the same as in Python 3. I preferred to
use them, because they behave differently, and 2to3 translates them
anyway, but we, Red Squad, in days of old, agreed this. So, we'd write
this as:
params = {
key: value file_match. groupdict( ).items( )
for key, value in config_
if value is not None
}