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
=== modified file 'src/maasserver/api/devices.py'
--- src/maasserver/api/devices.py 2016-04-28 01:14:14 +0000
+++ src/maasserver/api/devices.py 2016-05-09 20:14:05 +0000
@@ -12,6 +12,7 @@
12 NodesHandler,12 NodesHandler,
13 OwnerDataMixin,13 OwnerDataMixin,
14)14)
15from maasserver.api.support import operation
15from maasserver.enum import NODE_PERMISSION16from maasserver.enum import NODE_PERMISSION
16from maasserver.exceptions import MAASAPIValidationError17from maasserver.exceptions import MAASAPIValidationError
17from maasserver.forms import (18from maasserver.forms import (
@@ -19,6 +20,7 @@
19 DeviceWithMACsForm,20 DeviceWithMACsForm,
20)21)
21from maasserver.models.node import Device22from maasserver.models.node import Device
23from maasserver.utils.orm import reload_object
22from piston3.utils import rc24from piston3.utils import rc
2325
24# Device's fields exposed on the API.26# Device's fields exposed on the API.
@@ -116,6 +118,32 @@
116 device.delete()118 device.delete()
117 return rc.DELETED119 return rc.DELETED
118120
121 @operation(idempotent=False)
122 def restore_networking_configuration(self, request, system_id):
123 """Reset a device's network options.
124
125 Returns 404 if the device is not found
126 Returns 403 if the user does not have permission to reset the device.
127 """
128 device = self.model.objects.get_node_or_404(
129 system_id=system_id, user=request.user,
130 perm=NODE_PERMISSION.ADMIN)
131 device.set_initial_networking_configuration()
132 return reload_object(device)
133
134 @operation(idempotent=False)
135 def restore_default_configuration(self, request, system_id):
136 """Reset a device's configuration to its initial state.
137
138 Returns 404 if the device is not found.
139 Returns 403 if the user does not have permission to reset the device.
140 """
141 device = self.model.objects.get_node_or_404(
142 system_id=system_id, user=request.user,
143 perm=NODE_PERMISSION.ADMIN)
144 device.restore_default_configuration()
145 return reload_object(device)
146
119 @classmethod147 @classmethod
120 def resource_uri(cls, device=None):148 def resource_uri(cls, device=None):
121 # This method is called by piston in two different contexts:149 # This method is called by piston in two different contexts:
122150
=== modified file 'src/maasserver/api/machines.py'
--- src/maasserver/api/machines.py 2016-05-04 02:39:11 +0000
+++ src/maasserver/api/machines.py 2016-05-09 20:14:05 +0000
@@ -77,7 +77,10 @@
77 StorageLayoutForm,77 StorageLayoutForm,
78 StorageLayoutMissingBootDiskError,78 StorageLayoutMissingBootDiskError,
79)79)
80from maasserver.utils.orm import get_first80from maasserver.utils.orm import (
81 get_first,
82 reload_object,
83)
81import yaml84import yaml
8285
83# Machine's fields exposed on the API.86# Machine's fields exposed on the API.
@@ -420,7 +423,7 @@
420 Note: This will clear the current storage layout and any extra423 Note: This will clear the current storage layout and any extra
421 configuration and replace it will the new layout.424 configuration and replace it will the new layout.
422425
423 :param storage_layout: Storage layout for the machine. (flat, lvm426 :param storage_layout: Storage layout for the machine. (flat, lvm,
424 and bcache)427 and bcache)
425428
426 The following are optional for all layouts:429 The following are optional for all layouts:
@@ -583,6 +586,45 @@
583 content_type='text/plain')586 content_type='text/plain')
584587
585 @operation(idempotent=False)588 @operation(idempotent=False)
589 def restore_networking_configuration(self, request, system_id):
590 """Reset a machine's networking options to its initial state.
591
592 Returns 404 if the machine is not found.
593 Returns 403 if the user does not have permission to reset the machine.
594 """
595 machine = self.model.objects.get_node_or_404(
596 system_id=system_id, user=request.user,
597 perm=NODE_PERMISSION.ADMIN)
598 machine.set_initial_networking_configuration()
599 return reload_object(machine)
600
601 @operation(idempotent=False)
602 def restore_storage_configuration(self, request, system_id):
603 """Reset a machine's storage options to its initial state.
604
605 Returns 404 if the machine is not found.
606 Returns 403 if the user does not have permission to reset the machine.
607 """
608 machine = self.model.objects.get_node_or_404(
609 system_id=system_id, user=request.user,
610 perm=NODE_PERMISSION.ADMIN)
611 machine.set_default_storage_layout()
612 return reload_object(machine)
613
614 @operation(idempotent=False)
615 def restore_default_configuration(self, request, system_id):
616 """Reset a machine's configuration to its initial state.
617
618 Returns 404 if the machine is not found.
619 Returns 403 if the user does not have permission to reset the machine.
620 """
621 machine = self.model.objects.get_node_or_404(
622 system_id=system_id, user=request.user,
623 perm=NODE_PERMISSION.ADMIN)
624 machine.restore_default_configuration()
625 return reload_object(machine)
626
627 @operation(idempotent=False)
586 def mark_broken(self, request, system_id):628 def mark_broken(self, request, system_id):
587 """Mark a node as 'broken'.629 """Mark a node as 'broken'.
588630
589631
=== modified file 'src/maasserver/api/tests/test_devices.py'
--- src/maasserver/api/tests/test_devices.py 2016-04-22 17:28:15 +0000
+++ src/maasserver/api/tests/test_devices.py 2016-05-09 20:14:05 +0000
@@ -16,11 +16,13 @@
16from maasserver.models import (16from maasserver.models import (
17 Device,17 Device,
18 Domain,18 Domain,
19 node as node_module,
19)20)
20from maasserver.testing.api import APITestCase21from maasserver.testing.api import APITestCase
21from maasserver.testing.factory import factory22from maasserver.testing.factory import factory
22from maasserver.utils.converters import json_load_bytes23from maasserver.utils.converters import json_load_bytes
23from maasserver.utils.orm import reload_object24from maasserver.utils.orm import reload_object
25from maastesting.matchers import MockCalledOnce
2426
2527
26class DeviceOwnerDataTest(APITestCase):28class DeviceOwnerDataTest(APITestCase):
@@ -348,3 +350,45 @@
348 response = self.client.delete(get_device_uri(device))350 response = self.client.delete(get_device_uri(device))
349 self.assertEqual(http.client.FORBIDDEN, response.status_code)351 self.assertEqual(http.client.FORBIDDEN, response.status_code)
350 self.assertEqual(device, reload_object(device))352 self.assertEqual(device, reload_object(device))
353
354 def test_restore_networking_configuration(self):
355 self.become_admin()
356 device = factory.make_Device()
357 mock_set_initial_networking_config = self.patch(
358 node_module.Device, 'set_initial_networking_configuration')
359 response = self.client.post(
360 get_device_uri(device),
361 {'op': 'restore_networking_configuration'})
362 self.assertEqual(http.client.OK, response.status_code)
363 self.assertEqual(
364 device.system_id, json_load_bytes(response.content)['system_id'])
365 self.assertThat(mock_set_initial_networking_config, MockCalledOnce())
366
367 def test_restore_networking_configuration_requires_admin(self):
368 device = factory.make_Device()
369 response = self.client.post(
370 get_device_uri(device),
371 {'op': 'restore_networking_configuration'})
372 self.assertEqual(
373 http.client.FORBIDDEN, response.status_code, response.content)
374
375 def test_restore_default_configuration(self):
376 self.become_admin()
377 device = factory.make_Device()
378 mock_set_initial_networking_config = self.patch(
379 node_module.Device, 'restore_default_configuration')
380 response = self.client.post(
381 get_device_uri(device),
382 {'op': 'restore_default_configuration'})
383 self.assertEqual(http.client.OK, response.status_code)
384 self.assertEqual(
385 device.system_id, json_load_bytes(response.content)['system_id'])
386 self.assertThat(mock_set_initial_networking_config, MockCalledOnce())
387
388 def test_restore_default_configuration_requires_admin(self):
389 device = factory.make_Device()
390 response = self.client.post(
391 get_device_uri(device),
392 {'op': 'restore_default_configuration'})
393 self.assertEqual(
394 http.client.FORBIDDEN, response.status_code, response.content)
351395
=== modified file 'src/maasserver/api/tests/test_machine.py'
--- src/maasserver/api/tests/test_machine.py 2016-05-04 02:39:11 +0000
+++ src/maasserver/api/tests/test_machine.py 2016-05-09 20:14:05 +0000
@@ -57,6 +57,7 @@
57from maastesting.matchers import (57from maastesting.matchers import (
58 Equals,58 Equals,
59 HasLength,59 HasLength,
60 MockCalledOnce,
60 MockCalledOnceWith,61 MockCalledOnceWith,
61)62)
62from metadataserver.models import (63from metadataserver.models import (
@@ -1885,6 +1886,96 @@
1885 mock_get_curtin_merged_config, MockCalledOnceWith(machine))1886 mock_get_curtin_merged_config, MockCalledOnceWith(machine))
18861887
18871888
1889class TestRestoreNetworkingConfiguration(APITestCase):
1890 """Tests for
1891 /api/2.0/machines/<machine>/?op=restore_networking_configuration"""
1892
1893 def get_machine_uri(self, machine):
1894 """Get the API URI for `machine`."""
1895 return reverse('machine_handler', args=[machine.system_id])
1896
1897 def test_(self):
1898 self.become_admin()
1899 machine = factory.make_Machine(status=NODE_STATUS.READY)
1900 mock_set_initial_networking_config = self.patch(
1901 node_module.Machine, 'set_initial_networking_configuration')
1902 response = self.client.post(
1903 self.get_machine_uri(machine),
1904 {'op': 'restore_networking_configuration'})
1905 self.assertEqual(http.client.OK, response.status_code)
1906 self.assertEqual(
1907 machine.system_id, json_load_bytes(response.content)['system_id'])
1908 self.assertThat(mock_set_initial_networking_config, MockCalledOnce())
1909
1910 def test_restore_networking_configuration_requires_admin(self):
1911 machine = factory.make_Machine()
1912 response = self.client.post(
1913 self.get_machine_uri(machine),
1914 {'op': 'restore_networking_configuration'})
1915 self.assertEqual(
1916 http.client.FORBIDDEN, response.status_code, response.content)
1917
1918
1919class TestRestoreStorageConfiguration(APITestCase):
1920 """Tests for
1921 /api/2.0/machines/<machine>/?op=restore_storage_configuration"""
1922
1923 def get_machine_uri(self, machine):
1924 """Get the API URI for `machine`."""
1925 return reverse('machine_handler', args=[machine.system_id])
1926
1927 def test_restore_storage_configuration(self):
1928 self.become_admin()
1929 machine = factory.make_Machine(status=NODE_STATUS.READY)
1930 mock_set_default_storage_layout = self.patch(
1931 node_module.Machine, 'set_default_storage_layout')
1932 response = self.client.post(
1933 self.get_machine_uri(machine),
1934 {'op': 'restore_storage_configuration'})
1935 self.assertEqual(http.client.OK, response.status_code)
1936 self.assertEqual(
1937 machine.system_id, json_load_bytes(response.content)['system_id'])
1938 self.assertThat(mock_set_default_storage_layout, MockCalledOnce())
1939
1940 def test_restore_storage_configuration_requires_admin(self):
1941 machine = factory.make_Machine()
1942 response = self.client.post(
1943 self.get_machine_uri(machine),
1944 {'op': 'restore_storage_configuration'})
1945 self.assertEqual(
1946 http.client.FORBIDDEN, response.status_code, response.content)
1947
1948
1949class TestRestoreDefaultConfiguration(APITestCase):
1950 """Tests for
1951 /api/2.0/machines/<machine>/?op=restore_default_configuration"""
1952
1953 def get_machine_uri(self, machine):
1954 """Get the API URI for `machine`."""
1955 return reverse('machine_handler', args=[machine.system_id])
1956
1957 def test_restore_default_configuration(self):
1958 self.become_admin()
1959 machine = factory.make_Machine(status=NODE_STATUS.READY)
1960 mock_restore_default_configuration = self.patch(
1961 node_module.Machine, 'restore_default_configuration')
1962 response = self.client.post(
1963 self.get_machine_uri(machine),
1964 {'op': 'restore_default_configuration'})
1965 self.assertEqual(http.client.OK, response.status_code)
1966 self.assertEqual(
1967 machine.system_id, json_load_bytes(response.content)['system_id'])
1968 self.assertThat(mock_restore_default_configuration, MockCalledOnce())
1969
1970 def test_restore_default_configuration_requires_admin(self):
1971 machine = factory.make_Machine()
1972 response = self.client.post(
1973 self.get_machine_uri(machine),
1974 {'op': 'restore_default_configuration'})
1975 self.assertEqual(
1976 http.client.FORBIDDEN, response.status_code, response.content)
1977
1978
1888class TestMarkBroken(APITestCase):1979class TestMarkBroken(APITestCase):
18891980
1890 def get_node_uri(self, machine):1981 def get_node_uri(self, machine):
18911982
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2016-05-09 17:19:23 +0000
+++ src/maasserver/models/node.py 2016-05-09 20:14:05 +0000
@@ -2423,6 +2423,13 @@
2423 # No interfaces on the node. Nothing to do.2423 # No interfaces on the node. Nothing to do.
2424 return2424 return
24252425
2426 if self.node_type == NODE_TYPE.MACHINE and self.status not in [
2427 NODE_STATUS.NEW, NODE_STATUS.READY, NODE_STATUS.ALLOCATED,
2428 NODE_STATUS.FAILED_DEPLOYMENT]:
2429 raise ValidationError(
2430 "Machine must be in a new, ready, allocated, or failed "
2431 "deployment state to be reset.")
2432
2426 # Set AUTO mode on the boot interface.2433 # Set AUTO mode on the boot interface.
2427 auto_set = False2434 auto_set = False
2428 discovered_addresses = boot_interface.ip_addresses.filter(2435 discovered_addresses = boot_interface.ip_addresses.filter(
@@ -3066,6 +3073,17 @@
3066 super(Machine, self).__init__(3073 super(Machine, self).__init__(
3067 node_type=NODE_TYPE.MACHINE, *args, **kwargs)3074 node_type=NODE_TYPE.MACHINE, *args, **kwargs)
30683075
3076 def restore_default_configuration(self):
3077 """Restores a machine's configuration to default settings."""
3078 if self.status not in [
3079 NODE_STATUS.NEW, NODE_STATUS.READY, NODE_STATUS.ALLOCATED,
3080 NODE_STATUS.FAILED_DEPLOYMENT]:
3081 raise ValidationError(
3082 "Machine must be in a new, ready, allocated, or failed "
3083 "deployment state to restore default configuration.")
3084 self.set_default_storage_layout()
3085 self.set_initial_networking_configuration()
3086
30693087
3070class Controller(Node):3088class Controller(Node):
3071 """A node which is either a rack or region controller."""3089 """A node which is either a rack or region controller."""
@@ -3795,6 +3813,10 @@
3795 super(Device, self).__init__(3813 super(Device, self).__init__(
3796 node_type=NODE_TYPE.DEVICE, *args, **kwargs)3814 node_type=NODE_TYPE.DEVICE, *args, **kwargs)
37973815
3816 def restore_default_configuration(self):
3817 """Restores a device's configuration to default settings."""
3818 self.set_initial_networking_configuration()
3819
3798 def clean_architecture(self, prev):3820 def clean_architecture(self, prev):
3799 # Devices aren't required to have a defined architecture3821 # Devices aren't required to have a defined architecture
3800 pass3822 pass
38013823
=== modified file 'src/maasserver/models/tests/test_node.py'
--- src/maasserver/models/tests/test_node.py 2016-05-04 16:23:11 +0000
+++ src/maasserver/models/tests/test_node.py 2016-05-09 20:14:05 +0000
@@ -106,6 +106,7 @@
106 deferToDatabase,106 deferToDatabase,
107)107)
108from maastesting.matchers import (108from maastesting.matchers import (
109 MockCalledOnce,
109 MockCalledOnceWith,110 MockCalledOnceWith,
110 MockCallsMatch,111 MockCallsMatch,
111 MockNotCalled,112 MockNotCalled,
@@ -293,6 +294,30 @@
293 list(Machine.objects.get_available_machines_for_acquisition(user)))294 list(Machine.objects.get_available_machines_for_acquisition(user)))
294295
295296
297class TestMachine(MAASServerTestCase):
298 def test_restore_default_configuration(self):
299 machine = factory.make_Machine(status=NODE_STATUS.NEW)
300 mock_set_default_storage_layout = self.patch(
301 machine, 'set_default_storage_layout')
302 mock_set_initial_networking_configuration = self.patch(
303 machine, 'set_initial_networking_configuration')
304 machine.restore_default_configuration()
305 self.assertThat(mock_set_default_storage_layout, MockCalledOnce())
306 self.assertThat(
307 mock_set_initial_networking_configuration, MockCalledOnce())
308
309 def test_restore_default_configuration_error_wrong_status(self):
310 machine = factory.make_Machine_with_Interface_on_Subnet(
311 status=factory.pick_choice(
312 NODE_STATUS_CHOICES,
313 but_not=[
314 NODE_STATUS.NEW, NODE_STATUS.READY, NODE_STATUS.ALLOCATED,
315 NODE_STATUS.FAILED_DEPLOYMENT])
316 )
317 self.assertRaises(
318 ValidationError, machine.restore_default_configuration)
319
320
296class TestControllerManager(MAASServerTestCase):321class TestControllerManager(MAASServerTestCase):
297 def test_controller_lists_node_type_rack_and_region(self):322 def test_controller_lists_node_type_rack_and_region(self):
298 racks_and_regions = set()323 racks_and_regions = set()
@@ -481,6 +506,17 @@
481 self.assertEqual('', device.architecture)506 self.assertEqual('', device.architecture)
482507
483508
509class TestDevice(MAASServerTestCase):
510
511 def test_restore_default_configuration(self):
512 device = factory.make_Device()
513 mock_set_initial_networking_configuration = self.patch(
514 device, 'set_initial_networking_configuration')
515 device.restore_default_configuration()
516 self.assertThat(
517 mock_set_initial_networking_configuration, MockCalledOnce())
518
519
484class TestNode(MAASServerTestCase):520class TestNode(MAASServerTestCase):
485521
486 def setUp(self):522 def setUp(self):
@@ -3610,6 +3646,17 @@
3610 alloc_type=IPADDRESS_TYPE.AUTO).first()3646 alloc_type=IPADDRESS_TYPE.AUTO).first()
3611 self.assertIsNone(auto_ip)3647 self.assertIsNone(auto_ip)
36123648
3649 def test_set_initial_net_config_rasies_validation_error_wrong_status(self):
3650 machine = factory.make_Machine_with_Interface_on_Subnet(
3651 status=factory.pick_choice(
3652 NODE_STATUS_CHOICES,
3653 but_not=[
3654 NODE_STATUS.NEW, NODE_STATUS.READY, NODE_STATUS.ALLOCATED,
3655 NODE_STATUS.FAILED_DEPLOYMENT])
3656 )
3657 self.assertRaises(
3658 ValidationError, machine.set_initial_networking_configuration)
3659
3613 def test_set_initial_networking_configuration_auto_on_boot_nic(self):3660 def test_set_initial_networking_configuration_auto_on_boot_nic(self):
3614 node = factory.make_Node_with_Interface_on_Subnet()3661 node = factory.make_Node_with_Interface_on_Subnet()
3615 boot_interface = node.get_boot_interface()3662 boot_interface = node.get_boot_interface()
36163663
=== modified file 'src/maasserver/testing/factory.py'
--- src/maasserver/testing/factory.py 2016-05-05 17:22:16 +0000
+++ src/maasserver/testing/factory.py 2016-05-09 20:14:05 +0000
@@ -66,6 +66,7 @@
66 IPRange,66 IPRange,
67 LargeFile,67 LargeFile,
68 LicenseKey,68 LicenseKey,
69 Machine,
69 Node,70 Node,
70 OwnerData,71 OwnerData,
71 Partition,72 Partition,
@@ -426,6 +427,10 @@
426 Node.objects.filter(id=node.id).update(created=created)427 Node.objects.filter(id=node.id).update(created=created)
427 return reload_object(node)428 return reload_object(node)
428429
430 def make_Machine(self, *args, **kwargs):
431 machine = self.make_Node(*args, node_type=NODE_TYPE.MACHINE, **kwargs)
432 return typecast_node(machine, Machine)
433
429 def make_RackController(434 def make_RackController(
430 self, last_image_sync=undefined, owner=None, **kwargs):435 self, last_image_sync=undefined, owner=None, **kwargs):
431 if owner is None:436 if owner is None:
@@ -680,6 +685,11 @@
680 vlan.save()685 vlan.save()
681 return reload_object(node)686 return reload_object(node)
682687
688 def make_Machine_with_Interface_on_Subnet(self, *args, **kwargs):
689 machine = self.make_Node_with_Interface_on_Subnet(
690 *args, node_type=NODE_TYPE.MACHINE, **kwargs)
691 return typecast_node(machine, Machine)
692
683 UNDEFINED = float('NaN')693 UNDEFINED = float('NaN')
684694
685 def _get_exclude_list(self, subnet):695 def _get_exclude_list(self, subnet):