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
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2016-05-16 08:36:33 +0000
+++ src/maasserver/models/node.py 2016-05-25 21:25:22 +0000
@@ -1302,7 +1302,14 @@
1302 """Return memory in GiB."""1302 """Return memory in GiB."""
1303 if self.memory < 1024:1303 if self.memory < 1024:
1304 return '%.1f' % (self.memory / 1024.0)1304 return '%.1f' % (self.memory / 1024.0)
1305 return '%d' % (self.memory / 1024)1305 # Commissioning gets all available memory to the system. However some
1306 # memory can be reserved by the motherboard(e.g for video memory) or
1307 # the kernel itself. Commissioning can't detect reserved RAM so show
1308 # the RAM in GiB to the first decimal place. Python rounds the float
1309 # which results in the correct value. For example a KVM virt is
1310 # configured with 2048 MiB of RAM but only 2047MiB is detectable.
1311 # 2047 / 1024 = 1.9990 which rounds to 2.0.
1312 return '%.1f' % (self.memory / 1024.0)
13061313
1307 @property1314 @property
1308 def physicalblockdevice_set(self):1315 def physicalblockdevice_set(self):
13091316
=== modified file 'src/maasserver/models/signals/nodes.py'
--- src/maasserver/models/signals/nodes.py 2016-03-28 13:54:47 +0000
+++ src/maasserver/models/signals/nodes.py 2016-05-25 21:25:22 +0000
@@ -9,6 +9,7 @@
99
10from django.db.models.signals import post_save10from django.db.models.signals import post_save
11from maasserver.models import (11from maasserver.models import (
12 Controller,
12 Device,13 Device,
13 Machine,14 Machine,
14 Node,15 Node,
@@ -24,6 +25,7 @@
24 Node,25 Node,
25 Machine,26 Machine,
26 Device,27 Device,
28 Controller,
27 RackController,29 RackController,
28 RegionController,30 RegionController,
29]31]
3032
=== modified file 'src/maasserver/models/tests/test_node.py'
--- src/maasserver/models/tests/test_node.py 2016-05-16 08:36:33 +0000
+++ src/maasserver/models/tests/test_node.py 2016-05-25 21:25:22 +0000
@@ -578,8 +578,8 @@
578 self.assertEqual('0.5', node.display_memory())578 self.assertEqual('0.5', node.display_memory())
579579
580 def test_display_memory_returns_value_divided_by_1024(self):580 def test_display_memory_returns_value_divided_by_1024(self):
581 node = factory.make_Node(memory=2048)581 node = factory.make_Node(memory=2560)
582 self.assertEqual('2', node.display_memory())582 self.assertEqual('2.5', node.display_memory())
583583
584 def test_physicalblockdevice_set_returns_physicalblockdevices(self):584 def test_physicalblockdevice_set_returns_physicalblockdevices(self):
585 node = factory.make_Node(with_boot_disk=False)585 node = factory.make_Node(with_boot_disk=False)
586586
=== modified file 'src/maasserver/node_action.py'
--- src/maasserver/node_action.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/node_action.py 2016-05-25 21:25:22 +0000
@@ -1,4 +1,4 @@
1# Copyright 2012-2015 Canonical Ltd. This software is licensed under the1# Copyright 2012-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Node actions.4"""Node actions.
@@ -90,12 +90,10 @@
90 A list of NODE_TYPEs which are applicable for this action.90 A list of NODE_TYPEs which are applicable for this action.
91 """)91 """)
9292
93 actionable_statuses = abstractproperty("""93 # Optional node states for which this action makes sense.
94 Node states for which this action makes sense.94 # A collection of NODE_STATUS values. The action will be available
9595 # only if `node.status in action.actionable_statuses`.
96 A collection of NODE_STATUS values. The action will be available96 actionable_statuses = None
97 only if `node.status in action.actionable_statuses`.
98 """)
9997
100 permission = abstractproperty("""98 permission = abstractproperty("""
101 Required permission.99 Required permission.
@@ -211,10 +209,6 @@
211 zone = Zone.objects.get(id=zone_id)209 zone = Zone.objects.get(id=zone_id)
212 self.node.set_zone(zone)210 self.node.set_zone(zone)
213211
214 def is_actionable(self):
215 """Returns true if the selected nodes can be added to a zone"""
216 return super(SetZone, self).is_actionable()
217
218212
219class Commission(NodeAction):213class Commission(NodeAction):
220 """Accept a node into the MAAS, and start the commissioning process."""214 """Accept a node into the MAAS, and start the commissioning process."""
@@ -350,10 +344,6 @@
350 except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:344 except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
351 raise NodeActionError(exception)345 raise NodeActionError(exception)
352346
353 def is_actionable(self):
354 is_actionable = super(PowerOn, self).is_actionable()
355 return is_actionable
356
357347
358FAILED_STATUSES = [348FAILED_STATUSES = [
359 status for status in map_enum(NODE_STATUS).values()349 status for status in map_enum(NODE_STATUS).values()
@@ -470,6 +460,25 @@
470 return True460 return True
471461
472462
463class Refresh(NodeAction):
464 """Refresh a rack or region and rack controller."""
465 name = "refresh"
466 display = "Refresh"
467 display_sentence = "refreshed"
468 permission = NODE_PERMISSION.ADMIN
469 for_type = {
470 NODE_TYPE.RACK_CONTROLLER,
471 NODE_TYPE.REGION_AND_RACK_CONTROLLER
472 }
473
474 def execute(self):
475 """See `NodeAction.execute`."""
476 try:
477 self.node.refresh()
478 except RPC_EXCEPTIONS + (ExternalProcessError,) as exception:
479 raise NodeActionError(exception)
480
481
473ACTION_CLASSES = (482ACTION_CLASSES = (
474 Commission,483 Commission,
475 Acquire,484 Acquire,
@@ -482,6 +491,7 @@
482 MarkFixed,491 MarkFixed,
483 SetZone,492 SetZone,
484 Delete,493 Delete,
494 Refresh,
485)495)
486496
487ACTIONS_DICT = OrderedDict((action.name, action) for action in ACTION_CLASSES)497ACTIONS_DICT = OrderedDict((action.name, action) for action in ACTION_CLASSES)
488498
=== modified file 'src/maasserver/tests/test_node_action.py'
--- src/maasserver/tests/test_node_action.py 2016-05-16 09:21:53 +0000
+++ src/maasserver/tests/test_node_action.py 2016-05-25 21:25:22 +0000
@@ -5,6 +5,7 @@
55
6__all__ = []6__all__ = []
77
8import random
8from unittest.mock import ANY9from unittest.mock import ANY
910
10from django.db import transaction11from django.db import transaction
@@ -18,6 +19,7 @@
18 NODE_STATUS_CHOICES,19 NODE_STATUS_CHOICES,
19 NODE_STATUS_CHOICES_DICT,20 NODE_STATUS_CHOICES_DICT,
20 NODE_TYPE,21 NODE_TYPE,
22 NODE_TYPE_CHOICES,
21 POWER_STATE,23 POWER_STATE,
22)24)
23from maasserver.exceptions import NodeActionError25from maasserver.exceptions import NodeActionError
@@ -25,6 +27,10 @@
25 signals,27 signals,
26 StaticIPAddress,28 StaticIPAddress,
27)29)
30from maasserver.models.node import (
31 RackController,
32 typecast_to_node_type,
33)
28from maasserver.models.signals.testing import SignalsDisabled34from maasserver.models.signals.testing import SignalsDisabled
29from maasserver.node_action import (35from maasserver.node_action import (
30 Abort,36 Abort,
@@ -38,6 +44,7 @@
38 NodeAction,44 NodeAction,
39 PowerOff,45 PowerOff,
40 PowerOn,46 PowerOn,
47 Refresh,
41 Release,48 Release,
42 RPC_EXCEPTIONS,49 RPC_EXCEPTIONS,
43 SetZone,50 SetZone,
@@ -60,7 +67,10 @@
60 post_commit_hooks,67 post_commit_hooks,
61 reload_object,68 reload_object,
62)69)
63from maastesting.matchers import MockCalledOnceWith70from maastesting.matchers import (
71 MockCalledOnce,
72 MockCalledOnceWith,
73)
64from metadataserver.enum import RESULT_TYPE74from metadataserver.enum import RESULT_TYPE
65from metadataserver.models.noderesult import NodeResult75from metadataserver.models.noderesult import NodeResult
66from netaddr import IPNetwork76from netaddr import IPNetwork
@@ -815,6 +825,33 @@
815 self.assertItemsEqual([], actions)825 self.assertItemsEqual([], actions)
816826
817827
828class TestRefresh(MAASServerTestCase):
829
830 def test_refresh(self):
831 user = factory.make_admin()
832 rack = factory.make_RackController()
833 mock_refresh = self.patch(rack, 'refresh')
834
835 with post_commit_hooks:
836 Refresh(rack, user).execute()
837
838 self.assertThat(mock_refresh, MockCalledOnce())
839
840 def test_requires_admin_permission(self):
841 user = factory.make_User()
842 rack = factory.make_RackController()
843 self.assertFalse(Refresh(rack, user).is_permitted())
844
845 def test_requires_rack(self):
846 user = factory.make_User()
847 node = factory.make_Node(
848 node_type=factory.pick_choice(
849 NODE_TYPE_CHOICES, but_not=[
850 NODE_TYPE.RACK_CONTROLLER,
851 NODE_TYPE.REGION_AND_RACK_CONTROLLER]))
852 self.assertFalse(Refresh(node, user).is_actionable())
853
854
818class TestActionsErrorHandling(MAASServerTestCase):855class TestActionsErrorHandling(MAASServerTestCase):
819 """Tests for error handling in actions.856 """Tests for error handling in actions.
820857
@@ -838,13 +875,17 @@
838 exception = self.make_exception()875 exception = self.make_exception()
839 self.patch(node, '_start').side_effect = exception876 self.patch(node, '_start').side_effect = exception
840 self.patch(node, '_stop').side_effect = exception877 self.patch(node, '_stop').side_effect = exception
878 if isinstance(node, RackController):
879 self.patch(node, 'refresh').side_effect = exception
841880
842 def make_action(self, action_class, node_status, power_state=None):881 def make_action(
882 self, action_class, node_status, power_state=None,
883 node_type=NODE_TYPE.MACHINE):
843 node = factory.make_Node(884 node = factory.make_Node(
844 interface=True, status=node_status, power_type='manual',885 interface=True, status=node_status, power_type='manual',
845 power_state=power_state)886 power_state=power_state, node_type=node_type)
846 admin = factory.make_admin()887 admin = factory.make_admin()
847 return action_class(node, admin)888 return action_class(typecast_to_node_type(node), admin)
848889
849 def test_Commission_handles_rpc_errors(self):890 def test_Commission_handles_rpc_errors(self):
850 self.addCleanup(signals.power.signals.enable)891 self.addCleanup(signals.power.signals.enable)
@@ -891,3 +932,15 @@
891 self.assertEqual(932 self.assertEqual(
892 get_error_message_for_exception(action.node._stop.side_effect),933 get_error_message_for_exception(action.node._stop.side_effect),
893 str(exception))934 str(exception))
935
936 def test_Refresh_handles_rpc_errors(self):
937 action = self.make_action(
938 Refresh, NODE_STATUS.ALLOCATED, power_state=POWER_STATE.ON,
939 node_type=random.choice([
940 NODE_TYPE.RACK_CONTROLLER,
941 NODE_TYPE.REGION_AND_RACK_CONTROLLER]))
942 self.patch_rpc_methods(action.node)
943 exception = self.assertRaises(NodeActionError, action.execute)
944 self.assertEqual(
945 get_error_message_for_exception(action.node._stop.side_effect),
946 str(exception))
894947
=== modified file 'src/maasserver/websockets/handlers/tests/test_general.py'
--- src/maasserver/websockets/handlers/tests/test_general.py 2016-05-12 19:07:37 +0000
+++ src/maasserver/websockets/handlers/tests/test_general.py 2016-05-25 21:25:22 +0000
@@ -13,6 +13,7 @@
13 BOND_MODE_CHOICES,13 BOND_MODE_CHOICES,
14 BOND_XMIT_HASH_POLICY_CHOICES,14 BOND_XMIT_HASH_POLICY_CHOICES,
15 BOOT_RESOURCE_TYPE,15 BOOT_RESOURCE_TYPE,
16 NODE_TYPE,
16)17)
17from maasserver.models import BootSourceCache18from maasserver.models import BootSourceCache
18from maasserver.models.config import Config19from maasserver.models.config import Config
@@ -28,7 +29,7 @@
2829
29class TestGeneralHandler(MAASServerTestCase):30class TestGeneralHandler(MAASServerTestCase):
3031
31 def dehydrate_actions(self, actions):32 def dehydrate_actions(self, actions, node_type=None):
32 return [33 return [
33 {34 {
34 "name": name,35 "name": name,
@@ -36,6 +37,7 @@
36 "sentence": action.display_sentence,37 "sentence": action.display_sentence,
37 }38 }
38 for name, action in actions.items()39 for name, action in actions.items()
40 if node_type is None or node_type in action.for_type
39 ]41 ]
4042
41 def test_architectures(self):43 def test_architectures(self):
@@ -107,7 +109,8 @@
107109
108 def test_machine_actions_for_admin(self):110 def test_machine_actions_for_admin(self):
109 handler = GeneralHandler(factory.make_admin(), {})111 handler = GeneralHandler(factory.make_admin(), {})
110 actions_expected = self.dehydrate_actions(ACTIONS_DICT)112 actions_expected = self.dehydrate_actions(
113 ACTIONS_DICT, NODE_TYPE.MACHINE)
111 self.assertItemsEqual(actions_expected, handler.machine_actions({}))114 self.assertItemsEqual(actions_expected, handler.machine_actions({}))
112115
113 def test_machine_actions_for_non_admin(self):116 def test_machine_actions_for_non_admin(self):
@@ -141,7 +144,7 @@
141 def test_rack_controller_actions_for_admin(self):144 def test_rack_controller_actions_for_admin(self):
142 handler = GeneralHandler(factory.make_admin(), {})145 handler = GeneralHandler(factory.make_admin(), {})
143 self.assertItemsEqual(146 self.assertItemsEqual(
144 ['delete', 'off', 'on', 'set-zone'],147 ['delete', 'off', 'on', 'refresh', 'set-zone'],
145 [action['name'] for action in handler.rack_controller_actions({})])148 [action['name'] for action in handler.rack_controller_actions({})])
146149
147 def test_rack_controller_actions_for_non_admin(self):150 def test_rack_controller_actions_for_non_admin(self):
@@ -151,7 +154,7 @@
151 def test_region_and_rack_controller_actions_for_admin(self):154 def test_region_and_rack_controller_actions_for_admin(self):
152 handler = GeneralHandler(factory.make_admin(), {})155 handler = GeneralHandler(factory.make_admin(), {})
153 self.assertItemsEqual(156 self.assertItemsEqual(
154 ['set-zone', 'delete'],157 ['set-zone', 'refresh', 'delete'],
155 [action['name']158 [action['name']
156 for action in handler.region_and_rack_controller_actions({})])159 for action in handler.region_and_rack_controller_actions({})])
157160