Merge lp:~ltrager/maas/reset_node 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: 5003
Proposed branch: lp:~ltrager/maas/reset_node
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 457 lines (+286/-2)
7 files modified
src/maasserver/api/devices.py (+28/-0)
src/maasserver/api/machines.py (+44/-2)
src/maasserver/api/tests/test_devices.py (+44/-0)
src/maasserver/api/tests/test_machine.py (+91/-0)
src/maasserver/models/node.py (+22/-0)
src/maasserver/models/tests/test_node.py (+47/-0)
src/maasserver/testing/factory.py (+10/-0)
To merge this branch: bzr merge lp:~ltrager/maas/reset_node
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Gavin Panella (community) Approve
Review via email: mp+293222@code.launchpad.net

Commit message

Add the ability to reset machines and devices to default state.

To post a comment you must log in.
Revision history for this message
Andres Rodriguez (andreserl) wrote :

A method already exists that allows setting the storage back to how it was after commission. We discussed that a similar method needed to be added for the networking and I'm not seeing that method here.

As far as adding a 'reset' Is is a nice improvement but wasn't the discussed solution. Can we discuss this?

review: Needs Fixing
Revision history for this message
Gavin Panella (allenap) wrote :

[I wrote some comments on this yesterday and forgot to save them.]

Looks good to me. I've made some comments about `idempotent` which need addressing, but there's no need for me to block you on that.

You've just got to convince roaksoax now :)

review: Approve
Revision history for this message
Andres Rodriguez (andreserl) wrote :
Download full text (3.5 KiB)

Hehe se actually discussed it yesterday and came to the conclusion that we
need to change reset to restore and have individual
Methods for storage and networking!

On Friday, April 29, 2016, Gavin Panella <email address hidden>
wrote:

> Review: Approve
>
> [I wrote some comments on this yesterday and forgot to save them.]
>
> Looks good to me. I've made some comments about `idempotent` which need
> addressing, but there's no need for me to block you on that.
>
> You've just got to convince roaksoax now :)
>
> Diff comments:
>
> > === modified file 'src/maasserver/api/devices.py'
> > --- src/maasserver/api/devices.py 2016-04-14 15:25:36 +0000
> > +++ src/maasserver/api/devices.py 2016-04-28 09:04:07 +0000
> > @@ -117,6 +118,22 @@
> > device.delete()
> > return rc.DELETED
> >
> > + @operation(idempotent=True)
>
> I chose the argument name "idempotent" and unfortunately it wasn't a good
> choice. This reset operation is idempotent but it's also destructive, so
> this should be specified as idempotent=False. Ultimately this is used to
> specify an operation as available via GET (idempotent=True) or POST
> (idempotent=False), and this operation should definitely only be available
> via POST.
>
> > + def reset(self, request, system_id):
> > + """Reset a machine's network and storage options.
> > +
> > + Resets a machine's network and storage options as if the node
> was just
> > + commissioned.
> > +
> > + Returns 404 if the machine is not found
> > + Returns 403 if the user does not have permission to reset the
> machine.
>
> For this whole docstring, s/machine/device/ and remove mentions of storage
> options.
>
> > + """
> > + device = self.model.objects.get_node_or_404(
> > + system_id=system_id, user=request.user,
> > + perm=NODE_PERMISSION.ADMIN)
> > + device.set_initial_networking_configuration()
> > + return reload_object(device)
> > +
> > @operation(idempotent=False)
> > def set_owner_data(self, request, system_id):
> > """Set key/value data for the current owner.
> >
> > === modified file 'src/maasserver/api/machines.py'
> > --- src/maasserver/api/machines.py 2016-04-15 22:14:33 +0000
> > +++ src/maasserver/api/machines.py 2016-04-28 09:04:07 +0000
> > @@ -697,6 +700,22 @@
> > request.user.username)
> > return node
> >
> > + @operation(idempotent=True)
>
> Same here, re. idempotent.
>
> > + def reset(self, request, system_id):
> > + """Reset a machine's network and storage options.
> > +
> > + Resets a machine's network and storage options as if the node
> was just
> > + commissioned.
> > +
> > + Returns 404 if the machine is not found.
> > + Returns 403 if the user does not have permission to reset the
> machine.
> > + """
> > + machine = self.model.objects.get_node_or_404(
> > + system_id=system_id, user=request.user,
> > + perm=NODE_PERMISSION.ADMIN)
> > + machine.reset()
> > + return reload_object(machine)
> > +
> > @operation(idempotent=False)
> > def set_owner_data(...

Read more...

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

I've added a set_initial_networking_configuration and a restore_default_configuration operation in place of reset for both machines and devices. restore_default_configuration resets networking and storage options to default on machines, if there is anything else it should be resetting please let me know. For devices restore_default_configuration is the same as set_initial_networking_configuration, I added it to devices so MAAS has a consistent operation to reset all fields.

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

I thought we were gonna have a restore_network_configuration *and* a restore_storage_configuration or similar ?

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

We have set_storage_config which can be used to set the storage layout. If you don't know what storage layout you have set you'd have todo

maas <profile> machine set-storage-layout <system_id> storage_layout=$(maas <profile> maas get-config name=default_storage_layout)

I could add some code so that setting storage_layout=default would automatically look up what the default is or restore_storage_configuration as you suggested.

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

Hi Lee,

IIRC, we agreed to use restore for both and the restore for storage
wouldn't set the default layout but rather get it to original?

On Wednesday, May 4, 2016, Lee Trager <email address hidden> wrote:

> We have set_storage_config which can be used to set the storage layout. If
> you don't know what storage layout you have set you'd have todo
>
> maas <profile> machine set-storage-layout <system_id>
> storage_layout=$(maas <profile> maas get-config name=default_storage_layout)
>
> I could add some code so that setting storage_layout=default would
> automatically look up what the default is or restore_storage_configuration
> as you suggested.
> --
> https://code.launchpad.net/~ltrager/maas/reset_node/+merge/293222
> You are reviewing the proposed merge of lp:~ltrager/maas/reset_node into
> lp:maas.
>

--
Andres Rodriguez (RoAkSoAx)
Ubuntu Server Developer
MSc. Telecom & Networking
Systems Engineer

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

As per our IRC discussion this renames all restore operations to use the format store_*_configuration. I've also added a restore_storage_configuration which resets a machine's storage layout to whatever the global configuration option default_storage_layout is set to.

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

lgtm!

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

Attempt to merge into lp:maas failed due to conflicts:

text conflict in src/maasserver/api/machines.py
text conflict in src/maasserver/api/tests/test_machine.py

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

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

Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Hit:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [94.5 kB]
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Fetched 94.5 kB in 0s (197 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-mock python3-netaddr python3-netifaces 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).
libpq-dev is already the newest version (9.5.2-1).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
pxelin...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/api/devices.py'
2--- src/maasserver/api/devices.py 2016-04-28 01:14:14 +0000
3+++ src/maasserver/api/devices.py 2016-05-09 20:14:05 +0000
4@@ -12,6 +12,7 @@
5 NodesHandler,
6 OwnerDataMixin,
7 )
8+from maasserver.api.support import operation
9 from maasserver.enum import NODE_PERMISSION
10 from maasserver.exceptions import MAASAPIValidationError
11 from maasserver.forms import (
12@@ -19,6 +20,7 @@
13 DeviceWithMACsForm,
14 )
15 from maasserver.models.node import Device
16+from maasserver.utils.orm import reload_object
17 from piston3.utils import rc
18
19 # Device's fields exposed on the API.
20@@ -116,6 +118,32 @@
21 device.delete()
22 return rc.DELETED
23
24+ @operation(idempotent=False)
25+ def restore_networking_configuration(self, request, system_id):
26+ """Reset a device's network options.
27+
28+ Returns 404 if the device is not found
29+ Returns 403 if the user does not have permission to reset the device.
30+ """
31+ device = self.model.objects.get_node_or_404(
32+ system_id=system_id, user=request.user,
33+ perm=NODE_PERMISSION.ADMIN)
34+ device.set_initial_networking_configuration()
35+ return reload_object(device)
36+
37+ @operation(idempotent=False)
38+ def restore_default_configuration(self, request, system_id):
39+ """Reset a device's configuration to its initial state.
40+
41+ Returns 404 if the device is not found.
42+ Returns 403 if the user does not have permission to reset the device.
43+ """
44+ device = self.model.objects.get_node_or_404(
45+ system_id=system_id, user=request.user,
46+ perm=NODE_PERMISSION.ADMIN)
47+ device.restore_default_configuration()
48+ return reload_object(device)
49+
50 @classmethod
51 def resource_uri(cls, device=None):
52 # This method is called by piston in two different contexts:
53
54=== modified file 'src/maasserver/api/machines.py'
55--- src/maasserver/api/machines.py 2016-05-04 02:39:11 +0000
56+++ src/maasserver/api/machines.py 2016-05-09 20:14:05 +0000
57@@ -77,7 +77,10 @@
58 StorageLayoutForm,
59 StorageLayoutMissingBootDiskError,
60 )
61-from maasserver.utils.orm import get_first
62+from maasserver.utils.orm import (
63+ get_first,
64+ reload_object,
65+)
66 import yaml
67
68 # Machine's fields exposed on the API.
69@@ -420,7 +423,7 @@
70 Note: This will clear the current storage layout and any extra
71 configuration and replace it will the new layout.
72
73- :param storage_layout: Storage layout for the machine. (flat, lvm
74+ :param storage_layout: Storage layout for the machine. (flat, lvm,
75 and bcache)
76
77 The following are optional for all layouts:
78@@ -583,6 +586,45 @@
79 content_type='text/plain')
80
81 @operation(idempotent=False)
82+ def restore_networking_configuration(self, request, system_id):
83+ """Reset a machine's networking options to its initial state.
84+
85+ Returns 404 if the machine is not found.
86+ Returns 403 if the user does not have permission to reset the machine.
87+ """
88+ machine = self.model.objects.get_node_or_404(
89+ system_id=system_id, user=request.user,
90+ perm=NODE_PERMISSION.ADMIN)
91+ machine.set_initial_networking_configuration()
92+ return reload_object(machine)
93+
94+ @operation(idempotent=False)
95+ def restore_storage_configuration(self, request, system_id):
96+ """Reset a machine's storage options to its initial state.
97+
98+ Returns 404 if the machine is not found.
99+ Returns 403 if the user does not have permission to reset the machine.
100+ """
101+ machine = self.model.objects.get_node_or_404(
102+ system_id=system_id, user=request.user,
103+ perm=NODE_PERMISSION.ADMIN)
104+ machine.set_default_storage_layout()
105+ return reload_object(machine)
106+
107+ @operation(idempotent=False)
108+ def restore_default_configuration(self, request, system_id):
109+ """Reset a machine's configuration to its initial state.
110+
111+ Returns 404 if the machine is not found.
112+ Returns 403 if the user does not have permission to reset the machine.
113+ """
114+ machine = self.model.objects.get_node_or_404(
115+ system_id=system_id, user=request.user,
116+ perm=NODE_PERMISSION.ADMIN)
117+ machine.restore_default_configuration()
118+ return reload_object(machine)
119+
120+ @operation(idempotent=False)
121 def mark_broken(self, request, system_id):
122 """Mark a node as 'broken'.
123
124
125=== modified file 'src/maasserver/api/tests/test_devices.py'
126--- src/maasserver/api/tests/test_devices.py 2016-04-22 17:28:15 +0000
127+++ src/maasserver/api/tests/test_devices.py 2016-05-09 20:14:05 +0000
128@@ -16,11 +16,13 @@
129 from maasserver.models import (
130 Device,
131 Domain,
132+ node as node_module,
133 )
134 from maasserver.testing.api import APITestCase
135 from maasserver.testing.factory import factory
136 from maasserver.utils.converters import json_load_bytes
137 from maasserver.utils.orm import reload_object
138+from maastesting.matchers import MockCalledOnce
139
140
141 class DeviceOwnerDataTest(APITestCase):
142@@ -348,3 +350,45 @@
143 response = self.client.delete(get_device_uri(device))
144 self.assertEqual(http.client.FORBIDDEN, response.status_code)
145 self.assertEqual(device, reload_object(device))
146+
147+ def test_restore_networking_configuration(self):
148+ self.become_admin()
149+ device = factory.make_Device()
150+ mock_set_initial_networking_config = self.patch(
151+ node_module.Device, 'set_initial_networking_configuration')
152+ response = self.client.post(
153+ get_device_uri(device),
154+ {'op': 'restore_networking_configuration'})
155+ self.assertEqual(http.client.OK, response.status_code)
156+ self.assertEqual(
157+ device.system_id, json_load_bytes(response.content)['system_id'])
158+ self.assertThat(mock_set_initial_networking_config, MockCalledOnce())
159+
160+ def test_restore_networking_configuration_requires_admin(self):
161+ device = factory.make_Device()
162+ response = self.client.post(
163+ get_device_uri(device),
164+ {'op': 'restore_networking_configuration'})
165+ self.assertEqual(
166+ http.client.FORBIDDEN, response.status_code, response.content)
167+
168+ def test_restore_default_configuration(self):
169+ self.become_admin()
170+ device = factory.make_Device()
171+ mock_set_initial_networking_config = self.patch(
172+ node_module.Device, 'restore_default_configuration')
173+ response = self.client.post(
174+ get_device_uri(device),
175+ {'op': 'restore_default_configuration'})
176+ self.assertEqual(http.client.OK, response.status_code)
177+ self.assertEqual(
178+ device.system_id, json_load_bytes(response.content)['system_id'])
179+ self.assertThat(mock_set_initial_networking_config, MockCalledOnce())
180+
181+ def test_restore_default_configuration_requires_admin(self):
182+ device = factory.make_Device()
183+ response = self.client.post(
184+ get_device_uri(device),
185+ {'op': 'restore_default_configuration'})
186+ self.assertEqual(
187+ http.client.FORBIDDEN, response.status_code, response.content)
188
189=== modified file 'src/maasserver/api/tests/test_machine.py'
190--- src/maasserver/api/tests/test_machine.py 2016-05-04 02:39:11 +0000
191+++ src/maasserver/api/tests/test_machine.py 2016-05-09 20:14:05 +0000
192@@ -57,6 +57,7 @@
193 from maastesting.matchers import (
194 Equals,
195 HasLength,
196+ MockCalledOnce,
197 MockCalledOnceWith,
198 )
199 from metadataserver.models import (
200@@ -1885,6 +1886,96 @@
201 mock_get_curtin_merged_config, MockCalledOnceWith(machine))
202
203
204+class TestRestoreNetworkingConfiguration(APITestCase):
205+ """Tests for
206+ /api/2.0/machines/<machine>/?op=restore_networking_configuration"""
207+
208+ def get_machine_uri(self, machine):
209+ """Get the API URI for `machine`."""
210+ return reverse('machine_handler', args=[machine.system_id])
211+
212+ def test_(self):
213+ self.become_admin()
214+ machine = factory.make_Machine(status=NODE_STATUS.READY)
215+ mock_set_initial_networking_config = self.patch(
216+ node_module.Machine, 'set_initial_networking_configuration')
217+ response = self.client.post(
218+ self.get_machine_uri(machine),
219+ {'op': 'restore_networking_configuration'})
220+ self.assertEqual(http.client.OK, response.status_code)
221+ self.assertEqual(
222+ machine.system_id, json_load_bytes(response.content)['system_id'])
223+ self.assertThat(mock_set_initial_networking_config, MockCalledOnce())
224+
225+ def test_restore_networking_configuration_requires_admin(self):
226+ machine = factory.make_Machine()
227+ response = self.client.post(
228+ self.get_machine_uri(machine),
229+ {'op': 'restore_networking_configuration'})
230+ self.assertEqual(
231+ http.client.FORBIDDEN, response.status_code, response.content)
232+
233+
234+class TestRestoreStorageConfiguration(APITestCase):
235+ """Tests for
236+ /api/2.0/machines/<machine>/?op=restore_storage_configuration"""
237+
238+ def get_machine_uri(self, machine):
239+ """Get the API URI for `machine`."""
240+ return reverse('machine_handler', args=[machine.system_id])
241+
242+ def test_restore_storage_configuration(self):
243+ self.become_admin()
244+ machine = factory.make_Machine(status=NODE_STATUS.READY)
245+ mock_set_default_storage_layout = self.patch(
246+ node_module.Machine, 'set_default_storage_layout')
247+ response = self.client.post(
248+ self.get_machine_uri(machine),
249+ {'op': 'restore_storage_configuration'})
250+ self.assertEqual(http.client.OK, response.status_code)
251+ self.assertEqual(
252+ machine.system_id, json_load_bytes(response.content)['system_id'])
253+ self.assertThat(mock_set_default_storage_layout, MockCalledOnce())
254+
255+ def test_restore_storage_configuration_requires_admin(self):
256+ machine = factory.make_Machine()
257+ response = self.client.post(
258+ self.get_machine_uri(machine),
259+ {'op': 'restore_storage_configuration'})
260+ self.assertEqual(
261+ http.client.FORBIDDEN, response.status_code, response.content)
262+
263+
264+class TestRestoreDefaultConfiguration(APITestCase):
265+ """Tests for
266+ /api/2.0/machines/<machine>/?op=restore_default_configuration"""
267+
268+ def get_machine_uri(self, machine):
269+ """Get the API URI for `machine`."""
270+ return reverse('machine_handler', args=[machine.system_id])
271+
272+ def test_restore_default_configuration(self):
273+ self.become_admin()
274+ machine = factory.make_Machine(status=NODE_STATUS.READY)
275+ mock_restore_default_configuration = self.patch(
276+ node_module.Machine, 'restore_default_configuration')
277+ response = self.client.post(
278+ self.get_machine_uri(machine),
279+ {'op': 'restore_default_configuration'})
280+ self.assertEqual(http.client.OK, response.status_code)
281+ self.assertEqual(
282+ machine.system_id, json_load_bytes(response.content)['system_id'])
283+ self.assertThat(mock_restore_default_configuration, MockCalledOnce())
284+
285+ def test_restore_default_configuration_requires_admin(self):
286+ machine = factory.make_Machine()
287+ response = self.client.post(
288+ self.get_machine_uri(machine),
289+ {'op': 'restore_default_configuration'})
290+ self.assertEqual(
291+ http.client.FORBIDDEN, response.status_code, response.content)
292+
293+
294 class TestMarkBroken(APITestCase):
295
296 def get_node_uri(self, machine):
297
298=== modified file 'src/maasserver/models/node.py'
299--- src/maasserver/models/node.py 2016-05-09 17:19:23 +0000
300+++ src/maasserver/models/node.py 2016-05-09 20:14:05 +0000
301@@ -2423,6 +2423,13 @@
302 # No interfaces on the node. Nothing to do.
303 return
304
305+ if self.node_type == NODE_TYPE.MACHINE and self.status not in [
306+ NODE_STATUS.NEW, NODE_STATUS.READY, NODE_STATUS.ALLOCATED,
307+ NODE_STATUS.FAILED_DEPLOYMENT]:
308+ raise ValidationError(
309+ "Machine must be in a new, ready, allocated, or failed "
310+ "deployment state to be reset.")
311+
312 # Set AUTO mode on the boot interface.
313 auto_set = False
314 discovered_addresses = boot_interface.ip_addresses.filter(
315@@ -3066,6 +3073,17 @@
316 super(Machine, self).__init__(
317 node_type=NODE_TYPE.MACHINE, *args, **kwargs)
318
319+ def restore_default_configuration(self):
320+ """Restores a machine's configuration to default settings."""
321+ if self.status not in [
322+ NODE_STATUS.NEW, NODE_STATUS.READY, NODE_STATUS.ALLOCATED,
323+ NODE_STATUS.FAILED_DEPLOYMENT]:
324+ raise ValidationError(
325+ "Machine must be in a new, ready, allocated, or failed "
326+ "deployment state to restore default configuration.")
327+ self.set_default_storage_layout()
328+ self.set_initial_networking_configuration()
329+
330
331 class Controller(Node):
332 """A node which is either a rack or region controller."""
333@@ -3795,6 +3813,10 @@
334 super(Device, self).__init__(
335 node_type=NODE_TYPE.DEVICE, *args, **kwargs)
336
337+ def restore_default_configuration(self):
338+ """Restores a device's configuration to default settings."""
339+ self.set_initial_networking_configuration()
340+
341 def clean_architecture(self, prev):
342 # Devices aren't required to have a defined architecture
343 pass
344
345=== modified file 'src/maasserver/models/tests/test_node.py'
346--- src/maasserver/models/tests/test_node.py 2016-05-04 16:23:11 +0000
347+++ src/maasserver/models/tests/test_node.py 2016-05-09 20:14:05 +0000
348@@ -106,6 +106,7 @@
349 deferToDatabase,
350 )
351 from maastesting.matchers import (
352+ MockCalledOnce,
353 MockCalledOnceWith,
354 MockCallsMatch,
355 MockNotCalled,
356@@ -293,6 +294,30 @@
357 list(Machine.objects.get_available_machines_for_acquisition(user)))
358
359
360+class TestMachine(MAASServerTestCase):
361+ def test_restore_default_configuration(self):
362+ machine = factory.make_Machine(status=NODE_STATUS.NEW)
363+ mock_set_default_storage_layout = self.patch(
364+ machine, 'set_default_storage_layout')
365+ mock_set_initial_networking_configuration = self.patch(
366+ machine, 'set_initial_networking_configuration')
367+ machine.restore_default_configuration()
368+ self.assertThat(mock_set_default_storage_layout, MockCalledOnce())
369+ self.assertThat(
370+ mock_set_initial_networking_configuration, MockCalledOnce())
371+
372+ def test_restore_default_configuration_error_wrong_status(self):
373+ machine = factory.make_Machine_with_Interface_on_Subnet(
374+ status=factory.pick_choice(
375+ NODE_STATUS_CHOICES,
376+ but_not=[
377+ NODE_STATUS.NEW, NODE_STATUS.READY, NODE_STATUS.ALLOCATED,
378+ NODE_STATUS.FAILED_DEPLOYMENT])
379+ )
380+ self.assertRaises(
381+ ValidationError, machine.restore_default_configuration)
382+
383+
384 class TestControllerManager(MAASServerTestCase):
385 def test_controller_lists_node_type_rack_and_region(self):
386 racks_and_regions = set()
387@@ -481,6 +506,17 @@
388 self.assertEqual('', device.architecture)
389
390
391+class TestDevice(MAASServerTestCase):
392+
393+ def test_restore_default_configuration(self):
394+ device = factory.make_Device()
395+ mock_set_initial_networking_configuration = self.patch(
396+ device, 'set_initial_networking_configuration')
397+ device.restore_default_configuration()
398+ self.assertThat(
399+ mock_set_initial_networking_configuration, MockCalledOnce())
400+
401+
402 class TestNode(MAASServerTestCase):
403
404 def setUp(self):
405@@ -3610,6 +3646,17 @@
406 alloc_type=IPADDRESS_TYPE.AUTO).first()
407 self.assertIsNone(auto_ip)
408
409+ def test_set_initial_net_config_rasies_validation_error_wrong_status(self):
410+ machine = factory.make_Machine_with_Interface_on_Subnet(
411+ status=factory.pick_choice(
412+ NODE_STATUS_CHOICES,
413+ but_not=[
414+ NODE_STATUS.NEW, NODE_STATUS.READY, NODE_STATUS.ALLOCATED,
415+ NODE_STATUS.FAILED_DEPLOYMENT])
416+ )
417+ self.assertRaises(
418+ ValidationError, machine.set_initial_networking_configuration)
419+
420 def test_set_initial_networking_configuration_auto_on_boot_nic(self):
421 node = factory.make_Node_with_Interface_on_Subnet()
422 boot_interface = node.get_boot_interface()
423
424=== modified file 'src/maasserver/testing/factory.py'
425--- src/maasserver/testing/factory.py 2016-05-05 17:22:16 +0000
426+++ src/maasserver/testing/factory.py 2016-05-09 20:14:05 +0000
427@@ -66,6 +66,7 @@
428 IPRange,
429 LargeFile,
430 LicenseKey,
431+ Machine,
432 Node,
433 OwnerData,
434 Partition,
435@@ -426,6 +427,10 @@
436 Node.objects.filter(id=node.id).update(created=created)
437 return reload_object(node)
438
439+ def make_Machine(self, *args, **kwargs):
440+ machine = self.make_Node(*args, node_type=NODE_TYPE.MACHINE, **kwargs)
441+ return typecast_node(machine, Machine)
442+
443 def make_RackController(
444 self, last_image_sync=undefined, owner=None, **kwargs):
445 if owner is None:
446@@ -680,6 +685,11 @@
447 vlan.save()
448 return reload_object(node)
449
450+ def make_Machine_with_Interface_on_Subnet(self, *args, **kwargs):
451+ machine = self.make_Node_with_Interface_on_Subnet(
452+ *args, node_type=NODE_TYPE.MACHINE, **kwargs)
453+ return typecast_node(machine, Machine)
454+
455 UNDEFINED = float('NaN')
456
457 def _get_exclude_list(self, subnet):