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

Proposed by Newell Jensen
Status: Merged
Approved by: Newell Jensen
Approved revision: b218c144a610e7ca707c57f3a6f93e2e2969a43a
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~newell-jensen/maas:lp1713556
Merge into: maas:master
Diff against target: 297 lines (+164/-4)
4 files modified
src/maasserver/api/pods.py (+5/-0)
src/maasserver/api/tests/test_pods.py (+107/-1)
src/maasserver/forms/pods.py (+44/-1)
src/provisioningserver/drivers/pod/virsh.py (+8/-2)
Reviewer Review Type Date Requested Status
MAAS Lander Needs Fixing
Mike Pontillo (community) Approve
Andres Rodriguez (community) Approve
Review via email: mp+343053@code.launchpad.net

Commit message

LP: #1713556 -- Add ability to set console_logging boolean for a pod in the API for whether or not console logging should be enabled.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b lp1713556 lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: f97362ced1eb2952bd10f3eff7a9fa3a8d6a5412

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

questions inline.

review: Needs Information
Revision history for this message
Newell Jensen (newell-jensen) wrote :

responses inline.

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

UNIT TESTS
-b lp1713556 lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 9a9e717e2e2e5b8020de2821c31879af0dcaa0e2

review: Approve
Revision history for this message
Andres Rodriguez (andreserl) :
review: Needs Information
Revision history for this message
Newell Jensen (newell-jensen) wrote :

replies inline.

Revision history for this message
Newell Jensen (newell-jensen) :
Revision history for this message
Andres Rodriguez (andreserl) wrote :

comment inline.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

I have some comments/questions inline below.

Also, I feel that (assuming the same strategy is used) unit testing for this change needs to be expanded. Ideally, it should explicitly cover the following scenarios:

(1) Updating the pod while specifying console_logging=True - ensure that the tag gets set
(2) Updating the pod while specifying console_log=False - ensure that the tag is unset
(3) Updating the pod while not specifying console_logging at all - ensure that the tag remains set if already set
(4) Updating the pod while not specifying console_logging at all - ensure that the tag remains unset unset if already unset

So my main concern with this branch is the fact that I don't see comprehensive testing of the above scenarios. That's the main thing that makes this a "Needs Fixing" (but see my inline comments below, too).

If you implement this as a separate operation rather than changing the exiting update mechanism, you wouldn't have to worry about (3) and (4).

On the other hand, I do like how updating this value is "seamless" and doesn't require looking around for a separate operation. I think that's a better user experience that way. But what would make it even better is if this logic was instead implemented in the form. If you do it that way, it will be trivial to update the UI to allow setting this parameter as well.

Lastly, there's a better way to get a boolean value from the request when using the API; I would do it more like this:

        console_logging = get_optional_param(
            request.POST, 'console_logging', default=False, validator=StringBool)

By doing it this way, you would handle more variations than just the case-insensitive "true" and "false" strings.

review: Needs Fixing
Revision history for this message
Newell Jensen (newell-jensen) wrote :

My original branch had it in the form with a data migration. After talking with you guys on IRC about the best route forward, will move the pod console logging tag creation to the form so that it can potentially be used in the UI.

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Not sure if this is actually ready for another review yet, but I caught a couple things below.

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

UNIT TESTS
-b lp1713556 lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 2cba8df0c7c08436f6d6b04d2e0b04ab9eb8cc15

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

lgtm! Please seek Mike's approval before landing.

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Can you add a test that ensures that the following sequence of steps works as expected:

(1) Update a pod with console_logging=True
(2) Verify that the tag exists
(3) Update something unrelated on the pod, such as the name
(4) Verify that the tag still exists

Revision history for this message
Mike Pontillo (mpontillo) wrote :

Found a few more issues; see inline below.

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

UNIT TESTS
-b lp1713556 lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 2aae15b79c2b7c41d48a979aed47c6da30e218af

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

See inline.

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

UNIT TESTS
-b lp1713556 lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 8d6df467b2905130f0aad8af6317f3a7adc9689e

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

+1, thanks for the fixes!

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

UNIT TESTS
-b lp1713556 lp:~newell-jensen/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/4174/console
COMMIT: a8adb394a9d535eb92f1e2a040de4e92c6fc58e6

review: Needs Fixing
Revision history for this message
Mike Pontillo (mpontillo) wrote :

FYI, I tested this branch last week when I was gathering information for another bug report. It works great!

~newell-jensen/maas:lp1713556 updated
b218c14... by Newell Jensen

Fix lint.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/api/pods.py b/src/maasserver/api/pods.py
2index 0c6b28c..42036a6 100644
3--- a/src/maasserver/api/pods.py
4+++ b/src/maasserver/api/pods.py
5@@ -153,6 +153,11 @@ class PodHandler(OperationsHandler):
6 :type default_macvlan_mode: unicode
7 :param tags: A tag or tags (separated by comma) for the pod.
8 :type tags: unicode
9+ :param console_log: If True, created VMs for this pod will have
10+ their console output logged. To do this, a tag with the name
11+ 'pod-console-logging' is created. If False, it checks to see if
12+ this tag already exists and deletes it if it does.
13+ :type console_log: boolean
14
15 Note: 'type' cannot be updated on a Pod. The Pod must be deleted and
16 re-added to change the type.
17diff --git a/src/maasserver/api/tests/test_pods.py b/src/maasserver/api/tests/test_pods.py
18index 5cc1da7..4600e3f 100644
19--- a/src/maasserver/api/tests/test_pods.py
20+++ b/src/maasserver/api/tests/test_pods.py
21@@ -13,6 +13,7 @@ from maasserver.enum import NODE_CREATION_TYPE
22 from maasserver.forms import pods
23 from maasserver.models.bmc import Pod
24 from maasserver.models.node import Machine
25+from maasserver.models.tag import Tag
26 from maasserver.testing.api import APITestCase
27 from maasserver.testing.factory import factory
28 from maasserver.utils.converters import json_load_bytes
29@@ -276,7 +277,11 @@ class TestPodAPI(APITestCase.ForUser, PodMixin):
30 self.become_admin()
31 pod = factory.make_Pod(pod_type='virsh')
32 new_name = factory.make_name('pod')
33- new_tags = [factory.make_name('tag'), factory.make_name('tag')]
34+ new_tags = [
35+ factory.make_name('tag'),
36+ factory.make_name('tag'),
37+ 'pod-console-logging',
38+ ]
39 new_pool = factory.make_ResourcePool()
40 new_zone = factory.make_Zone()
41 new_power_parameters = {
42@@ -291,10 +296,12 @@ class TestPodAPI(APITestCase.ForUser, PodMixin):
43 'power_pass': new_power_parameters['power_pass'],
44 'zone': new_zone.name,
45 'pool': new_pool.name,
46+ 'console_logging': 'True',
47 })
48 self.assertEqual(
49 http.client.OK, response.status_code, response.content)
50 pod.refresh_from_db()
51+ self.assertIsNotNone(Tag.objects.get(name="pod-console-logging"))
52 self.assertEqual(new_name, pod.name)
53 self.assertEqual(new_pool, pod.pool)
54 self.assertItemsEqual(new_tags, pod.tags)
55@@ -313,10 +320,13 @@ class TestPodAPI(APITestCase.ForUser, PodMixin):
56 'power_address': pod_info['power_address'],
57 'power_pass': pod_info['power_pass'],
58 'zone': pod_info['zone'],
59+ 'console_logging': 'False',
60 })
61 self.assertEqual(
62 http.client.OK, response.status_code, response.content)
63 parsed_output = json_load_bytes(response.content)
64+ self.assertRaises(
65+ Tag.DoesNotExist, Tag.objects.get, name="pod-console-logging")
66 self.assertEqual(new_name, parsed_output['name'])
67 self.assertEqual(discovered_pod.cores, parsed_output['total']['cores'])
68
69@@ -337,6 +347,8 @@ class TestPodAPI(APITestCase.ForUser, PodMixin):
70 self.assertEqual(
71 http.client.OK, response.status_code, response.content)
72 pod.refresh_from_db()
73+ self.assertRaises(
74+ Tag.DoesNotExist, Tag.objects.get, name="pod-console-logging")
75 self.assertEqual(new_name, pod.name)
76 self.assertEqual(power_parameters, pod.power_parameters)
77
78@@ -354,6 +366,100 @@ class TestPodAPI(APITestCase.ForUser, PodMixin):
79 pod.refresh_from_db()
80 self.assertEqual(pod.default_macvlan_mode, default_macvlan_mode)
81
82+ def test_PUT_update_deletes_pod_console_logging_tag_if_not_in_use(self):
83+ self.become_admin()
84+ factory.make_Tag(name='pod-console-logging')
85+ pod1_info = self.make_pod_info()
86+ factory.make_Pod(pod_type=pod1_info['type'])
87+ pod2_info = self.make_pod_info()
88+ pod2 = factory.make_Pod(
89+ pod_type=pod2_info['type'],
90+ tags=['pod-console-logging'])
91+ self.fake_pod_discovery()
92+ response = self.client.put(get_pod_uri(pod2), {
93+ 'console_logging': 'False',
94+ })
95+ self.assertEqual(
96+ http.client.OK, response.status_code, response.content)
97+ self.assertRaises(
98+ Tag.DoesNotExist, Tag.objects.get, name='pod-console-logging')
99+
100+ def test_PUT_update_wont_delete_pod_console_logging_tag_if_in_use(self):
101+ self.become_admin()
102+ factory.make_Tag(name='pod-console-logging')
103+ pod1_info = self.make_pod_info()
104+ factory.make_Pod(
105+ pod_type=pod1_info['type'],
106+ tags=['pod-console-logging'])
107+ pod2_info = self.make_pod_info()
108+ pod2 = factory.make_Pod(
109+ pod_type=pod2_info['type'],
110+ tags=['pod-console-logging'])
111+ self.fake_pod_discovery()
112+ response = self.client.put(get_pod_uri(pod2), {
113+ 'console_logging': 'False',
114+ })
115+ self.assertEqual(
116+ http.client.OK, response.status_code, response.content)
117+ self.assertIsNotNone(Tag.objects.get(name='pod-console-logging'))
118+
119+ def test_PUT_update_creates_pod_console_logging_tag(self):
120+ self.become_admin()
121+ self.assertRaises(
122+ Tag.DoesNotExist, Tag.objects.get, name='pod-console-logging')
123+ pod_info = self.make_pod_info()
124+ pod = factory.make_Pod(pod_type=pod_info['type'])
125+ self.fake_pod_discovery()
126+ response = self.client.put(get_pod_uri(pod), {
127+ 'console_logging': 'True',
128+ })
129+ self.assertEqual(
130+ http.client.OK, response.status_code, response.content)
131+ self.assertIsNotNone(Tag.objects.get(name='pod-console-logging'))
132+ response = self.client.put(get_pod_uri(pod), {
133+ 'console_logging': 'True',
134+ })
135+ self.assertEqual(
136+ http.client.OK, response.status_code, response.content)
137+ self.assertIsNotNone(Tag.objects.get(name='pod-console-logging'))
138+ response = self.client.put(get_pod_uri(pod), {
139+ 'name': factory.make_name('name'),
140+ })
141+ self.assertEqual(
142+ http.client.OK, response.status_code, response.content)
143+ pod.refresh_from_db()
144+ self.assertIn('pod-console-logging', pod.tags)
145+ self.assertIsNotNone(Tag.objects.get(name='pod-console-logging'))
146+
147+ def test_PUT_update_deletes_pod_console_logging_tag(self):
148+ self.become_admin()
149+ factory.make_Tag(name='pod-console-logging')
150+ pod_info = self.make_pod_info()
151+ pod = factory.make_Pod(
152+ pod_type=pod_info['type'], tags=['pod-console-logging'])
153+ self.fake_pod_discovery()
154+ response = self.client.put(get_pod_uri(pod), {
155+ 'console_logging': 'False',
156+ })
157+ self.assertEqual(
158+ http.client.OK, response.status_code, response.content)
159+ self.assertRaises(
160+ Tag.DoesNotExist, Tag.objects.get, name='pod-console-logging')
161+ response = self.client.put(get_pod_uri(pod), {
162+ 'console_logging': 'False',
163+ })
164+ self.assertEqual(
165+ http.client.OK, response.status_code, response.content)
166+ response = self.client.put(get_pod_uri(pod), {
167+ 'name': factory.make_name('name'),
168+ })
169+ self.assertEqual(
170+ http.client.OK, response.status_code, response.content)
171+ pod.refresh_from_db()
172+ self.assertNotIn('pod-console-logging', pod.tags)
173+ self.assertRaises(
174+ Tag.DoesNotExist, Tag.objects.get, name='pod-console-logging')
175+
176 def test_refresh_requires_admin(self):
177 pod = factory.make_Pod()
178 response = self.client.post(get_pod_uri(pod), {
179diff --git a/src/maasserver/forms/pods.py b/src/maasserver/forms/pods.py
180index 77f9f3e..4a43f01 100644
181--- a/src/maasserver/forms/pods.py
182+++ b/src/maasserver/forms/pods.py
183@@ -46,6 +46,7 @@ from maasserver.models import (
184 PodStoragePool,
185 RackController,
186 ResourcePool,
187+ Tag,
188 Zone,
189 )
190 from maasserver.node_constraint_filter_forms import (
191@@ -148,6 +149,23 @@ class PodForm(MAASModelForm):
192 if data is None:
193 data = {}
194 type_value = data.get('type', self.initial.get('type'))
195+ console_log = data.get('console_logging', '').lower()
196+ if console_log in ['true', 'false']:
197+ console_log = True if console_log == 'true' else False
198+ if self.is_new:
199+ self.update_console_log = True
200+ elif 'pod-console-logging' in instance.tags and console_log:
201+ self.update_console_log = False
202+ elif 'pod-console-logging' in instance.tags and not console_log:
203+ self.update_console_log = True
204+ else:
205+ self.update_console_log = True
206+ else:
207+ self.update_console_log = False
208+
209+ if self.update_console_log:
210+ self.console_log = console_log
211+
212 self.drivers_orig = driver_parameters.get_all_power_types()
213 self.drivers = {
214 driver['name']: driver
215@@ -243,6 +261,31 @@ class PodForm(MAASModelForm):
216 self.instance = super(PodForm, self).save(commit=False)
217 self.instance.power_type = power_type
218 self.instance.power_parameters = power_parameters
219+ # If console_log is set, create a tag for the kernel parameters
220+ # if it does not already exist. Delete otherwise.
221+ if self.update_console_log:
222+ if self.console_log:
223+ tag, _ = Tag.objects.get_or_create(
224+ name="pod-console-logging",
225+ kernel_opts="console=tty1 console=ttyS0")
226+ # Add this tag to the pod.
227+ self.instance.add_tag(tag.name)
228+ else:
229+ try:
230+ tag = Tag.objects.get(name="pod-console-logging")
231+ # Remove this tag from the pod.
232+ self.instance.remove_tag(tag.name)
233+ # Delete the tag if there are no longer any other
234+ # pods using it.
235+ pods = Pod.objects.filter(
236+ tags__contains=['pod-console-logging']).exclude(
237+ id=self.instance.id)
238+ if not pods:
239+ tag.delete()
240+ except Tag.DoesNotExist:
241+ # There was no tag so just continue.
242+ pass
243+
244 return self.instance
245
246 power_type = self.cleaned_data['type']
247@@ -258,7 +301,7 @@ class PodForm(MAASModelForm):
248 d = deferToDatabase(
249 transactional(check_for_duplicate),
250 power_type, power_parameters)
251- d.addCallback(update_obj)
252+ d.addCallback(partial(deferToDatabase, transactional(update_obj)))
253 d.addCallback(lambda _: self.discover_and_sync_pod())
254 return d
255 else:
256diff --git a/src/provisioningserver/drivers/pod/virsh.py b/src/provisioningserver/drivers/pod/virsh.py
257index 2a84bbb..69629d7 100644
258--- a/src/provisioningserver/drivers/pod/virsh.py
259+++ b/src/provisioningserver/drivers/pod/virsh.py
260@@ -118,6 +118,7 @@ DOM_TEMPLATE_AMD64 = dedent("""\
261 bus='0x00' slot='0x05' function='0x0'/>
262 </controller>
263 <serial type='pty'>
264+ <log file="/var/log/libvirt/qemu/{name}-serial0.log" append="off" />
265 <target port='0'/>
266 </serial>
267 <console type='pty'>
268@@ -162,6 +163,7 @@ DOM_TEMPLATE_ARM64 = dedent("""\
269 <emulator>{emulator}</emulator>
270 <controller type='pci' index='0' model='pcie-root'/>
271 <serial type='pty'>
272+ <log file="/var/log/libvirt/qemu/{name}-serial0.log" append="off" />
273 <target port='0'/>
274 </serial>
275 <console type='pty'>
276@@ -192,6 +194,7 @@ DOM_TEMPLATE_PPC64 = dedent("""\
277 <emulator>{emulator}</emulator>
278 <controller type='pci' index='0' model='pci-root'/>
279 <serial type='pty'>
280+ <log file="/var/log/libvirt/qemu/{name}-serial0.log" append="off" />
281 <target port='0'/>
282 </serial>
283 <console type='pty'>
284@@ -218,8 +221,11 @@ DOM_TEMPLATE_S390X = dedent("""
285 <pae/>
286 </features>
287 <devices>
288- <console type='pty' tty='/dev/pts/3'>
289- <source path='/dev/pts/3'/>
290+ <serial type='pty'>
291+ <log file="/var/log/libvirt/qemu/{name}-serial0.log" append="off" />
292+ <target port='0'/>
293+ </serial>
294+ <console type='pty'>
295 <target type='sclp' port='0'/>
296 <alias name='console0'/>
297 </console>

Subscribers

People subscribed via source and target branches