Merge ~newell-jensen/maas:lp1820984 into maas:master

Proposed by Newell Jensen
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)
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.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Code path looks good, but need to fix log message.

review: Needs Fixing
Revision history for this message
Andres Rodriguez (andreserl) :
review: Needs Fixing
Revision history for this message
Andres Rodriguez (andreserl) wrote :

A quick q, for the devices we are managing or tracking in MAAS, do we /always/ create dhcp hostmaps ?

Revision history for this message
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.

Revision history for this message
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).

review: Needs Fixing
Revision history for this message
Andres Rodriguez (andreserl) wrote :

another comment inline.

Revision history for this message
Andres Rodriguez (andreserl) wrote :

see inline

review: Needs Fixing
Revision history for this message
Newell Jensen (newell-jensen) :
Revision history for this message
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:

https://paste.ubuntu.com/p/zcqDPm9yY9/

Revision history for this message
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?

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

Having the log in both places is acceptable.

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

Looks good. Thanks for all the fixes and working through this.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/models/node.py b/src/maasserver/models/node.py
2index 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.
34diff --git a/src/maasserver/models/tests/test_node.py b/src/maasserver/models/tests/test_node.py
35index 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(
59diff --git a/src/maasserver/rpc/boot.py b/src/maasserver/rpc/boot.py
60index 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,
107diff --git a/src/maasserver/rpc/tests/test_boot.py b/src/maasserver/rpc/tests/test_boot.py
108index 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()
167diff --git a/src/provisioningserver/rackdservices/tests/test_tftp.py b/src/provisioningserver/rackdservices/tests/test_tftp.py
168index 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")
285diff --git a/src/provisioningserver/rackdservices/tftp.py b/src/provisioningserver/rackdservices/tftp.py
286index 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:

Subscribers

People subscribed via source and target branches