Merge ~newell-jensen/maas:lp1820984 into maas:master
- Git
- lp:~newell-jensen/maas
- lp1820984
- Merge into master
Status: | Merged |
---|---|
Approved by: | Newell Jensen |
Approved revision: | 8884e951001ec63c330d454ed154e76b8a53ef8b |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~newell-jensen/maas:lp1820984 |
Merge into: | maas:master |
Diff against target: |
325 lines (+130/-27) 6 files modified
src/maasserver/models/node.py (+7/-8) src/maasserver/models/tests/test_node.py (+7/-0) src/maasserver/rpc/boot.py (+15/-1) src/maasserver/rpc/tests/test_boot.py (+42/-0) src/provisioningserver/rackdservices/tests/test_tftp.py (+45/-15) src/provisioningserver/rackdservices/tftp.py (+14/-3) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Blake Rouse (community) | Approve | ||
Andres Rodriguez (community) | Needs Fixing | ||
Review via email: mp+366945@code.launchpad.net |
Commit message
LP: #1820984 -- Local boot a device if it tries PXE booting from MAAS.
Description of the change
Andres Rodriguez (andreserl) : | # |
Andres Rodriguez (andreserl) wrote : | # |
A quick q, for the devices we are managing or tracking in MAAS, do we /always/ create dhcp hostmaps ?
Newell Jensen (newell-jensen) wrote : | # |
> A quick q, for the devices we are managing or tracking in MAAS, do we /always/
> create dhcp hostmaps ?
No, I do not seeing any hostmaps in dhcpd.conf for devices. I updated the log message to use warning instead of info as well as the unit test.
Andres Rodriguez (andreserl) wrote : | # |
@Newell,
Can you provide the output of how things show up in both maas.log and rackd.log ? Looking at the bug report, the user was looking at rackd.log and i think we should also have such message in rackd.log (and in maas.log too).
Andres Rodriguez (andreserl) wrote : | # |
another comment inline.
Andres Rodriguez (andreserl) wrote : | # |
see inline
Newell Jensen (newell-jensen) : | # |
Newell Jensen (newell-jensen) wrote : | # |
> @Newell,
>
> Can you provide the output of how things show up in both maas.log and
> rackd.log ? Looking at the bug report, the user was looking at rackd.log and i
> think we should also have such message in rackd.log (and in maas.log too).
Currently, there is not output from maas.log when booting a device from MAAS. Only current output that we get for this is in rackd.log as shown here:
Newell Jensen (newell-jensen) wrote : | # |
Andres and Blake, do you guys both agree that this should be logged in both maas.log and rackd.log? If not, which one?
Blake Rouse (blake-rouse) wrote : | # |
Having the log in both places is acceptable.
Blake Rouse (blake-rouse) wrote : | # |
Looks good. Thanks for all the fixes and working through this.
MAAS Lander (maas-lander) wrote : | # |
LANDING
-b lp1820984 lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas
STATUS: FAILED BUILD
LOG: http://
Preview Diff
1 | diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py |
2 | index 10af042..0e7f4c5 100644 |
3 | --- a/src/maasserver/models/node.py |
4 | +++ b/src/maasserver/models/node.py |
5 | @@ -1351,6 +1351,9 @@ class Node(CleanSave, TimestampedModel): |
6 | @property |
7 | def ephemeral_deployment(self): |
8 | """Return if node is set to ephemeral deployment.""" |
9 | + # Devices should always local boot. |
10 | + if self.is_device: |
11 | + return False |
12 | return self.is_diskless or self.ephemeral_deploy |
13 | |
14 | def retrieve_storage_layout_issues( |
15 | @@ -4205,14 +4208,10 @@ class Node(CleanSave, TimestampedModel): |
16 | """ |
17 | Return a suitable "purpose" for this boot, e.g. "install". |
18 | """ |
19 | - # XXX: allenap bug=1031406 2012-07-31: The boot purpose is |
20 | - # still in flux. It may be that there will just be an |
21 | - # "ephemeral" environment and an "install" environment, and |
22 | - # the differing behaviour between, say, enlistment and |
23 | - # commissioning - both of which will use the "ephemeral" |
24 | - # environment - will be governed by varying the preseed or PXE |
25 | - # configuration. |
26 | - if self.status in COMMISSIONING_LIKE_STATUSES: |
27 | + if self.status == NODE_STATUS.DEFAULT and self.is_device: |
28 | + # Always local boot a device. |
29 | + return "local" |
30 | + elif self.status in COMMISSIONING_LIKE_STATUSES: |
31 | # It is commissioning or disk erasing. The environment (boot |
32 | # images, kernel options, etc for erasing is the same as that |
33 | # of commissioning. |
34 | diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py |
35 | index 2a721ae..548659c 100644 |
36 | --- a/src/maasserver/models/tests/test_node.py |
37 | +++ b/src/maasserver/models/tests/test_node.py |
38 | @@ -1051,6 +1051,10 @@ class TestNode(MAASServerTestCase): |
39 | node = factory.make_Node(ephemeral_deploy=False) |
40 | self.assertFalse(node.ephemeral_deployment) |
41 | |
42 | + def test_ephemeral_deployment_checks_is_device(self): |
43 | + device = factory.make_Device() |
44 | + self.assertFalse(device.ephemeral_deployment) |
45 | + |
46 | def test_system_id_is_a_valid_znum(self): |
47 | node = factory.make_Node() |
48 | self.assertThat( |
49 | @@ -3990,6 +3994,9 @@ class TestNode(MAASServerTestCase): |
50 | ("local", {"status": NODE_STATUS.DEPLOYING, "netboot": False}), |
51 | ("local", {"status": NODE_STATUS.DEPLOYED}), |
52 | ("poweroff", {"status": NODE_STATUS.RETIRED}), |
53 | + ("local", { |
54 | + "status": NODE_STATUS.DEFAULT, |
55 | + "node_type": NODE_TYPE.DEVICE}), |
56 | ] |
57 | node = factory.make_Node() |
58 | mock_get_boot_images_for = self.patch( |
59 | diff --git a/src/maasserver/rpc/boot.py b/src/maasserver/rpc/boot.py |
60 | index 4cbd7ab..e15c817 100644 |
61 | --- a/src/maasserver/rpc/boot.py |
62 | +++ b/src/maasserver/rpc/boot.py |
63 | @@ -39,6 +39,7 @@ from maasserver.third_party_drivers import get_third_party_driver |
64 | from maasserver.utils.orm import transactional |
65 | from maasserver.utils.osystems import validate_hwe_kernel |
66 | from provisioningserver.events import EVENT_TYPES |
67 | +from provisioningserver.logger import get_maas_logger |
68 | from provisioningserver.rpc.exceptions import BootConfigNoResponse |
69 | from provisioningserver.utils.network import get_source_address |
70 | from provisioningserver.utils.twisted import ( |
71 | @@ -48,6 +49,9 @@ from provisioningserver.utils.twisted import ( |
72 | from provisioningserver.utils.url import splithost |
73 | |
74 | |
75 | +maaslog = get_maas_logger("rpc.boot") |
76 | + |
77 | + |
78 | DEFAULT_ARCH = 'i386' |
79 | |
80 | |
81 | @@ -293,7 +297,7 @@ def get_final_boot_purpose(machine, arch, purpose): |
82 | def get_config( |
83 | system_id, local_ip, remote_ip, arch=None, subarch=None, mac=None, |
84 | hardware_uuid=None, bios_boot_method=None): |
85 | - """Get the booting configration for the a machine. |
86 | + """Get the booting configration for a machine. |
87 | |
88 | Returns a structure suitable for returning in the response |
89 | for :py:class:`~provisioningserver.rpc.region.GetBootConfig`. |
90 | @@ -408,6 +412,16 @@ def get_config( |
91 | |
92 | # Early out if the machine is booting local. |
93 | if purpose == 'local': |
94 | + if machine.is_device: |
95 | + # Log that we are setting to local boot for a device. |
96 | + maaslog.warning( |
97 | + "Device %s with MAC address %s is PXE booting; " |
98 | + "instructing the device to boot locally." % ( |
99 | + machine.hostname, mac)) |
100 | + # Set the purpose to 'local-device' so we can log a message |
101 | + # on the rack. |
102 | + purpose = 'local-device' |
103 | + |
104 | return { |
105 | "system_id": machine.system_id, |
106 | "arch": arch, |
107 | diff --git a/src/maasserver/rpc/tests/test_boot.py b/src/maasserver/rpc/tests/test_boot.py |
108 | index 1501348..a0bfd50 100644 |
109 | --- a/src/maasserver/rpc/tests/test_boot.py |
110 | +++ b/src/maasserver/rpc/tests/test_boot.py |
111 | @@ -16,6 +16,7 @@ from maasserver.enum import ( |
112 | INTERFACE_TYPE, |
113 | IPADDRESS_TYPE, |
114 | NODE_STATUS, |
115 | + NODE_TYPE, |
116 | ) |
117 | from maasserver.models import ( |
118 | Config, |
119 | @@ -261,6 +262,47 @@ class TestGetConfig(MAASServerTestCase): |
120 | "http_boot": True, |
121 | }, config) |
122 | |
123 | + def test__changes_purpose_to_local_device_for_device(self): |
124 | + rack_controller = factory.make_RackController() |
125 | + local_ip = factory.make_ip_address() |
126 | + remote_ip = factory.make_ip_address() |
127 | + device = self.make_node_with_extra( |
128 | + status=NODE_STATUS.DEPLOYED, netboot=False, |
129 | + node_type=NODE_TYPE.DEVICE) |
130 | + device.boot_cluster_ip = local_ip |
131 | + device.save() |
132 | + mac = device.get_boot_interface().mac_address |
133 | + self.patch_autospec(boot_module, 'event_log_pxe_request') |
134 | + maaslog = self.patch(boot_module, 'maaslog') |
135 | + config = get_config( |
136 | + rack_controller.system_id, local_ip, remote_ip, mac=mac, |
137 | + query_count=8) |
138 | + self.assertEquals({ |
139 | + "system_id": device.system_id, |
140 | + "arch": device.split_arch()[0], |
141 | + "subarch": device.split_arch()[1], |
142 | + "osystem": '', |
143 | + "release": '', |
144 | + "kernel": '', |
145 | + "initrd": '', |
146 | + "boot_dtb": '', |
147 | + "purpose": 'local-device', |
148 | + "hostname": device.hostname, |
149 | + "domain": device.domain.name, |
150 | + "preseed_url": ANY, |
151 | + "fs_host": local_ip, |
152 | + "log_host": local_ip, |
153 | + "log_port": 5247, |
154 | + "extra_opts": '', |
155 | + "http_boot": True, |
156 | + }, config) |
157 | + self.assertThat( |
158 | + maaslog.warning, |
159 | + MockCalledOnceWith( |
160 | + "Device %s with MAC address %s is PXE booting; " |
161 | + "instructing the device to boot locally." % ( |
162 | + device.hostname, mac))) |
163 | + |
164 | def test__purpose_local_to_xinstall_for_ephemeral_deployment(self): |
165 | # A diskless node is one that it is ephemerally deployed. |
166 | rack_controller = factory.make_RackController() |
167 | diff --git a/src/provisioningserver/rackdservices/tests/test_tftp.py b/src/provisioningserver/rackdservices/tests/test_tftp.py |
168 | index 7b58405..ebb7934 100644 |
169 | --- a/src/provisioningserver/rackdservices/tests/test_tftp.py |
170 | +++ b/src/provisioningserver/rackdservices/tests/test_tftp.py |
171 | @@ -1,4 +1,4 @@ |
172 | -# Copyright 2012-2016 Canonical Ltd. This software is licensed under the |
173 | +# Copyright 2012-2019 Canonical Ltd. This software is licensed under the |
174 | # GNU Affero General Public License version 3 (see the file LICENSE). |
175 | |
176 | """Tests for the maastftp Twisted plugin.""" |
177 | @@ -379,8 +379,6 @@ class TestTFTPBackend(MAASTestCase): |
178 | |
179 | @inlineCallbacks |
180 | def test_get_boot_method_reader_uses_same_client(self): |
181 | - # Fake configuration parameters, as discovered from the file path. |
182 | - fake_params = {"mac": factory.make_mac_address("-")} |
183 | # Fake kernel configuration parameters, as returned from the RPC call. |
184 | fake_kernel_params = make_kernel_parameters() |
185 | fake_params = fake_kernel_params._asdict() |
186 | @@ -450,8 +448,6 @@ class TestTFTPBackend(MAASTestCase): |
187 | |
188 | @inlineCallbacks |
189 | def test_get_boot_method_reader_uses_different_clients(self): |
190 | - # Fake configuration parameters, as discovered from the file path. |
191 | - fake_params = {"mac": factory.make_mac_address("-")} |
192 | # Fake kernel configuration parameters, as returned from the RPC call. |
193 | fake_kernel_params = make_kernel_parameters() |
194 | fake_params = fake_kernel_params._asdict() |
195 | @@ -524,8 +520,6 @@ class TestTFTPBackend(MAASTestCase): |
196 | |
197 | @inlineCallbacks |
198 | def test_get_boot_method_reader_grabs_new_client_on_lost_conn(self): |
199 | - # Fake configuration parameters, as discovered from the file path. |
200 | - fake_params = {"mac": factory.make_mac_address("-")} |
201 | # Fake kernel configuration parameters, as returned from the RPC call. |
202 | fake_kernel_params = make_kernel_parameters() |
203 | fake_params = fake_kernel_params._asdict() |
204 | @@ -602,8 +596,6 @@ class TestTFTPBackend(MAASTestCase): |
205 | |
206 | @inlineCallbacks |
207 | def test_get_boot_method_reader_returns_rendered_params(self): |
208 | - # Fake configuration parameters, as discovered from the file path. |
209 | - fake_params = {"mac": factory.make_mac_address("-")} |
210 | # Fake kernel configuration parameters, as returned from the RPC call. |
211 | fake_kernel_params = make_kernel_parameters() |
212 | fake_params = fake_kernel_params._asdict() |
213 | @@ -657,8 +649,6 @@ class TestTFTPBackend(MAASTestCase): |
214 | |
215 | @inlineCallbacks |
216 | def test_get_boot_method_reader_returns_rendered_params_for_local(self): |
217 | - # Fake configuration parameters, as discovered from the file path. |
218 | - fake_params = {"mac": factory.make_mac_address("-")} |
219 | # Fake kernel configuration parameters, as returned from the RPC call. |
220 | fake_kernel_params = make_kernel_parameters( |
221 | purpose="local", label="local") |
222 | @@ -699,9 +689,51 @@ class TestTFTPBackend(MAASTestCase): |
223 | backend, kernel_params=fake_kernel_params, **params_with_ip)) |
224 | |
225 | @inlineCallbacks |
226 | + def test_get_boot_method_reader_returns_rendered_params_local_device(self): |
227 | + # Fake kernel configuration parameters, as returned from the RPC call. |
228 | + fake_kernel_params = make_kernel_parameters( |
229 | + purpose="local", label="local") |
230 | + fake_params = fake_kernel_params._asdict() |
231 | + del fake_params["label"] |
232 | + |
233 | + # Set purpose to `local-device` as this is what will be passed on. |
234 | + fake_params["purpose"] = "local-device" |
235 | + |
236 | + # Stub RPC call to return the fake configuration parameters. |
237 | + client = Mock() |
238 | + client.localIdent = factory.make_name("system_id") |
239 | + client.return_value = succeed(fake_params) |
240 | + client_service = Mock() |
241 | + client_service.getClientNow.return_value = succeed(client) |
242 | + |
243 | + # get_boot_method_reader() takes a dict() of parameters and returns an |
244 | + # `IReader` of a PXE configuration, rendered by |
245 | + # `PXEBootMethod.get_reader`. |
246 | + backend = TFTPBackend( |
247 | + self.make_dir(), client_service) |
248 | + |
249 | + # Stub get_reader to return the render parameters. |
250 | + method = PXEBootMethod() |
251 | + fake_render_result = factory.make_name("render").encode("utf-8") |
252 | + render_patch = self.patch(method, "get_reader") |
253 | + render_patch.return_value = BytesReader(fake_render_result) |
254 | + |
255 | + # Get the rendered configuration, which will actually be a JSON dump |
256 | + # of the render-time parameters. |
257 | + params_with_ip = dict(fake_params) |
258 | + params_with_ip['remote_ip'] = factory.make_ipv4_address() |
259 | + reader = yield backend.get_boot_method_reader(method, params_with_ip) |
260 | + self.addCleanup(reader.finish) |
261 | + self.assertIsInstance(reader, BytesReader) |
262 | + output = reader.read(10000) |
263 | + |
264 | + # The result has been rendered by `method.get_reader`. |
265 | + self.assertEqual(fake_render_result, output) |
266 | + self.assertThat(method.get_reader, MockCalledOnceWith( |
267 | + backend, kernel_params=fake_kernel_params, **params_with_ip)) |
268 | + |
269 | + @inlineCallbacks |
270 | def test_get_boot_method_reader_returns_no_image(self): |
271 | - # Fake configuration parameters, as discovered from the file path. |
272 | - fake_params = {"mac": factory.make_mac_address("-")} |
273 | # Fake kernel configuration parameters, as returned from the RPC call. |
274 | fake_kernel_params = make_kernel_parameters(label='no-such-image') |
275 | fake_params = fake_kernel_params._asdict() |
276 | @@ -745,8 +777,6 @@ class TestTFTPBackend(MAASTestCase): |
277 | |
278 | @inlineCallbacks |
279 | def test_get_boot_method_reader_rendered_parms_for_depoyed_ephemeral(self): |
280 | - # Fake configuration parameters, as discovered from the file path. |
281 | - fake_params = {"mac": factory.make_mac_address("-")} |
282 | # Fake kernel configuration parameters, as returned from the RPC call. |
283 | fake_kernel_params = make_kernel_parameters( |
284 | purpose="local", label="local", osystem="caringo") |
285 | diff --git a/src/provisioningserver/rackdservices/tftp.py b/src/provisioningserver/rackdservices/tftp.py |
286 | index 0512b10..6a919fe 100644 |
287 | --- a/src/provisioningserver/rackdservices/tftp.py |
288 | +++ b/src/provisioningserver/rackdservices/tftp.py |
289 | @@ -1,4 +1,4 @@ |
290 | -# Copyright 2012-2016 Canonical Ltd. This software is licensed under the |
291 | +# Copyright 2012-2019 Canonical Ltd. This software is licensed under the |
292 | # GNU Affero General Public License version 3 (see the file LICENSE). |
293 | |
294 | """Twisted Application Plugin for the MAAS TFTP server.""" |
295 | @@ -36,6 +36,7 @@ from provisioningserver.rpc.region import ( |
296 | MarkNodeFailed, |
297 | ) |
298 | from provisioningserver.utils import ( |
299 | + network, |
300 | tftp, |
301 | typed, |
302 | ) |
303 | @@ -236,10 +237,20 @@ class TFTPBackend(FilesystemSynchronousBackend): |
304 | except Exception: |
305 | pass |
306 | |
307 | + # Check to see if the we are PXE booting a device. |
308 | + if params["purpose"] == "local-device": |
309 | + mac = network.find_mac_via_arp(remote_ip) |
310 | + log.info( |
311 | + "Device %s with MAC address %s is PXE booting; " |
312 | + "instructing the device to boot locally." % ( |
313 | + params["hostname"], mac)) |
314 | + # Set purpose back to local now that we have the message logged. |
315 | + params["purpose"] = "local" |
316 | + |
317 | system_id = params.pop("system_id", None) |
318 | if params["purpose"] == "local" and not is_ephemeral: |
319 | - # Local purpose doesn't use a boot image so jsut set the label |
320 | - # to "local", but this value will no be used. |
321 | + # Local purpose doesn't use a boot image so just set the label |
322 | + # to "local". |
323 | params["label"] = "local" |
324 | return params |
325 | else: |
Code path looks good, but need to fix log message.