Merge lp:~ltrager/maas/round_ram into lp:~maas-committers/maas/trunk

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: no longer in the source branch.
Merged at revision: 5046
Proposed branch: lp:~ltrager/maas/round_ram
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 315 lines (+101/-26)
6 files modified
src/maasserver/models/node.py (+8/-1)
src/maasserver/models/signals/nodes.py (+2/-0)
src/maasserver/models/tests/test_node.py (+2/-2)
src/maasserver/node_action.py (+25/-15)
src/maasserver/tests/test_node_action.py (+57/-4)
src/maasserver/websockets/handlers/tests/test_general.py (+7/-4)
To merge this branch: bzr merge lp:~ltrager/maas/round_ram
Reviewer Review Type Date Requested Status
Jeffrey C Jones (community) Approve
MAAS Maintainers Pending
Review via email: mp+295516@code.launchpad.net

Commit message

Allow refreshing and import images on rack controllers via the UI and show memory to the first decimal place

Description of the change

As discussed during the sprint round show RAM size to the first decimal place, and allow rack controllers to be refreshed and import images via the node action list.

The node actions themselves are only being seen in the node-list page, not the node-details page.

To post a comment you must log in.
Revision history for this message
Jeffrey C Jones (trapnine) wrote :

Cool - please revert the import images bits from your branch, as I've merged and tested them in my import images branch.

I've also fixed the issue you mention where actions weren't appearing properly on the controller details page.

See https://code.launchpad.net/~trapnine/maas/fix-1583685-force-import/+merge/295434

review: Needs Fixing
Revision history for this message
Lee Trager (ltrager) wrote :

I've removed the import images node action so you can drive it in your MP. There is still some duplicated code as its needed for the refresh command. Hopefully bzr will merge it for us, otherwise we'll just have to deal with a small merge conflict.

Revision history for this message
Jeffrey C Jones (trapnine) wrote :

Looks good. The merge shouldn't be too bad.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.1 MiB)

The attempt to merge lp:~ltrager/maas/round_ram into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [94.5 kB]
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:4 http://security.ubuntu.com/ubuntu xenial-security InRelease [94.5 kB]
Fetched 189 kB in 0s (412 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-all python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu3).
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
bzr is already the newest version (2.7.0-2ubuntu1).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu12).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
pxelinux is already the newest version (3:6...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/models/node.py'
2--- src/maasserver/models/node.py 2016-05-16 08:36:33 +0000
3+++ src/maasserver/models/node.py 2016-05-25 21:25:22 +0000
4@@ -1302,7 +1302,14 @@
5 """Return memory in GiB."""
6 if self.memory < 1024:
7 return '%.1f' % (self.memory / 1024.0)
8- return '%d' % (self.memory / 1024)
9+ # Commissioning gets all available memory to the system. However some
10+ # memory can be reserved by the motherboard(e.g for video memory) or
11+ # the kernel itself. Commissioning can't detect reserved RAM so show
12+ # the RAM in GiB to the first decimal place. Python rounds the float
13+ # which results in the correct value. For example a KVM virt is
14+ # configured with 2048 MiB of RAM but only 2047MiB is detectable.
15+ # 2047 / 1024 = 1.9990 which rounds to 2.0.
16+ return '%.1f' % (self.memory / 1024.0)
17
18 @property
19 def physicalblockdevice_set(self):
20
21=== modified file 'src/maasserver/models/signals/nodes.py'
22--- src/maasserver/models/signals/nodes.py 2016-03-28 13:54:47 +0000
23+++ src/maasserver/models/signals/nodes.py 2016-05-25 21:25:22 +0000
24@@ -9,6 +9,7 @@
25
26 from django.db.models.signals import post_save
27 from maasserver.models import (
28+ Controller,
29 Device,
30 Machine,
31 Node,
32@@ -24,6 +25,7 @@
33 Node,
34 Machine,
35 Device,
36+ Controller,
37 RackController,
38 RegionController,
39 ]
40
41=== modified file 'src/maasserver/models/tests/test_node.py'
42--- src/maasserver/models/tests/test_node.py 2016-05-16 08:36:33 +0000
43+++ src/maasserver/models/tests/test_node.py 2016-05-25 21:25:22 +0000
44@@ -578,8 +578,8 @@
45 self.assertEqual('0.5', node.display_memory())
46
47 def test_display_memory_returns_value_divided_by_1024(self):
48- node = factory.make_Node(memory=2048)
49- self.assertEqual('2', node.display_memory())
50+ node = factory.make_Node(memory=2560)
51+ self.assertEqual('2.5', node.display_memory())
52
53 def test_physicalblockdevice_set_returns_physicalblockdevices(self):
54 node = factory.make_Node(with_boot_disk=False)
55
56=== modified file 'src/maasserver/node_action.py'
57--- src/maasserver/node_action.py 2016-05-12 19:07:37 +0000
58+++ src/maasserver/node_action.py 2016-05-25 21:25:22 +0000
59@@ -1,4 +1,4 @@
60-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
61+# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
62 # GNU Affero General Public License version 3 (see the file LICENSE).
63
64 """Node actions.
65@@ -90,12 +90,10 @@
66 A list of NODE_TYPEs which are applicable for this action.
67 """)
68
69- actionable_statuses = abstractproperty("""
70- Node states for which this action makes sense.
71-
72- A collection of NODE_STATUS values. The action will be available
73- only if `node.status in action.actionable_statuses`.
74- """)
75+ # Optional node states for which this action makes sense.
76+ # A collection of NODE_STATUS values. The action will be available
77+ # only if `node.status in action.actionable_statuses`.
78+ actionable_statuses = None
79
80 permission = abstractproperty("""
81 Required permission.
82@@ -211,10 +209,6 @@
83 zone = Zone.objects.get(id=zone_id)
84 self.node.set_zone(zone)
85
86- def is_actionable(self):
87- """Returns true if the selected nodes can be added to a zone"""
88- return super(SetZone, self).is_actionable()
89-
90
91 class Commission(NodeAction):
92 """Accept a node into the MAAS, and start the commissioning process."""
93@@ -350,10 +344,6 @@
94 except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
95 raise NodeActionError(exception)
96
97- def is_actionable(self):
98- is_actionable = super(PowerOn, self).is_actionable()
99- return is_actionable
100-
101
102 FAILED_STATUSES = [
103 status for status in map_enum(NODE_STATUS).values()
104@@ -470,6 +460,25 @@
105 return True
106
107
108+class Refresh(NodeAction):
109+ """Refresh a rack or region and rack controller."""
110+ name = "refresh"
111+ display = "Refresh"
112+ display_sentence = "refreshed"
113+ permission = NODE_PERMISSION.ADMIN
114+ for_type = {
115+ NODE_TYPE.RACK_CONTROLLER,
116+ NODE_TYPE.REGION_AND_RACK_CONTROLLER
117+ }
118+
119+ def execute(self):
120+ """See `NodeAction.execute`."""
121+ try:
122+ self.node.refresh()
123+ except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
124+ raise NodeActionError(exception)
125+
126+
127 ACTION_CLASSES = (
128 Commission,
129 Acquire,
130@@ -482,6 +491,7 @@
131 MarkFixed,
132 SetZone,
133 Delete,
134+ Refresh,
135 )
136
137 ACTIONS_DICT = OrderedDict((action.name, action) for action in ACTION_CLASSES)
138
139=== modified file 'src/maasserver/tests/test_node_action.py'
140--- src/maasserver/tests/test_node_action.py 2016-05-16 09:21:53 +0000
141+++ src/maasserver/tests/test_node_action.py 2016-05-25 21:25:22 +0000
142@@ -5,6 +5,7 @@
143
144 __all__ = []
145
146+import random
147 from unittest.mock import ANY
148
149 from django.db import transaction
150@@ -18,6 +19,7 @@
151 NODE_STATUS_CHOICES,
152 NODE_STATUS_CHOICES_DICT,
153 NODE_TYPE,
154+ NODE_TYPE_CHOICES,
155 POWER_STATE,
156 )
157 from maasserver.exceptions import NodeActionError
158@@ -25,6 +27,10 @@
159 signals,
160 StaticIPAddress,
161 )
162+from maasserver.models.node import (
163+ RackController,
164+ typecast_to_node_type,
165+)
166 from maasserver.models.signals.testing import SignalsDisabled
167 from maasserver.node_action import (
168 Abort,
169@@ -38,6 +44,7 @@
170 NodeAction,
171 PowerOff,
172 PowerOn,
173+ Refresh,
174 Release,
175 RPC_EXCEPTIONS,
176 SetZone,
177@@ -60,7 +67,10 @@
178 post_commit_hooks,
179 reload_object,
180 )
181-from maastesting.matchers import MockCalledOnceWith
182+from maastesting.matchers import (
183+ MockCalledOnce,
184+ MockCalledOnceWith,
185+)
186 from metadataserver.enum import RESULT_TYPE
187 from metadataserver.models.noderesult import NodeResult
188 from netaddr import IPNetwork
189@@ -815,6 +825,33 @@
190 self.assertItemsEqual([], actions)
191
192
193+class TestRefresh(MAASServerTestCase):
194+
195+ def test_refresh(self):
196+ user = factory.make_admin()
197+ rack = factory.make_RackController()
198+ mock_refresh = self.patch(rack, 'refresh')
199+
200+ with post_commit_hooks:
201+ Refresh(rack, user).execute()
202+
203+ self.assertThat(mock_refresh, MockCalledOnce())
204+
205+ def test_requires_admin_permission(self):
206+ user = factory.make_User()
207+ rack = factory.make_RackController()
208+ self.assertFalse(Refresh(rack, user).is_permitted())
209+
210+ def test_requires_rack(self):
211+ user = factory.make_User()
212+ node = factory.make_Node(
213+ node_type=factory.pick_choice(
214+ NODE_TYPE_CHOICES, but_not=[
215+ NODE_TYPE.RACK_CONTROLLER,
216+ NODE_TYPE.REGION_AND_RACK_CONTROLLER]))
217+ self.assertFalse(Refresh(node, user).is_actionable())
218+
219+
220 class TestActionsErrorHandling(MAASServerTestCase):
221 """Tests for error handling in actions.
222
223@@ -838,13 +875,17 @@
224 exception = self.make_exception()
225 self.patch(node, '_start').side_effect = exception
226 self.patch(node, '_stop').side_effect = exception
227+ if isinstance(node, RackController):
228+ self.patch(node, 'refresh').side_effect = exception
229
230- def make_action(self, action_class, node_status, power_state=None):
231+ def make_action(
232+ self, action_class, node_status, power_state=None,
233+ node_type=NODE_TYPE.MACHINE):
234 node = factory.make_Node(
235 interface=True, status=node_status, power_type='manual',
236- power_state=power_state)
237+ power_state=power_state, node_type=node_type)
238 admin = factory.make_admin()
239- return action_class(node, admin)
240+ return action_class(typecast_to_node_type(node), admin)
241
242 def test_Commission_handles_rpc_errors(self):
243 self.addCleanup(signals.power.signals.enable)
244@@ -891,3 +932,15 @@
245 self.assertEqual(
246 get_error_message_for_exception(action.node._stop.side_effect),
247 str(exception))
248+
249+ def test_Refresh_handles_rpc_errors(self):
250+ action = self.make_action(
251+ Refresh, NODE_STATUS.ALLOCATED, power_state=POWER_STATE.ON,
252+ node_type=random.choice([
253+ NODE_TYPE.RACK_CONTROLLER,
254+ NODE_TYPE.REGION_AND_RACK_CONTROLLER]))
255+ self.patch_rpc_methods(action.node)
256+ exception = self.assertRaises(NodeActionError, action.execute)
257+ self.assertEqual(
258+ get_error_message_for_exception(action.node._stop.side_effect),
259+ str(exception))
260
261=== modified file 'src/maasserver/websockets/handlers/tests/test_general.py'
262--- src/maasserver/websockets/handlers/tests/test_general.py 2016-05-12 19:07:37 +0000
263+++ src/maasserver/websockets/handlers/tests/test_general.py 2016-05-25 21:25:22 +0000
264@@ -13,6 +13,7 @@
265 BOND_MODE_CHOICES,
266 BOND_XMIT_HASH_POLICY_CHOICES,
267 BOOT_RESOURCE_TYPE,
268+ NODE_TYPE,
269 )
270 from maasserver.models import BootSourceCache
271 from maasserver.models.config import Config
272@@ -28,7 +29,7 @@
273
274 class TestGeneralHandler(MAASServerTestCase):
275
276- def dehydrate_actions(self, actions):
277+ def dehydrate_actions(self, actions, node_type=None):
278 return [
279 {
280 "name": name,
281@@ -36,6 +37,7 @@
282 "sentence": action.display_sentence,
283 }
284 for name, action in actions.items()
285+ if node_type is None or node_type in action.for_type
286 ]
287
288 def test_architectures(self):
289@@ -107,7 +109,8 @@
290
291 def test_machine_actions_for_admin(self):
292 handler = GeneralHandler(factory.make_admin(), {})
293- actions_expected = self.dehydrate_actions(ACTIONS_DICT)
294+ actions_expected = self.dehydrate_actions(
295+ ACTIONS_DICT, NODE_TYPE.MACHINE)
296 self.assertItemsEqual(actions_expected, handler.machine_actions({}))
297
298 def test_machine_actions_for_non_admin(self):
299@@ -141,7 +144,7 @@
300 def test_rack_controller_actions_for_admin(self):
301 handler = GeneralHandler(factory.make_admin(), {})
302 self.assertItemsEqual(
303- ['delete', 'off', 'on', 'set-zone'],
304+ ['delete', 'off', 'on', 'refresh', 'set-zone'],
305 [action['name'] for action in handler.rack_controller_actions({})])
306
307 def test_rack_controller_actions_for_non_admin(self):
308@@ -151,7 +154,7 @@
309 def test_region_and_rack_controller_actions_for_admin(self):
310 handler = GeneralHandler(factory.make_admin(), {})
311 self.assertItemsEqual(
312- ['set-zone', 'delete'],
313+ ['set-zone', 'refresh', 'delete'],
314 [action['name']
315 for action in handler.region_and_rack_controller_actions({})])
316