Merge lp:~lamont/maas/bug-1647703 into lp:maas/2.1

Proposed by LaMont Jones
Status: Superseded
Proposed branch: lp:~lamont/maas/bug-1647703
Merge into: lp:maas/2.1
Diff against target: 17272 lines (+7220/-4756) (has conflicts)
183 files modified
HACKING.txt (+9/-5)
Makefile (+2/-1)
buildout.cfg (+2/-3)
docs/_templates/maas/static/css/main.css (+14/-0)
docs/conf.py (+2/-1)
docs/troubleshooting.rst (+1/-1)
media/README (+4/-4)
required-packages/dev (+2/-1)
services/reloader/run (+1/-1)
src/maascli/cli.py (+2/-2)
src/maasserver/__init__.py (+1/-1)
src/maasserver/api/chassis.py (+78/-0)
src/maasserver/api/doc.py (+5/-6)
src/maasserver/api/doc_handler.py (+10/-10)
src/maasserver/api/interfaces.py (+6/-6)
src/maasserver/api/nodes.py (+7/-4)
src/maasserver/api/results.py (+2/-3)
src/maasserver/api/storage.py (+76/-0)
src/maasserver/api/subnets.py (+149/-64)
src/maasserver/api/tags.py (+1/-2)
src/maasserver/api/tests/test_chassis.py (+127/-0)
src/maasserver/api/tests/test_doc.py (+12/-14)
src/maasserver/api/tests/test_nodes.py (+0/-1)
src/maasserver/api/tests/test_storage.py (+125/-0)
src/maasserver/api/tests/test_subnets.py (+48/-4)
src/maasserver/api/tests/test_vlans.py (+40/-0)
src/maasserver/api/vlans.py (+8/-1)
src/maasserver/bootresources.py (+43/-17)
src/maasserver/clusterrpc/power_parameters.py (+8/-10)
src/maasserver/clusterrpc/testing/power_parameters.py (+4/-2)
src/maasserver/clusterrpc/tests/test_power_parameters.py (+13/-13)
src/maasserver/dhcp.py (+45/-64)
src/maasserver/djangosettings/demo.py (+1/-1)
src/maasserver/djangosettings/development.py (+1/-1)
src/maasserver/djangosettings/settings.py (+3/-3)
src/maasserver/djangosettings/tests/test_settings.py (+2/-2)
src/maasserver/enum.py (+4/-0)
src/maasserver/exceptions.py (+0/-4)
src/maasserver/forms_commission.py (+0/-5)
src/maasserver/forms_subnet.py (+8/-1)
src/maasserver/forms_vlan.py (+25/-0)
src/maasserver/locks.py (+3/-3)
src/maasserver/management/commands/dbupgrade.py (+46/-3)
src/maasserver/management/commands/tests/test_dbupgrade.py (+4/-1)
src/maasserver/migrations/builtin/maasserver/0016_migrate_power_data_node_to_bmc.py (+9/-7)
src/maasserver/migrations/builtin/maasserver/0022_extract_ip_for_bmcs.py (+6/-3)
src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py (+1/-1)
src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py (+1/-1)
src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py (+22/-0)
src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py (+23/-0)
src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py (+24/-0)
src/maasserver/migrations/builtin/maasserver/0097_node_chassis_storage_hints.py (+73/-0)
src/maasserver/models/__init__.py (+6/-0)
src/maasserver/models/bmc.py (+17/-11)
src/maasserver/models/chassishints.py (+33/-0)
src/maasserver/models/event.py (+8/-1)
src/maasserver/models/node.py (+177/-56)
src/maasserver/models/signals/nodes.py (+23/-1)
src/maasserver/models/signals/tests/test_nodes.py (+25/-0)
src/maasserver/models/staticipaddress.py (+207/-28)
src/maasserver/models/subnet.py (+69/-7)
src/maasserver/models/tests/test_discovery.py (+2/-1)
src/maasserver/models/tests/test_event.py (+10/-0)
src/maasserver/models/tests/test_neighbour.py (+2/-2)
src/maasserver/models/tests/test_node.py (+214/-96)
src/maasserver/models/tests/test_staticipaddress.py (+136/-106)
src/maasserver/models/tests/test_subnet.py (+85/-11)
src/maasserver/models/tests/test_vlan.py (+8/-0)
src/maasserver/models/vlan.py (+5/-0)
src/maasserver/node_action.py (+0/-4)
src/maasserver/rpc/nodes.py (+8/-7)
src/maasserver/rpc/rackcontrollers.py (+1/-2)
src/maasserver/rpc/regionservice.py (+16/-19)
src/maasserver/rpc/tests/test_nodes.py (+2/-2)
src/maasserver/rpc/tests/test_regionservice.py (+4/-1)
src/maasserver/static/js/angular/controllers/node_details.js (+7/-2)
src/maasserver/static/js/angular/controllers/node_events.js (+14/-7)
src/maasserver/static/js/angular/controllers/node_result.js (+14/-6)
src/maasserver/static/js/angular/controllers/subnet_details.js (+3/-1)
src/maasserver/static/js/angular/controllers/tests/test_node_details.js (+33/-0)
src/maasserver/static/js/angular/controllers/tests/test_node_events.js (+19/-2)
src/maasserver/static/js/angular/controllers/tests/test_node_result.js (+17/-2)
src/maasserver/static/js/angular/controllers/tests/test_subnet_details.js (+3/-1)
src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js (+56/-3)
src/maasserver/static/js/angular/controllers/vlan_details.js (+126/-18)
src/maasserver/static/js/angular/factories/tests/test_vlans.js (+6/-3)
src/maasserver/static/js/angular/factories/vlans.js (+12/-7)
src/maasserver/static/js/angular/maas.js (+10/-0)
src/maasserver/static/partials/domain-details.html (+4/-1)
src/maasserver/static/partials/node-details.html (+37/-35)
src/maasserver/static/partials/node-events.html (+3/-3)
src/maasserver/static/partials/node-result.html (+1/-1)
src/maasserver/static/partials/nodes-list.html (+3/-3)
src/maasserver/static/partials/subnet-details.html (+7/-2)
src/maasserver/static/partials/vlan-details.html (+105/-20)
src/maasserver/testing/factory.py (+37/-22)
src/maasserver/tests/test_bootresources.py (+7/-0)
src/maasserver/tests/test_commands.py (+4/-1)
src/maasserver/tests/test_dhcp.py (+54/-81)
src/maasserver/tests/test_forms_commission.py (+10/-7)
src/maasserver/tests/test_forms_vlan.py (+70/-0)
src/maasserver/tests/test_node_action.py (+22/-11)
src/maasserver/triggers/system.py (+56/-0)
src/maasserver/triggers/tests/test_system_listener.py (+114/-0)
src/maasserver/triggers/tests/test_websocket_listener.py (+21/-0)
src/maasserver/triggers/websocket.py (+42/-0)
src/maasserver/urls_api.py (+20/-0)
src/maasserver/utils/orm.py (+102/-20)
src/maasserver/utils/tests/test_mac.py (+3/-3)
src/maasserver/utils/tests/test_orm.py (+204/-1)
src/maasserver/websockets/handlers/controller.py (+5/-2)
src/maasserver/websockets/handlers/device.py (+2/-0)
src/maasserver/websockets/handlers/machine.py (+1/-0)
src/maasserver/websockets/handlers/node.py (+2/-3)
src/maasserver/websockets/handlers/tests/test_controller.py (+6/-0)
src/maasserver/websockets/handlers/tests/test_machine.py (+1/-0)
src/maasserver/websockets/handlers/tests/test_subnet.py (+1/-0)
src/maasserver/websockets/handlers/tests/test_vlan.py (+15/-0)
src/maasserver/websockets/handlers/vlan.py (+13/-6)
src/maastesting/matchers.py (+54/-0)
src/maastesting/tests/test_matchers.py (+89/-0)
src/provisioningserver/boot/__init__.py (+11/-3)
src/provisioningserver/boot/pxe.py (+7/-1)
src/provisioningserver/boot/tests/test_boot.py (+6/-0)
src/provisioningserver/boot/uefi_amd64.py (+7/-1)
src/provisioningserver/dhcp/tests/test_config.py (+31/-102)
src/provisioningserver/diskless.py (+0/-237)
src/provisioningserver/drivers/__init__.py (+78/-104)
src/provisioningserver/drivers/chassis/__init__.py (+282/-0)
src/provisioningserver/drivers/chassis/tests/test_base.py (+585/-0)
src/provisioningserver/drivers/diskless/__init__.py (+0/-102)
src/provisioningserver/drivers/diskless/tests/test_base.py (+0/-163)
src/provisioningserver/drivers/hardware/tests/test_virsh.py (+14/-0)
src/provisioningserver/drivers/hardware/virsh.py (+3/-1)
src/provisioningserver/drivers/power/__init__.py (+85/-20)
src/provisioningserver/drivers/power/amt.py (+11/-2)
src/provisioningserver/drivers/power/apc.py (+17/-2)
src/provisioningserver/drivers/power/dli.py (+17/-2)
src/provisioningserver/drivers/power/fence_cdu.py (+18/-2)
src/provisioningserver/drivers/power/hmc.py (+19/-2)
src/provisioningserver/drivers/power/ipmi.py (+31/-2)
src/provisioningserver/drivers/power/manual.py (+3/-1)
src/provisioningserver/drivers/power/moonshot.py (+16/-2)
src/provisioningserver/drivers/power/mscm.py (+19/-2)
src/provisioningserver/drivers/power/msftocs.py (+17/-2)
src/provisioningserver/drivers/power/nova.py (+21/-2)
src/provisioningserver/drivers/power/seamicro.py (+27/-2)
src/provisioningserver/drivers/power/tests/test_base.py (+17/-3)
src/provisioningserver/drivers/power/ucsm.py (+18/-2)
src/provisioningserver/drivers/power/virsh.py (+18/-2)
src/provisioningserver/drivers/power/vmware.py (+25/-2)
src/provisioningserver/drivers/power/wedge.py (+13/-2)
src/provisioningserver/drivers/tests/test_base.py (+151/-61)
src/provisioningserver/events.py (+70/-5)
src/provisioningserver/import_images/boot_resources.py (+43/-16)
src/provisioningserver/import_images/tests/test_boot_resources.py (+15/-6)
src/provisioningserver/power/change.py (+0/-269)
src/provisioningserver/power/poweraction.py (+0/-136)
src/provisioningserver/power/query.py (+0/-206)
src/provisioningserver/power/schema.py (+0/-476)
src/provisioningserver/power/tests/test_change.py (+0/-563)
src/provisioningserver/power/tests/test_query.py (+0/-557)
src/provisioningserver/rackdservices/node_power_monitor_service.py (+1/-1)
src/provisioningserver/rackdservices/tests/test_tftp.py (+3/-3)
src/provisioningserver/rackdservices/tftp.py (+2/-2)
src/provisioningserver/rpc/arguments.py (+17/-0)
src/provisioningserver/rpc/chassis.py (+67/-0)
src/provisioningserver/rpc/cluster.py (+27/-0)
src/provisioningserver/rpc/clusterservice.py (+20/-9)
src/provisioningserver/rpc/exceptions.py (+8/-0)
src/provisioningserver/rpc/power.py (+432/-31)
src/provisioningserver/rpc/tests/test_arguments.py (+22/-0)
src/provisioningserver/rpc/tests/test_chassis.py (+124/-0)
src/provisioningserver/rpc/tests/test_clusterservice.py (+63/-17)
src/provisioningserver/rpc/tests/test_power.py (+1109/-145)
src/provisioningserver/templates/dns/zone.template (+1/-1)
src/provisioningserver/testing/network.py (+0/-40)
src/provisioningserver/tests/test_diskless.py (+0/-493)
src/provisioningserver/tests/test_events.py (+21/-6)
src/provisioningserver/utils/network.py (+4/-3)
src/provisioningserver/utils/tests/test_network.py (+8/-5)
utilities/check-imports (+0/-27)
utilities/remote-reinstall (+0/-4)
Contents conflict in src/maasserver/migrations/south/django16_south_maas19.tar.gz
Text conflict in src/maasserver/models/staticipaddress.py
Text conflict in src/maasserver/models/tests/test_staticipaddress.py
Text conflict in src/maasserver/static/partials/vlan-details.html
To merge this branch: bzr merge lp:~lamont/maas/bug-1647703
Reviewer Review Type Date Requested Status
MAAS Committers Pending
Review via email: mp+312686@code.launchpad.net

This proposal has been superseded by a proposal from 2016-12-07.

Commit message

Update the websocket node when the domain name changes.

Description of the change

Update the websocket node when the domain name changes.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'HACKING.txt'
2--- HACKING.txt 2016-03-28 13:54:47 +0000
3+++ HACKING.txt 2016-12-07 15:50:52 +0000
4@@ -138,8 +138,9 @@
5 regiond.log. To enable logging of all exceptions even exceptions where MAAS
6 will return the correct HTTP status code.::
7
8- $ sudo sed -i 's/DEBUG = False/DEBUG = True/g' /usr/share/maas/maas/settings.py
9- $ sudo service maas-regiond restart
10+ $ sudo sed -i 's/DEBUG = False/DEBUG = True/g' \
11+ > /usr/lib/python3/dist-packages/maasserver/djangosettings/settings.py
12+ $ sudo service maas-regiond restart
13
14 Run regiond in foreground
15 ^^^^^^^^^^^^^^^^^^^^^^^^^
16@@ -149,8 +150,10 @@
17 placed a breakpoint into the code you want to inspect you can start the regiond
18 process in the foreground.::
19
20- $ sudo service maas-regiond stop
21- $ sudo -u maas -H DJANGO_SETTINGS_MODULE=maas.settings PYTHONPATH=/usr/share/maas twistd3 --nodaemon --pidfile= maas-regiond
22+ $ sudo service maas-regiond stop
23+ $ sudo -u maas -H \
24+ > DJANGO_SETTINGS_MODULE=maasserver.djangosettings.settings \
25+ > twistd3 --nodaemon --pidfile= maas-regiond
26
27
28 .. Note::
29@@ -175,7 +178,8 @@
30 Development MAAS server setup
31 =============================
32
33-Access to the database is configured in ``src/maas/development.py``.
34+Access to the database is configured in
35+``src/maasserver/djangosettings/development.py``.
36
37 The ``Makefile`` or the test suite sets up a development database
38 cluster inside your branch. It lives in the ``db`` directory, which
39
40=== modified file 'Makefile'
41--- Makefile 2016-10-17 06:38:56 +0000
42+++ Makefile 2016-12-07 15:50:52 +0000
43@@ -427,7 +427,8 @@
44 $(warning 'distclean' is deprecated; use 'clean')
45
46 harness: bin/maas-region bin/database
47- $(dbrun) bin/maas-region shell --settings=maas.demo
48+ $(dbrun) bin/maas-region shell \
49+ --settings=maasserver.djangosettings.demo
50
51 dbharness: bin/database
52 bin/database --preserve shell
53
54=== modified file 'buildout.cfg'
55--- buildout.cfg 2016-10-12 15:26:17 +0000
56+++ buildout.cfg 2016-12-07 15:50:52 +0000
57@@ -104,7 +104,7 @@
58 twistd.region=twisted.scripts.twistd:run
59 initialization =
60 ${common:initialization}
61- environ.setdefault("DJANGO_SETTINGS_MODULE", "maas.development")
62+ environ.setdefault("DJANGO_SETTINGS_MODULE", "maasserver.djangosettings.development")
63 scripts =
64 maas-region
65 twistd.region
66@@ -129,7 +129,6 @@
67 # "--with-resources",
68 "--with-scenarios",
69 "--with-select",
70- "--select-dir=src/maas",
71 "--select-dir=src/maasserver",
72 "--select-dir=src/metadataserver",
73 "--cover-package=maas,maasserver,metadataserver",
74@@ -294,7 +293,7 @@
75 from os import environ
76 environ.setdefault("MAAS_RACK_DEVELOP", "TRUE")
77 environ.setdefault("MAAS_ROOT", "${buildout:directory}/run-e2e")
78- environ.setdefault("DJANGO_SETTINGS_MODULE", "maas.development")
79+ environ.setdefault("DJANGO_SETTINGS_MODULE", "maasserver.djangosettings.development")
80 environ.setdefault("DEV_DB_NAME", "test_maas_e2e")
81 environ.setdefault("MAAS_PREVENT_MIGRATIONS", "1")
82
83
84=== modified file 'docs/_templates/maas/static/css/main.css'
85--- docs/_templates/maas/static/css/main.css 2014-06-09 16:25:19 +0000
86+++ docs/_templates/maas/static/css/main.css 2016-12-07 15:50:52 +0000
87@@ -73,3 +73,17 @@
88 text-decoration: none;
89 border-bottom: 1px solid #6D4100;
90 }
91+
92+/*
93+ * Custom CSS selectors for the API documentation page.
94+ *
95+ * Make subtitles for each API endpoint smaller, so they don't overwhelm
96+ * the remainder of the documentation.
97+ */
98+div#maas-api div#operations h4 code.docutils {
99+ font-size: 75%;
100+}
101+
102+div#maas-api div#operations div.section h5 {
103+ font-size: 90%;
104+}
105
106=== modified file 'docs/conf.py'
107--- docs/conf.py 2016-03-28 13:54:47 +0000
108+++ docs/conf.py 2016-12-07 15:50:52 +0000
109@@ -24,7 +24,8 @@
110 from pytz import UTC
111
112 # Configure MAAS's settings.
113-environ.setdefault("DJANGO_SETTINGS_MODULE", "maas.settings")
114+environ.setdefault(
115+ "DJANGO_SETTINGS_MODULE", "maasserver.djangosettings.settings")
116
117 # If extensions (or modules to document with autodoc) are in another directory,
118 # add these directories to sys.path here. If the directory is relative to the
119
120=== modified file 'docs/troubleshooting.rst'
121--- docs/troubleshooting.rst 2014-09-10 16:20:31 +0000
122+++ docs/troubleshooting.rst 2016-12-07 15:50:52 +0000
123@@ -111,7 +111,7 @@
124 always point at the local server.
125 #. If you are still getting "404 - Page not found" errors, check that the MAAS
126 web interface has been installed in the right place. There should be a file
127- present called /usr/share/maas/maas/urls.py
128+ called ``urls.py`` in ``/usr/lib/python3/dist-packages/maasserver/djangosettings/``.
129
130 Debugging ephemeral image
131 =========================
132
133=== modified file 'media/README'
134--- media/README 2012-03-11 21:13:22 +0000
135+++ media/README 2016-12-07 15:50:52 +0000
136@@ -1,5 +1,5 @@
137 This folder contains somewhat ephemeral things: subfolders serve as
138-MEDIA_ROOT for maas.demo and maas.development environments. The
139-media/demo directory should always exist and not be deleted, though
140-its contents can be. The media/development directory should be created
141-and destroyed by tests, as needed.
142+MEDIA_ROOT for maasserver.djangosettings.demo and .development
143+environments. The media/demo directory should always exist and not be
144+deleted, though its contents can be. The media/development directory
145+should be created and destroyed by tests, as needed.
146
147=== modified file 'required-packages/dev'
148--- required-packages/dev 2016-08-24 20:20:55 +0000
149+++ required-packages/dev 2016-12-07 15:50:52 +0000
150@@ -13,10 +13,10 @@
151 libjs-jquery
152 libjs-jquery-hotkeys
153 libjs-yui3-full
154+libnss-wrapper
155 make
156 nodejs-legacy
157 npm
158-python-pocket-lint
159 python-bson
160 python-crochet
161 python-django
162@@ -26,6 +26,7 @@
163 python-lxml
164 python-netaddr
165 python-netifaces
166+python-pocket-lint
167 python-psycopg2
168 python-simplejson
169 python-tempita
170
171=== modified file 'services/reloader/run'
172--- services/reloader/run 2016-05-11 19:01:48 +0000
173+++ services/reloader/run 2016-12-07 15:50:52 +0000
174@@ -128,7 +128,7 @@
175 exclude_filter=lambda path: (
176 "/test/" in path or "/testing/" in path or "/." in path))
177 wm.add_watch(
178- ["src/maas*", "src/meta*"], TRIGGER_EVENTS,
179+ ["src/maasserver", "src/metadataserver"], TRIGGER_EVENTS,
180 proc_fun=handle_maas_change, rec=True, auto_add=True, do_glob=True)
181 wm.add_watch(
182 ["src/prov*"], TRIGGER_EVENTS, proc_fun=handle_pserv_change,
183
184=== modified file 'src/maascli/cli.py'
185--- src/maascli/cli.py 2016-07-30 01:17:54 +0000
186+++ src/maascli/cli.py 2016-12-07 15:50:52 +0000
187@@ -189,8 +189,8 @@
188 # Setup and the allowed django commands into the maascli.
189 management = get_django_management()
190 if management is not None and is_maasserver_available():
191- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "maas.settings")
192- sys.path.append('/usr/share/maas')
193+ os.environ.setdefault(
194+ "DJANGO_SETTINGS_MODULE", "maasserver.djangosettings.settings")
195 load_regiond_commands(management, parser)
196
197
198
199=== modified file 'src/maasserver/__init__.py'
200--- src/maasserver/__init__.py 2016-10-20 14:45:06 +0000
201+++ src/maasserver/__init__.py 2016-12-07 15:50:52 +0000
202@@ -8,7 +8,7 @@
203 'DefaultViewMeta',
204 'is_master_process',
205 'logger',
206- ]
207+]
208
209 import logging
210 from os import environ
211
212=== added file 'src/maasserver/api/chassis.py'
213--- src/maasserver/api/chassis.py 1970-01-01 00:00:00 +0000
214+++ src/maasserver/api/chassis.py 2016-12-07 15:50:52 +0000
215@@ -0,0 +1,78 @@
216+# Copyright 2016 Canonical Ltd. This software is licensed under the
217+# GNU Affero General Public License version 3 (see the file LICENSE).
218+
219+__all__ = [
220+ "ChassiHandler",
221+ "ChassisHandler",
222+ ]
223+
224+from maasserver.api.nodes import (
225+ NodeHandler,
226+ NodesHandler,
227+)
228+from maasserver.enum import NODE_PERMISSION
229+from maasserver.models.node import Chassis
230+from piston3.utils import rc
231+
232+# Chassis fields exposed on the API.
233+DISPLAYED_CHASSIS_FIELDS = (
234+ 'system_id',
235+ 'hostname',
236+ 'cpu_count',
237+ 'memory',
238+ 'chassis_type',
239+ 'node_type',
240+ 'node_type_name',
241+ )
242+
243+
244+class ChassiHandler(NodeHandler):
245+ """Manage an individual chassis.
246+
247+ The chassis is identified by its system_id.
248+ """
249+ api_doc_section_name = "Chassis"
250+
251+ create = update = None
252+ model = Chassis
253+ fields = DISPLAYED_CHASSIS_FIELDS
254+
255+ @classmethod
256+ def chassis_type(cls, chassis):
257+ return chassis.power_type
258+
259+ def delete(self, request, system_id):
260+ """Delete a specific Chassis.
261+
262+ Returns 404 if the chassis is not found.
263+ Returns 403 if the user does not have permission to delete the chassis.
264+ Returns 204 if the chassis is successfully deleted.
265+ """
266+ chassis = self.model.objects.get_node_or_404(
267+ system_id=system_id, user=request.user,
268+ perm=NODE_PERMISSION.ADMIN)
269+ chassis.delete()
270+ return rc.DELETED
271+
272+ @classmethod
273+ def resource_uri(cls, chassis=None):
274+ # This method is called by piston in two different contexts:
275+ # - when generating an uri template to be used in the documentation
276+ # (in this case, it is called with node=None).
277+ # - when populating the 'resource_uri' field of an object
278+ # returned by the API (in this case, node is a node object).
279+ chassis_system_id = "system_id"
280+ if chassis is not None:
281+ chassis_system_id = chassis.system_id
282+ return ('chassi_handler', (chassis_system_id,))
283+
284+
285+class ChassisHandler(NodesHandler):
286+ """Manage the collection of all the chassis in the MAAS."""
287+ api_doc_section_name = "Chassis"
288+ create = update = delete = None
289+ base_model = Chassis
290+
291+ @classmethod
292+ def resource_uri(cls, *args, **kwargs):
293+ return ('chassis_handler', [])
294
295=== modified file 'src/maasserver/api/doc.py'
296--- src/maasserver/api/doc.py 2016-04-12 22:25:44 +0000
297+++ src/maasserver/api/doc.py 2016-12-07 15:50:52 +0000
298@@ -33,7 +33,7 @@
299 from piston3.doc import generate_doc
300 from piston3.handler import BaseHandler
301 from piston3.resource import Resource
302-from provisioningserver.power.schema import JSON_POWER_TYPE_PARAMETERS
303+from provisioningserver.drivers.power import PowerDriverRegistry
304
305
306 def accumulate_api_resources(resolver, accumulator):
307@@ -77,8 +77,7 @@
308 def generate_power_types_doc():
309 """Generate ReST documentation for the supported power types.
310
311- The documentation is derived from the `JSON_POWER_TYPE_PARAMETERS`
312- object.
313+ The documentation is derived from the `PowerDriverRegistry`.
314 """
315 output = StringIO()
316 line = partial(print, file=output)
317@@ -92,14 +91,14 @@
318 "list if the cluster in question is from an older version of "
319 "MAAS.")
320 line()
321- for item in JSON_POWER_TYPE_PARAMETERS:
322- title = "%s (%s)" % (item['name'], item['description'])
323+ for _, driver in PowerDriverRegistry:
324+ title = "%s (%s)" % (driver.name, driver.description)
325 line(title)
326 line('=' * len(title))
327 line('')
328 line("Power parameters:")
329 line('')
330- for field in item['fields']:
331+ for field in driver.settings:
332 field_description = []
333 field_description.append(
334 "* %s (%s)." % (field['name'], field['label']))
335
336=== modified file 'src/maasserver/api/doc_handler.py'
337--- src/maasserver/api/doc_handler.py 2016-08-18 17:31:05 +0000
338+++ src/maasserver/api/doc_handler.py 2016-12-07 15:50:52 +0000
339@@ -9,7 +9,7 @@
340
341
342 API versions
343-------------
344+````````````
345
346 At any given time, MAAS may support multiple versions of its API. The version
347 number is included in the API's URL, e.g. /api/2.0/
348@@ -23,7 +23,7 @@
349
350
351 HTTP methods and parameter-passing
352-----------------------------------
353+``````````````````````````````````
354
355 The following HTTP methods are available for accessing the API:
356 * GET (for information retrieval and queries),
357@@ -82,6 +82,7 @@
358 # etc. whatever render_api_docs() produces, so that you can concatenate
359 # the two.
360 api_doc_title = dedent("""
361+ :tocdepth: 3
362 .. _region-controller-api:
363
364 ========
365@@ -109,7 +110,7 @@
366 line()
367 line()
368 line('Operations')
369- line('----------')
370+ line('``````````')
371 line()
372
373 def export_key(export):
374@@ -132,25 +133,24 @@
375 section_name = doc.handler.api_doc_section_name
376 line(section_name)
377 line('=' * len(section_name))
378- line(doc.handler.__doc__.strip())
379+ line(dedent(doc.handler.__doc__).strip())
380 line()
381 line()
382 for (http_method, op), function in sorted(exports, key=export_key):
383- line("``%s %s``" % (http_method, uri_template), end="")
384- if op is not None:
385- line(" ``op=%s``" % op, end="")
386+ operation = " op=%s" % op if op is not None else ""
387+ subsection = "``%s %s%s``" % (http_method, uri_template, operation)
388+ line("%s\n%s\n" % (subsection, '#' * len(subsection)))
389 line()
390 docstring = getdoc(function)
391 if docstring is not None:
392- for docline in docstring.splitlines():
393+ for docline in dedent(docstring).splitlines():
394 if docline.strip() == '':
395 # Blank line. Don't indent.
396 line()
397 else:
398 # Print documentation line, indented.
399- line(" ", docline, sep="")
400+ line(docline)
401 line()
402-
403 line()
404 line()
405 line(generate_power_types_doc())
406
407=== modified file 'src/maasserver/api/interfaces.py'
408--- src/maasserver/api/interfaces.py 2016-10-20 16:04:24 +0000
409+++ src/maasserver/api/interfaces.py 2016-12-07 15:50:52 +0000
410@@ -436,18 +436,18 @@
411
412 Following are parameters specific to bonds:
413
414- :param bond-mode: The operating mode of the bond.
415+ :param bond_mode: The operating mode of the bond.
416 (Default: active-backup).
417- :param bond-miimon: The link monitoring freqeuncy in milliseconds.
418+ :param bond_miimon: The link monitoring freqeuncy in milliseconds.
419 (Default: 100).
420- :param bond-downdelay: Specifies the time, in milliseconds, to wait
421+ :param bond_downdelay: Specifies the time, in milliseconds, to wait
422 before disabling a slave after a link failure has been detected.
423- :param bond-updelay: Specifies the time, in milliseconds, to wait
424+ :param bond_updelay: Specifies the time, in milliseconds, to wait
425 before enabling a slave after a link recovery has been detected.
426- :param bond-lacp_rate: Option specifying the rate in which we'll ask
427+ :param bond_lacp_rate: Option specifying the rate in which we'll ask
428 our link partner to transmit LACPDU packets in 802.3ad mode.
429 Available options are fast or slow. (Default: slow).
430- :param bond-xmit_hash_policy: The transmit hash policy to use for
431+ :param bond_xmit_hash_policy: The transmit hash policy to use for
432 slave selection in balance-xor, 802.3ad, and tlb modes.
433
434 Supported bonding modes (bond-mode):
435
436=== modified file 'src/maasserver/api/nodes.py'
437--- src/maasserver/api/nodes.py 2016-06-17 07:16:39 +0000
438+++ src/maasserver/api/nodes.py 2016-12-07 15:50:52 +0000
439@@ -47,10 +47,9 @@
440 Node,
441 OwnerData,
442 )
443-from maasserver.models.node import typecast_to_node_type
444 from maasserver.models.nodeprobeddetails import get_single_probed_details
445 from piston3.utils import rc
446-from provisioningserver.power.schema import UNKNOWN_POWER_TYPE
447+from provisioningserver.drivers.power import UNKNOWN_POWER_TYPE
448
449
450 def store_node_power_parameters(node, request):
451@@ -171,7 +170,7 @@
452 else:
453 # Return the specific node type object so we get the correct
454 # listing
455- return typecast_to_node_type(node)
456+ return node.as_self()
457
458 def delete(self, request, system_id):
459 """Delete a specific Node.
460@@ -183,7 +182,7 @@
461 node = self.model.objects.get_node_or_404(
462 system_id=system_id, user=request.user,
463 perm=NODE_PERMISSION.ADMIN)
464- typecast_to_node_type(node).delete()
465+ node.as_self().delete()
466 return rc.DELETED
467
468 @classmethod
469@@ -315,19 +314,23 @@
470
471 if self.base_model == Node:
472 # Avoid circular dependencies
473+ from maasserver.api.chassis import ChassisHandler
474 from maasserver.api.devices import DevicesHandler
475 from maasserver.api.machines import MachinesHandler
476 from maasserver.api.rackcontrollers import RackControllersHandler
477 from maasserver.api.regioncontrollers import (
478 RegionControllersHandler
479 )
480+ from maasserver.api.storage import StoragesHandler
481 racks = RackControllersHandler().read(request).order_by("id")
482 nodes = list(chain(
483+ ChassisHandler().read(request).order_by("id"),
484 DevicesHandler().read(request).order_by("id"),
485 MachinesHandler().read(request).order_by("id"),
486 racks,
487 RegionControllersHandler().read(request).exclude(
488 id__in=racks).order_by("id"),
489+ StoragesHandler().read(request).order_by("id"),
490 ))
491 return nodes
492 else:
493
494=== modified file 'src/maasserver/api/results.py'
495--- src/maasserver/api/results.py 2016-07-30 01:17:54 +0000
496+++ src/maasserver/api/results.py 2016-12-07 15:50:52 +0000
497@@ -14,7 +14,6 @@
498 )
499 from maasserver.enum import NODE_PERMISSION
500 from maasserver.models import Node
501-from maasserver.models.node import typecast_to_node_type
502 from metadataserver.models import NodeResult
503
504
505@@ -54,9 +53,9 @@
506 if result_type is not None:
507 results = results.filter(result_type__in=result_type)
508 # Convert the node objects into typed node objects so we get the
509- # proper listing
510+ # proper listing.
511 for result in results:
512- result.node = typecast_to_node_type(result.node)
513+ result.node = result.node.as_self()
514 return results
515
516 @classmethod
517
518=== added file 'src/maasserver/api/storage.py'
519--- src/maasserver/api/storage.py 1970-01-01 00:00:00 +0000
520+++ src/maasserver/api/storage.py 2016-12-07 15:50:52 +0000
521@@ -0,0 +1,76 @@
522+# Copyright 2016 Canonical Ltd. This software is licensed under the
523+# GNU Affero General Public License version 3 (see the file LICENSE).
524+
525+__all__ = [
526+ "StorageHandler",
527+ "StoragesHandler",
528+ ]
529+
530+from maasserver.api.nodes import (
531+ NodeHandler,
532+ NodesHandler,
533+)
534+from maasserver.enum import NODE_PERMISSION
535+from maasserver.models.node import Storage
536+from piston3.utils import rc
537+
538+# Storage fields exposed on the API.
539+DISPLAYED_STORAGE_FIELDS = (
540+ 'system_id',
541+ 'hostname',
542+ 'storage_type',
543+ 'node_type',
544+ 'node_type_name',
545+ )
546+
547+
548+class StorageHandler(NodeHandler):
549+ """Manage an individual storage system.
550+
551+ The storage is identified by its system_id.
552+ """
553+ api_doc_section_name = "Storage"
554+
555+ create = update = None
556+ model = Storage
557+ fields = DISPLAYED_STORAGE_FIELDS
558+
559+ @classmethod
560+ def storage_type(cls, storage):
561+ return storage.power_type
562+
563+ def delete(self, request, system_id):
564+ """Delete a specific Storage.
565+
566+ Returns 404 if the storage is not found.
567+ Returns 403 if the user does not have permission to delete the storage.
568+ Returns 204 if the storage is successfully deleted.
569+ """
570+ storage = self.model.objects.get_node_or_404(
571+ system_id=system_id, user=request.user,
572+ perm=NODE_PERMISSION.ADMIN)
573+ storage.delete()
574+ return rc.DELETED
575+
576+ @classmethod
577+ def resource_uri(cls, storage=None):
578+ # This method is called by piston in two different contexts:
579+ # - when generating an uri template to be used in the documentation
580+ # (in this case, it is called with node=None).
581+ # - when populating the 'resource_uri' field of an object
582+ # returned by the API (in this case, node is a node object).
583+ storage_system_id = "system_id"
584+ if storage is not None:
585+ storage_system_id = storage.system_id
586+ return ('storage_handler', (storage_system_id,))
587+
588+
589+class StoragesHandler(NodesHandler):
590+ """Manage the collection of all the storage in the MAAS."""
591+ api_doc_section_name = "Storages"
592+ create = update = delete = None
593+ base_model = Storage
594+
595+ @classmethod
596+ def resource_uri(cls, *args, **kwargs):
597+ return ('storages_handler', [])
598
599=== modified file 'src/maasserver/api/subnets.py'
600--- src/maasserver/api/subnets.py 2016-09-23 01:32:02 +0000
601+++ src/maasserver/api/subnets.py 2016-12-07 15:50:52 +0000
602@@ -29,6 +29,7 @@
603 'rdns_mode',
604 'active_discovery',
605 'allow_proxy',
606+ 'managed',
607 )
608
609
610@@ -49,32 +50,76 @@
611
612 @admin_method
613 def create(self, request):
614- """Create a subnet.
615-
616- :param name: Name of the subnet.
617- :param description: Description of the subnet.
618- :param fabric: Fabric for the subnet. Defaults to the fabric the
619- provided VLAN belongs to or defaults to the default fabric.
620- :param vlan: VLAN this subnet belongs to. Defaults to the default
621- VLAN for the provided fabric or defaults to the default VLAN in
622- the default fabric.
623- :param vid: VID of the VLAN this subnet belongs to. Only used when
624- vlan is not provided. Picks the VLAN with this VID in the provided
625- fabric or the default fabric if one is not given.
626- :param space: Space this subnet is in. Defaults to the default space.
627- :param cidr: The network CIDR for this subnet.
628- :param gateway_ip: The gateway IP address for this subnet.
629- :param rdns_mode: How reverse DNS is handled for this subnet.
630- One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled means
631- no reverse zone is created; Enabled means generate the reverse
632- zone; RFC2317 extends Enabled to create the necessary parent zone
633- with the appropriate CNAME resource records for the network, if the
634- network is small enough to require the support described in
635- RFC2317.
636- :param allow_proxy: Configure maas-proxy to allow requests from this
637- subnet.
638- :param dns_servers: Comma-seperated list of DNS servers for this
639- subnet.
640+ """\
641+ Create a subnet.
642+
643+ Required parameters
644+ -------------------
645+
646+ cidr
647+ The network CIDR for this subnet.
648+
649+
650+ Optional parameters
651+ -------------------
652+
653+ name
654+ Name of the subnet.
655+
656+ description
657+ Description of the subnet.
658+
659+ vlan
660+ VLAN this subnet belongs to. Defaults to the default VLAN for the
661+ provided fabric or defaults to the default VLAN in the default fabric
662+ (if unspecified).
663+
664+ fabric
665+ Fabric for the subnet. Defaults to the fabric the
666+ provided VLAN belongs to, or defaults to the default fabric.
667+
668+ vid
669+ VID of the VLAN this subnet belongs to. Only used when vlan is
670+ not provided. Picks the VLAN with this VID in the provided
671+ fabric or the default fabric if one is not given.
672+
673+ space
674+ Space this subnet is in. Defaults to the default space.
675+
676+ gateway_ip
677+ The gateway IP address for this subnet.
678+
679+ rdns_mode
680+ How reverse DNS is handled for this subnet.
681+ One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled
682+ means no reverse zone is created; Enabled means generate the
683+ reverse zone; RFC2317 extends Enabled to create the necessary
684+ parent zone with the appropriate CNAME resource records for the
685+ network, if the network is small enough to require the support
686+ described in RFC2317.
687+
688+ allow_proxy
689+ Configure maas-proxy to allow requests from this
690+ subnet.
691+
692+ dns_servers
693+ Comma-seperated list of DNS servers for this subnet.
694+
695+ managed
696+ In MAAS 2.0+, all subnets are assumed to be managed by default.
697+
698+ Only managed subnets allow DHCP to be enabled on their related
699+ dynamic ranges. (Thus, dynamic ranges become "informational
700+ only"; an indication that another DHCP server is currently
701+ handling them, or that MAAS will handle them when the subnet is
702+ enabled for management.)
703+
704+ Managed subnets do not allow IP allocation by default. The
705+ meaning of a "reserved" IP range is reversed for an unmanaged
706+ subnet. (That is, for managed subnets, "reserved" means "MAAS
707+ cannot allocate any IP address within this reserved block". For
708+ unmanaged subnets, "reserved" means "MAAS must allocate IP
709+ addresses only from reserved IP ranges".
710 """
711 form = SubnetForm(data=request.data)
712 if form.is_valid():
713@@ -100,7 +145,8 @@
714
715 @classmethod
716 def space(cls, subnet):
717- """Return the name of the space.
718+ """\
719+ Return the name of the space.
720
721 Only the name is returned because the space endpoint will return
722 a list of all subnets in that space. If this returned the subnet
723@@ -109,7 +155,8 @@
724 return subnet.space.get_name()
725
726 def read(self, request, subnet_id):
727- """Read subnet.
728+ """\
729+ Read subnet.
730
731 Returns 404 if the subnet is not found.
732 """
733@@ -117,19 +164,44 @@
734 subnet_id, request.user, NODE_PERMISSION.VIEW)
735
736 def update(self, request, subnet_id):
737- """Update subnet.
738-
739- :param name: Name of the subnet.
740- :param description: Description of the subnet.
741- :param vlan: VLAN this subnet belongs to.
742- :param space: Space this subnet is in.
743- :param cidr: The network CIDR for this subnet.
744- :param gateway_ip: The gateway IP address for this subnet.
745- :param rdns_mode: How reverse DNS is handled for this subnet.
746- :param allow_proxy: Configure maas-proxy to allow requests from this \
747- subnet.
748- :param dns_servers: Comma-seperated list of DNS servers for this \
749- subnet.
750+ """\
751+ Update the specified subnet.
752+
753+ Please see the documentation for the 'create' operation for detailed
754+ descriptions of each parameter.
755+
756+ Optional parameters
757+ -------------------
758+
759+ name
760+ Name of the subnet.
761+
762+ description
763+ Description of the subnet.
764+
765+ vlan
766+ VLAN this subnet belongs to.
767+
768+ space
769+ Space this subnet is in.
770+
771+ cidr
772+ The network CIDR for this subnet.
773+
774+ gateway_ip
775+ The gateway IP address for this subnet.
776+
777+ rdns_mode
778+ How reverse DNS is handled for this subnet.
779+
780+ allow_proxy
781+ Configure maas-proxy to allow requests from this subnet.
782+
783+ dns_servers
784+ Comma-seperated list of DNS servers for this subnet.
785+
786+ managed
787+ If False, MAAS should not manage this subnet. (Default: True)
788
789 Returns 404 if the subnet is not found.
790 """
791@@ -142,7 +214,8 @@
792 raise MAASAPIValidationError(form.errors)
793
794 def delete(self, request, subnet_id):
795- """Delete subnet.
796+ """\
797+ Delete subnet.
798
799 Returns 404 if the subnet is not found.
800 """
801@@ -153,7 +226,8 @@
802
803 @operation(idempotent=True)
804 def reserved_ip_ranges(self, request, subnet_id):
805- """Lists IP ranges currently reserved in the subnet.
806+ """\
807+ Lists IP ranges currently reserved in the subnet.
808
809 Returns 404 if the subnet is not found.
810 """
811@@ -163,7 +237,8 @@
812
813 @operation(idempotent=True)
814 def unreserved_ip_ranges(self, request, subnet_id):
815- """Lists IP ranges currently unreserved in the subnet.
816+ """\
817+ Lists IP ranges currently unreserved in the subnet.
818
819 Returns 404 if the subnet is not found.
820 """
821@@ -174,22 +249,27 @@
822
823 @operation(idempotent=True)
824 def statistics(self, request, subnet_id):
825- """
826+ """\
827 Returns statistics for the specified subnet, including:
828
829- num_available - the number of available IP addresses
830- largest_available - the largest number of contiguous free IP addresses
831- num_unavailable - the number of unavailable IP addresses
832- total_addresses - the sum of the available plus unavailable addresses
833- usage - the (floating point) usage percentage of this subnet
834- usage_string - the (formatted unicode) usage percentage of this subnet
835- ranges - the specific IP ranges present in ths subnet (if specified)
836-
837- Optional arguments:
838- include_ranges: if True, includes detailed information
839- about the usage of this range.
840- include_suggestions: if True, includes the suggested gateway and
841- dynamic range for this subnet, if it were to be configured.
842+ num_available: the number of available IP addresses
843+ largest_available: the largest number of contiguous free IP addresses
844+ num_unavailable: the number of unavailable IP addresses
845+ total_addresses: the sum of the available plus unavailable addresses
846+ usage: the (floating point) usage percentage of this subnet
847+ usage_string: the (formatted unicode) usage percentage of this subnet
848+ ranges: the specific IP ranges present in ths subnet (if specified)
849+
850+ Optional parameters
851+ -------------------
852+
853+ include_ranges
854+ If True, includes detailed information
855+ about the usage of this range.
856+
857+ include_suggestions
858+ If True, includes the suggested gateway and dynamic range for this
859+ subnet, if it were to be configured.
860
861 Returns 404 if the subnet is not found.
862 """
863@@ -208,14 +288,19 @@
864
865 @operation(idempotent=True)
866 def ip_addresses(self, request, subnet_id):
867- """
868+ """\
869 Returns a summary of IP addresses assigned to this subnet.
870
871- Optional arguments:
872- with_username: (default=True) if False, suppresses the display
873- of usernames associated with each address.
874- with_node_summary: (default=True) if False, suppresses the display
875- of any node associated with each address.
876+ Optional parameters
877+ -------------------
878+
879+ with_username
880+ If False, suppresses the display of usernames associated with each
881+ address. (Default: True)
882+
883+ with_node_summary
884+ If False, suppresses the display of any node associated with each
885+ address. (Default: True)
886 """
887 subnet = Subnet.objects.get_subnet_or_404(
888 subnet_id, request.user, NODE_PERMISSION.VIEW)
889
890=== modified file 'src/maasserver/api/tags.py'
891--- src/maasserver/api/tags.py 2016-04-27 00:55:47 +0000
892+++ src/maasserver/api/tags.py 2016-12-07 15:50:52 +0000
893@@ -37,7 +37,6 @@
894 RegionController,
895 Tag,
896 )
897-from maasserver.models.node import typecast_to_node_type
898 from maasserver.models.user import get_auth_tokens
899 from maasserver.utils.orm import get_one
900 from piston3.utils import rc
901@@ -137,7 +136,7 @@
902 self.fields = None
903 tag = Tag.objects.get_tag_or_404(name=name, user=request.user)
904 return [
905- typecast_to_node_type(node)
906+ node.as_self()
907 for node in model.objects.get_nodes(
908 request.user, NODE_PERMISSION.VIEW,
909 from_nodes=tag.node_set.all())
910
911=== added file 'src/maasserver/api/tests/test_chassis.py'
912--- src/maasserver/api/tests/test_chassis.py 1970-01-01 00:00:00 +0000
913+++ src/maasserver/api/tests/test_chassis.py 2016-12-07 15:50:52 +0000
914@@ -0,0 +1,127 @@
915+# Copyright 2016 Canonical Ltd. This software is licensed under the
916+# GNU Affero General Public License version 3 (see the file LICENSE).
917+
918+"""Tests for chassis API."""
919+
920+__all__ = []
921+
922+import http.client
923+
924+from django.core.urlresolvers import reverse
925+from maasserver.enum import (
926+ NODE_STATUS,
927+ NODE_TYPE,
928+)
929+from maasserver.testing.api import APITestCase
930+from maasserver.testing.factory import factory
931+from maasserver.utils.converters import json_load_bytes
932+from maasserver.utils.orm import reload_object
933+
934+
935+class TestChassisAPI(APITestCase.ForUser):
936+
937+ def test_handler_path(self):
938+ self.assertEqual(
939+ '/api/2.0/chassis/', reverse('chassis_handler'))
940+
941+ def create_chassis(self, owner, nb=3):
942+ return [
943+ factory.make_Node(
944+ interface=True, node_type=NODE_TYPE.CHASSIS, owner=owner)
945+ for _ in range(nb)
946+ ]
947+
948+ def test_read_lists_chassis(self):
949+ # The api allows for fetching the list of chassis.
950+ chassis = self.create_chassis(owner=self.user)
951+ factory.make_Node(
952+ status=NODE_STATUS.ALLOCATED, owner=self.user)
953+ response = self.client.get(reverse('chassis_handler'))
954+ parsed_result = json_load_bytes(response.content)
955+
956+ self.assertEqual(http.client.OK, response.status_code)
957+ self.assertItemsEqual(
958+ [chassi.system_id for chassi in chassis],
959+ [chassi.get('system_id') for chassi in parsed_result])
960+
961+ def test_read_ignores_nodes(self):
962+ factory.make_Node(
963+ status=NODE_STATUS.ALLOCATED, owner=self.user)
964+ response = self.client.get(reverse('chassis_handler'))
965+ parsed_result = json_load_bytes(response.content)
966+
967+ self.assertEqual(http.client.OK, response.status_code)
968+ self.assertEqual(
969+ [],
970+ [chassi.get('system_id') for chassi in parsed_result])
971+
972+ def test_read_with_id_returns_matching_chassis(self):
973+ # The "list" operation takes optional "id" parameters. Only
974+ # chassis with matching ids will be returned.
975+ chassis = self.create_chassis(owner=self.user)
976+ ids = [chassi.system_id for chassi in chassis]
977+ matching_id = ids[0]
978+ response = self.client.get(reverse('chassis_handler'), {
979+ 'id': [matching_id],
980+ })
981+ parsed_result = json_load_bytes(response.content)
982+ self.assertItemsEqual(
983+ [matching_id],
984+ [chassi.get('system_id') for chassi in parsed_result])
985+
986+ def test_read_returns_limited_fields(self):
987+ self.create_chassis(owner=self.user)
988+ response = self.client.get(reverse('chassis_handler'))
989+ parsed_result = json_load_bytes(response.content)
990+ self.assertItemsEqual(
991+ [
992+ 'hostname',
993+ 'system_id',
994+ 'cpu_count',
995+ 'memory',
996+ 'chassis_type',
997+ 'node_type',
998+ 'node_type_name',
999+ 'resource_uri',
1000+ ],
1001+ list(parsed_result[0]))
1002+
1003+
1004+def get_chassi_uri(chassis):
1005+ """Return a chassis URI on the API."""
1006+ return reverse('chassi_handler', args=[chassis.system_id])
1007+
1008+
1009+class TestChassiAPI(APITestCase.ForUser):
1010+
1011+ def test_handler_path(self):
1012+ system_id = factory.make_name('system-id')
1013+ self.assertEqual(
1014+ '/api/2.0/chassis/%s/' % system_id,
1015+ reverse('chassi_handler', args=[system_id]))
1016+
1017+ def test_GET_reads_chassis(self):
1018+ chassis = factory.make_Node(
1019+ node_type=NODE_TYPE.CHASSIS, owner=self.user)
1020+
1021+ response = self.client.get(get_chassi_uri(chassis))
1022+ self.assertEqual(
1023+ http.client.OK, response.status_code, response.content)
1024+ parsed_chassis = json_load_bytes(response.content)
1025+ self.assertEqual(chassis.system_id, parsed_chassis["system_id"])
1026+
1027+ def test_DELETE_removes_chassis(self):
1028+ self.become_admin()
1029+ chassis = factory.make_Node(
1030+ node_type=NODE_TYPE.CHASSIS, owner=self.user)
1031+ response = self.client.delete(get_chassi_uri(chassis))
1032+ self.assertEqual(
1033+ http.client.NO_CONTENT, response.status_code, response.content)
1034+ self.assertIsNone(reload_object(chassis))
1035+
1036+ def test_DELETE_rejects_deletion_if_not_permitted(self):
1037+ chassis = factory.make_Node(
1038+ node_type=NODE_TYPE.CHASSIS, owner=factory.make_User())
1039+ response = self.client.delete(get_chassi_uri(chassis))
1040+ self.assertEqual(http.client.FORBIDDEN, response.status_code)
1041+ self.assertEqual(chassis, reload_object(chassis))
1042
1043=== modified file 'src/maasserver/api/tests/test_doc.py'
1044--- src/maasserver/api/tests/test_doc.py 2016-08-31 13:52:59 +0000
1045+++ src/maasserver/api/tests/test_doc.py 2016-12-07 15:50:52 +0000
1046@@ -8,6 +8,7 @@
1047 import http.client
1048 from inspect import getdoc
1049 from io import StringIO
1050+import random
1051 import sys
1052 import types
1053 from unittest.mock import sentinel
1054@@ -49,7 +50,7 @@
1055 from piston3.doc import HandlerDocumentation
1056 from piston3.handler import BaseHandler
1057 from piston3.resource import Resource
1058-from provisioningserver.power.schema import make_json_field
1059+from provisioningserver.drivers.power import PowerDriverRegistry
1060 from testtools.matchers import (
1061 AfterPreprocessing,
1062 AllMatch,
1063@@ -416,22 +417,19 @@
1064 self.assertThat(doc, ContainsAll(["Power types", "IPMI"]))
1065
1066 def test__generate_power_types_doc_generates_describes_power_type(self):
1067- name = factory.make_name('name')
1068- description = factory.make_name('description')
1069- param_name = factory.make_name('param_name')
1070- param_description = factory.make_name('param_description')
1071- json_fields = [{
1072- 'name': name,
1073- 'description': description,
1074- 'fields': [
1075- make_json_field(param_name, param_description),
1076- ],
1077- }]
1078- self.patch(doc_module, "JSON_POWER_TYPE_PARAMETERS", json_fields)
1079+ power_driver = random.choice([
1080+ driver
1081+ for _, driver in PowerDriverRegistry
1082+ if len(driver.settings) > 0
1083+ ])
1084 doc = generate_power_types_doc()
1085 self.assertThat(
1086 doc,
1087- ContainsAll([name, description, param_name, param_description]))
1088+ ContainsAll([
1089+ power_driver.name,
1090+ power_driver.description,
1091+ power_driver.settings[0]['name'],
1092+ power_driver.settings[0]['label']]))
1093
1094
1095 class TestDescribeCanonical(MAASTestCase):
1096
1097=== modified file 'src/maasserver/api/tests/test_nodes.py'
1098--- src/maasserver/api/tests/test_nodes.py 2016-10-28 08:43:09 +0000
1099+++ src/maasserver/api/tests/test_nodes.py 2016-12-07 15:50:52 +0000
1100@@ -304,7 +304,6 @@
1101 response = self.client.get(reverse('nodes_handler'))
1102 parsed_result = json.loads(
1103 response.content.decode(settings.DEFAULT_CHARSET))
1104-
1105 self.assertEqual(http.client.OK, response.status_code)
1106 self.assertItemsEqual(system_ids, extract_system_ids(parsed_result))
1107
1108
1109=== added file 'src/maasserver/api/tests/test_storage.py'
1110--- src/maasserver/api/tests/test_storage.py 1970-01-01 00:00:00 +0000
1111+++ src/maasserver/api/tests/test_storage.py 2016-12-07 15:50:52 +0000
1112@@ -0,0 +1,125 @@
1113+# Copyright 2016 Canonical Ltd. This software is licensed under the
1114+# GNU Affero General Public License version 3 (see the file LICENSE).
1115+
1116+"""Tests for storage API."""
1117+
1118+__all__ = []
1119+
1120+import http.client
1121+
1122+from django.core.urlresolvers import reverse
1123+from maasserver.enum import (
1124+ NODE_STATUS,
1125+ NODE_TYPE,
1126+)
1127+from maasserver.testing.api import APITestCase
1128+from maasserver.testing.factory import factory
1129+from maasserver.utils.converters import json_load_bytes
1130+from maasserver.utils.orm import reload_object
1131+
1132+
1133+class TestStoragesAPI(APITestCase.ForUser):
1134+
1135+ def test_handler_path(self):
1136+ self.assertEqual(
1137+ '/api/2.0/storages/', reverse('storages_handler'))
1138+
1139+ def create_storages(self, owner, nb=3):
1140+ return [
1141+ factory.make_Node(
1142+ interface=True, node_type=NODE_TYPE.STORAGE, owner=owner)
1143+ for _ in range(nb)
1144+ ]
1145+
1146+ def test_read_lists_storage(self):
1147+ # The api allows for fetching the list of storages.
1148+ storages = self.create_storages(owner=self.user)
1149+ factory.make_Node(
1150+ status=NODE_STATUS.ALLOCATED, owner=self.user)
1151+ response = self.client.get(reverse('storages_handler'))
1152+ parsed_result = json_load_bytes(response.content)
1153+
1154+ self.assertEqual(http.client.OK, response.status_code)
1155+ self.assertItemsEqual(
1156+ [storage.system_id for storage in storages],
1157+ [storage.get('system_id') for storage in parsed_result])
1158+
1159+ def test_read_ignores_nodes(self):
1160+ factory.make_Node(
1161+ status=NODE_STATUS.ALLOCATED, owner=self.user)
1162+ response = self.client.get(reverse('storages_handler'))
1163+ parsed_result = json_load_bytes(response.content)
1164+
1165+ self.assertEqual(http.client.OK, response.status_code)
1166+ self.assertEqual(
1167+ [],
1168+ [storage.get('system_id') for storage in parsed_result])
1169+
1170+ def test_read_with_id_returns_matching_storage(self):
1171+ # The "list" operation takes optional "id" parameters. Only
1172+ # storages with matching ids will be returned.
1173+ storages = self.create_storages(owner=self.user)
1174+ ids = [storage.system_id for storage in storages]
1175+ matching_id = ids[0]
1176+ response = self.client.get(reverse('storages_handler'), {
1177+ 'id': [matching_id],
1178+ })
1179+ parsed_result = json_load_bytes(response.content)
1180+ self.assertItemsEqual(
1181+ [matching_id],
1182+ [storage.get('system_id') for storage in parsed_result])
1183+
1184+ def test_read_returns_limited_fields(self):
1185+ self.create_storages(owner=self.user)
1186+ response = self.client.get(reverse('storages_handler'))
1187+ parsed_result = json_load_bytes(response.content)
1188+ self.assertItemsEqual(
1189+ [
1190+ 'hostname',
1191+ 'system_id',
1192+ 'storage_type',
1193+ 'node_type',
1194+ 'node_type_name',
1195+ 'resource_uri',
1196+ ],
1197+ list(parsed_result[0]))
1198+
1199+
1200+def get_storage_uri(storage):
1201+ """Return a storage's URI on the API."""
1202+ return reverse('storage_handler', args=[storage.system_id])
1203+
1204+
1205+class TestStorageAPI(APITestCase.ForUser):
1206+
1207+ def test_handler_path(self):
1208+ system_id = factory.make_name('system-id')
1209+ self.assertEqual(
1210+ '/api/2.0/storages/%s/' % system_id,
1211+ reverse('storage_handler', args=[system_id]))
1212+
1213+ def test_GET_reads_storage(self):
1214+ storage = factory.make_Node(
1215+ node_type=NODE_TYPE.STORAGE, owner=self.user)
1216+
1217+ response = self.client.get(get_storage_uri(storage))
1218+ self.assertEqual(
1219+ http.client.OK, response.status_code, response.content)
1220+ parsed_storage = json_load_bytes(response.content)
1221+ self.assertEqual(storage.system_id, parsed_storage["system_id"])
1222+
1223+ def test_DELETE_removes_storage(self):
1224+ self.become_admin()
1225+ storage = factory.make_Node(
1226+ node_type=NODE_TYPE.STORAGE, owner=self.user)
1227+ response = self.client.delete(get_storage_uri(storage))
1228+ self.assertEqual(
1229+ http.client.NO_CONTENT, response.status_code, response.content)
1230+ self.assertIsNone(reload_object(storage))
1231+
1232+ def test_DELETE_rejects_deletion_if_not_permitted(self):
1233+ storage = factory.make_Node(
1234+ node_type=NODE_TYPE.STORAGE, owner=factory.make_User())
1235+ response = self.client.delete(get_storage_uri(storage))
1236+ self.assertEqual(http.client.FORBIDDEN, response.status_code)
1237+ self.assertEqual(storage, reload_object(storage))
1238
1239=== modified file 'src/maasserver/api/tests/test_subnets.py'
1240--- src/maasserver/api/tests/test_subnets.py 2016-10-20 21:30:58 +0000
1241+++ src/maasserver/api/tests/test_subnets.py 2016-12-07 15:50:52 +0000
1242@@ -87,6 +87,7 @@
1243 rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)
1244 allow_proxy = factory.pick_bool()
1245 gateway_ip = factory.pick_ip_in_network(network)
1246+ managed = factory.pick_bool()
1247 dns_servers = []
1248 for _ in range(2):
1249 dns_servers.append(
1250@@ -102,6 +103,7 @@
1251 "dns_servers": ','.join(dns_servers),
1252 "rdns_mode": rdns_mode,
1253 "allow_proxy": allow_proxy,
1254+ "managed": managed,
1255 })
1256 self.assertEqual(
1257 http.client.OK, response.status_code, response.content)
1258@@ -115,6 +117,7 @@
1259 self.assertEqual(dns_servers, created_subnet['dns_servers'])
1260 self.assertEqual(rdns_mode, created_subnet['rdns_mode'])
1261 self.assertEqual(allow_proxy, created_subnet['allow_proxy'])
1262+ self.assertEqual(managed, created_subnet['managed'])
1263
1264 def test_create_defaults_to_allow_proxy(self):
1265 self.become_admin()
1266@@ -151,7 +154,43 @@
1267 self.assertEqual(gateway_ip, created_subnet['gateway_ip'])
1268 self.assertEqual(dns_servers, created_subnet['dns_servers'])
1269 self.assertEqual(rdns_mode, created_subnet['rdns_mode'])
1270- self.assertEqual(True, created_subnet['allow_proxy'])
1271+
1272+ def test_create_defaults_to_managed(self):
1273+ self.become_admin()
1274+ subnet_name = factory.make_name("subnet")
1275+ vlan = factory.make_VLAN()
1276+ space = factory.make_Space()
1277+ network = factory.make_ip4_or_6_network()
1278+ cidr = str(network.cidr)
1279+ rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)
1280+ gateway_ip = factory.pick_ip_in_network(network)
1281+ dns_servers = []
1282+ for _ in range(2):
1283+ dns_servers.append(
1284+ factory.pick_ip_in_network(
1285+ network, but_not=[gateway_ip] + dns_servers))
1286+ uri = get_subnets_uri()
1287+ response = self.client.post(uri, {
1288+ "name": subnet_name,
1289+ "vlan": vlan.id,
1290+ "space": space.id,
1291+ "cidr": cidr,
1292+ "gateway_ip": gateway_ip,
1293+ "dns_servers": ','.join(dns_servers),
1294+ "rdns_mode": rdns_mode,
1295+ })
1296+ self.assertEqual(
1297+ http.client.OK, response.status_code, response.content)
1298+ created_subnet = json.loads(
1299+ response.content.decode(settings.DEFAULT_CHARSET))
1300+ self.assertEqual(subnet_name, created_subnet['name'])
1301+ self.assertEqual(vlan.vid, created_subnet['vlan']['vid'])
1302+ self.assertEqual(space.get_name(), created_subnet['space'])
1303+ self.assertEqual(cidr, created_subnet['cidr'])
1304+ self.assertEqual(gateway_ip, created_subnet['gateway_ip'])
1305+ self.assertEqual(dns_servers, created_subnet['dns_servers'])
1306+ self.assertEqual(rdns_mode, created_subnet['rdns_mode'])
1307+ self.assertEqual(True, created_subnet['managed'])
1308
1309 def test_create_admin_only(self):
1310 subnet_name = factory.make_name("subnet")
1311@@ -200,6 +239,7 @@
1312 "cidr": Equals(subnet.cidr),
1313 "gateway_ip": Equals(subnet.gateway_ip),
1314 "dns_servers": Equals(subnet.dns_servers),
1315+ "managed": Equals(subnet.managed),
1316 }))
1317
1318 def test_read_404_when_bad_id(self):
1319@@ -232,20 +272,24 @@
1320 new_name = factory.make_name("subnet")
1321 new_rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)
1322 new_allow_proxy = factory.pick_bool()
1323+ new_managed = factory.pick_bool()
1324 uri = get_subnet_uri(subnet)
1325 response = self.client.put(uri, {
1326 "name": new_name,
1327 "rdns_mode": new_rdns_mode,
1328 "allow_proxy": new_allow_proxy,
1329+ "managed": new_managed,
1330 })
1331 self.assertEqual(
1332 http.client.OK, response.status_code, response.content)
1333 self.assertEqual(
1334 new_name, json.loads(
1335 response.content.decode(settings.DEFAULT_CHARSET))['name'])
1336- self.assertEqual(new_name, reload_object(subnet).name)
1337- self.assertEqual(new_rdns_mode, reload_object(subnet).rdns_mode)
1338- self.assertEqual(new_allow_proxy, reload_object(subnet).allow_proxy)
1339+ subnet = reload_object(subnet)
1340+ self.assertEqual(new_name, subnet.name)
1341+ self.assertEqual(new_rdns_mode, subnet.rdns_mode)
1342+ self.assertEqual(new_allow_proxy, subnet.allow_proxy)
1343+ self.assertEqual(new_managed, subnet.managed)
1344
1345 def test_update_admin_only(self):
1346 subnet = factory.make_Subnet()
1347
1348=== modified file 'src/maasserver/api/tests/test_vlans.py'
1349--- src/maasserver/api/tests/test_vlans.py 2016-05-24 21:29:53 +0000
1350+++ src/maasserver/api/tests/test_vlans.py 2016-12-07 15:50:52 +0000
1351@@ -84,6 +84,29 @@
1352 self.assertEqual(vid, response_data['vid'])
1353 self.assertEqual(mtu, response_data['mtu'])
1354
1355+ def test_create_with_relay_vlan(self):
1356+ self.become_admin()
1357+ fabric = factory.make_Fabric()
1358+ vlan_name = factory.make_name("fabric")
1359+ vid = random.randint(1, 1000)
1360+ mtu = random.randint(552, 1500)
1361+ relay_vlan = factory.make_VLAN()
1362+ uri = get_vlans_uri(fabric)
1363+ response = self.client.post(uri, {
1364+ "name": vlan_name,
1365+ "vid": vid,
1366+ "mtu": mtu,
1367+ "relay_vlan": relay_vlan.id,
1368+ })
1369+ self.assertEqual(
1370+ http.client.OK, response.status_code, response.content)
1371+ response_data = json.loads(
1372+ response.content.decode(settings.DEFAULT_CHARSET))
1373+ self.assertEqual(vlan_name, response_data['name'])
1374+ self.assertEqual(vid, response_data['vid'])
1375+ self.assertEqual(mtu, response_data['mtu'])
1376+ self.assertEqual(relay_vlan.vid, response_data['relay_vlan']['vid'])
1377+
1378 def test_create_admin_only(self):
1379 fabric = factory.make_Fabric()
1380 vlan_name = factory.make_name("fabric")
1381@@ -182,6 +205,23 @@
1382 self.assertEqual(new_vid, parsed_vlan['vid'])
1383 self.assertEqual(new_vid, vlan.vid)
1384
1385+ def test_update_sets_relay_vlan(self):
1386+ self.become_admin()
1387+ fabric = factory.make_Fabric()
1388+ vlan = factory.make_VLAN(fabric=fabric)
1389+ uri = get_vlan_uri(vlan)
1390+ relay_vlan = factory.make_VLAN()
1391+ response = self.client.put(uri, {
1392+ "relay_vlan": relay_vlan.id,
1393+ })
1394+ self.assertEqual(
1395+ http.client.OK, response.status_code, response.content)
1396+ parsed_vlan = json.loads(
1397+ response.content.decode(settings.DEFAULT_CHARSET))
1398+ vlan = reload_object(vlan)
1399+ self.assertEqual(relay_vlan.vid, parsed_vlan['relay_vlan']['vid'])
1400+ self.assertEqual(relay_vlan, vlan.relay_vlan)
1401+
1402 def test_update_with_fabric(self):
1403 self.become_admin()
1404 fabric = factory.make_Fabric()
1405
1406=== modified file 'src/maasserver/api/vlans.py'
1407--- src/maasserver/api/vlans.py 2016-04-27 20:40:24 +0000
1408+++ src/maasserver/api/vlans.py 2016-12-07 15:50:52 +0000
1409@@ -26,6 +26,7 @@
1410 'secondary_rack',
1411 'dhcp_on',
1412 'external_dhcp',
1413+ 'relay_vlan',
1414 )
1415
1416
1417@@ -165,12 +166,18 @@
1418 :type vid: integer
1419 :param mtu: The MTU to use on the VLAN.
1420 :type mtu: integer
1421- :Param dhcp_on: Whether or not DHCP should be managed on the VLAN.
1422+ :param dhcp_on: Whether or not DHCP should be managed on the VLAN.
1423 :type dhcp_on: boolean
1424 :param primary_rack: The primary rack controller managing the VLAN.
1425 :type primary_rack: system_id
1426 :param secondary_rack: The secondary rack controller manging the VLAN.
1427 :type secondary_rack: system_id
1428+ :param relay_vlan: Only set when this VLAN will be using a DHCP relay
1429+ to forward DHCP requests to another VLAN that MAAS is or will run
1430+ the DHCP server. MAAS will not run the DHCP relay itself, it must
1431+ be configured to proxy reqests to the primary and/or secondary
1432+ rack controller interfaces for the VLAN specified in this field.
1433+ :type relay_vlan: ID of VLAN
1434
1435 Returns 404 if the fabric or VLAN is not found.
1436 """
1437
1438=== modified file 'src/maasserver/bootresources.py'
1439--- src/maasserver/bootresources.py 2016-10-28 15:58:32 +0000
1440+++ src/maasserver/bootresources.py 2016-12-07 15:50:52 +0000
1441@@ -60,6 +60,7 @@
1442 BootResourceSet,
1443 BootSourceSelection,
1444 Config,
1445+ Event,
1446 LargeFile,
1447 )
1448 from maasserver.rpc import getAllClients
1449@@ -78,6 +79,7 @@
1450 from maasserver.utils.threads import deferToDatabase
1451 from maasserver.utils.version import get_maas_version_ui
1452 from provisioningserver.config import is_dev_environment
1453+from provisioningserver.events import EVENT_TYPES
1454 from provisioningserver.import_images.download_descriptions import (
1455 download_all_image_descriptions,
1456 image_passes_filter,
1457@@ -661,10 +663,13 @@
1458 # not allowed.
1459 prev_largefile = largefile
1460 largefile = None
1461- maaslog.warning(
1462+ msg = (
1463 "Hash mismatch for prev_file=%s resourceset=%s "
1464- "resource=%s",
1465- prev_largefile, resource_set, resource)
1466+ "resource=%s" % (prev_largefile, resource_set, resource)
1467+ )
1468+ Event.objects.create_region_event(
1469+ EVENT_TYPES.REGION_IMPORT_WARNING, msg)
1470+ maaslog.warning(msg)
1471
1472 if largefile is None:
1473 # The resource file current does not have a largefile linked. Lets
1474@@ -695,8 +700,10 @@
1475 is_resource_initially_complete and
1476 resource.get_latest_complete_set() is None)
1477 if is_resource_broken:
1478- maaslog.error(
1479- "Resource %s has no complete resource set!", resource)
1480+ msg = "Resource %s has no complete resource set!" % resource
1481+ Event.objects.create_region_event(
1482+ EVENT_TYPES.REGION_IMPORT_ERROR, msg)
1483+ maaslog.error(msg)
1484
1485 if prev_largefile is not None:
1486 # If the previous largefile had a miss matching sha256 then it
1487@@ -773,11 +780,15 @@
1488 # Calculated sha256 hash from the data does not match, what
1489 # simplestreams is telling us it should be. This resource file
1490 # will be deleted since it is corrupt.
1491- maaslog.error(
1492+ msg = (
1493 "Failed to finalize boot image %s. Unexpected "
1494- "checksum '%s' (found: %s expected: %s)",
1495- ident, cksummer.algorithm,
1496- cksummer.hexdigest(), cksummer.expected)
1497+ "checksum '%s' (found: %s expected: %s)" %
1498+ (
1499+ ident, cksummer.algorithm, cksummer.hexdigest(),
1500+ cksummer.expected))
1501+ Event.objects.create_region_event(
1502+ EVENT_TYPES.REGION_IMPORT_ERROR, msg)
1503+ maaslog.error(msg)
1504 transactional(rfile.delete)()
1505 else:
1506 maaslog.debug('Finalized boot image %s.', ident)
1507@@ -877,11 +888,15 @@
1508 self.get_resource_identity(delete_resource))
1509 delete_resource.delete()
1510 else:
1511- maaslog.info(
1512+ msg = (
1513 "Boot image %s no longer exists in stream, but "
1514 "remains in selections. To delete this image "
1515- "remove its selection.",
1516- self.get_resource_identity(delete_resource))
1517+ "remove its selection." %
1518+ self.get_resource_identity(delete_resource)
1519+ )
1520+ Event.objects.create_region_event(
1521+ EVENT_TYPES.REGION_IMPORT_INFO, msg)
1522+ maaslog.info(msg)
1523 else:
1524 # No resource set on the boot resource so it should be
1525 # removed as it has not files.
1526@@ -960,6 +975,8 @@
1527 "Finalization of imported images skipped, "
1528 "or all %s synced images would be deleted." % (
1529 self._resources_to_delete))
1530+ Event.objects.create_region_event(
1531+ EVENT_TYPES.REGION_IMPORT_ERROR, error_msg)
1532 maaslog.error(error_msg)
1533 if notify is not None:
1534 failure = Failure(Exception(error_msg))
1535@@ -1192,7 +1209,10 @@
1536
1537 # Download all of the metadata first.
1538 for source in sources:
1539- maaslog.info("Importing images from source: %s", source['url'])
1540+ msg = "Importing images from source: %s" % source['url']
1541+ Event.objects.create_region_event(
1542+ EVENT_TYPES.REGION_IMPORT_INFO, msg)
1543+ maaslog.info(msg)
1544 download_boot_resources(
1545 source['url'], store, product_mapping,
1546 keyring_file=source.get('keyring'))
1547@@ -1318,15 +1338,21 @@
1548 with tempdir('keyrings') as keyrings_path:
1549 sources = get_boot_sources()
1550 sources = write_all_keyrings(keyrings_path, sources)
1551- maaslog.info(
1552- "Started importing of boot images from %d source(s).",
1553+ msg = (
1554+ "Started importing of boot images from %d source(s)." %
1555 len(sources))
1556+ Event.objects.create_region_event(EVENT_TYPES.REGION_IMPORT_INFO, msg)
1557+ maaslog.info(msg)
1558
1559 image_descriptions = download_all_image_descriptions(sources)
1560 if image_descriptions.is_empty():
1561- maaslog.warning(
1562+ msg = (
1563 "Unable to import boot images, no image "
1564- "descriptions avaliable.")
1565+ "descriptions avaliable."
1566+ )
1567+ Event.objects.create_region_event(
1568+ EVENT_TYPES.REGION_IMPORT_WARNING, msg)
1569+ maaslog.warning(msg)
1570 return
1571 product_mapping = map_products(image_descriptions)
1572
1573
1574=== modified file 'src/maasserver/clusterrpc/power_parameters.py'
1575--- src/maasserver/clusterrpc/power_parameters.py 2016-10-20 19:41:25 +0000
1576+++ src/maasserver/clusterrpc/power_parameters.py 2016-12-07 15:50:52 +0000
1577@@ -14,7 +14,7 @@
1578 power type with a set of power parameters.
1579
1580 The power types are retrieved from the cluster controllers using the json
1581-schema provisioningserver.power_schema.JSON_POWER_TYPE_SCHEMA. To add new
1582+schema provisioningserver.drivers.power.JSON_POWER_DRIVERS_SCHEMA. To add new
1583 parameters requires changes to hardware drivers that run in the cluster
1584 controllers.
1585 """
1586@@ -33,10 +33,8 @@
1587 from maasserver.config_forms import DictCharField
1588 from maasserver.fields import MACAddressFormField
1589 from maasserver.utils.forms import compose_invalid_choice_text
1590-from provisioningserver.power.schema import (
1591- JSON_POWER_TYPE_SCHEMA,
1592- POWER_TYPE_PARAMETER_FIELD_SCHEMA,
1593-)
1594+from provisioningserver.drivers import SETTING_PARAMETER_FIELD_SCHEMA
1595+from provisioningserver.drivers.power import JSON_POWER_DRIVERS_SCHEMA
1596 from provisioningserver.rpc import cluster
1597
1598
1599@@ -93,10 +91,10 @@
1600 :type description: string
1601 :param fields: The fields that make up the parameters for the power
1602 type. Will be validated against
1603- POWER_TYPE_PARAMETER_FIELD_SCHEMA.
1604+ SETTING_PARAMETER_FIELD_SCHEMA.
1605 :param missing_packages: System packages that must be installed on
1606 the cluster before the power type can be used.
1607- :type fields: list of `make_json_field` results.
1608+ :type fields: list of `make_setting_field` results.
1609 :param parameters_set: An existing list of power type parameters to
1610 mutate.
1611 :type parameters_set: list
1612@@ -107,7 +105,7 @@
1613 field_set_schema = {
1614 'title': "Power type parameters field set schema",
1615 'type': 'array',
1616- 'items': POWER_TYPE_PARAMETER_FIELD_SCHEMA,
1617+ 'items': SETTING_PARAMETER_FIELD_SCHEMA,
1618 }
1619 validate(fields, field_set_schema)
1620 parameters_set.append(
1621@@ -132,7 +130,7 @@
1622 :return: A dict of power parameters for all power types, indexed by
1623 power type name.
1624 """
1625- validate(json_power_type_parameters, JSON_POWER_TYPE_SCHEMA)
1626+ validate(json_power_type_parameters, JSON_POWER_DRIVERS_SCHEMA)
1627 power_parameters = {
1628 # Empty type, for the case where nothing is entered in the form yet.
1629 '': DictCharField(
1630@@ -197,7 +195,7 @@
1631 """Query every cluster controller and obtain all known power types.
1632
1633 :return: a list of power types matching the schema
1634- provisioningserver.power_schema.JSON_POWER_TYPE_PARAMETERS_SCHEMA
1635+ provisioningserver.drivers.power.JSON_POWER_DRIVERS_SCHEMA
1636 """
1637 merged_types = []
1638 responses = call_clusters(
1639
1640=== modified file 'src/maasserver/clusterrpc/testing/power_parameters.py'
1641--- src/maasserver/clusterrpc/testing/power_parameters.py 2016-06-22 17:03:02 +0000
1642+++ src/maasserver/clusterrpc/testing/power_parameters.py 2016-12-07 15:50:52 +0000
1643@@ -11,7 +11,7 @@
1644
1645 from fixtures import Fixture
1646 from maasserver.clusterrpc import power_parameters
1647-from provisioningserver.power import schema
1648+from provisioningserver.drivers.power import PowerDriverRegistry
1649 from testtools import monkey
1650
1651
1652@@ -26,7 +26,9 @@
1653 super(StaticPowerTypesFixture, self).setUp()
1654 # This patch prevents communication with a non-existent cluster
1655 # controller when fetching power types.
1656+ power_types = PowerDriverRegistry.get_schema(
1657+ detect_missing_packages=False)
1658 restore = monkey.patch(
1659 power_parameters, 'get_all_power_types_from_clusters',
1660- Mock(return_value=schema.JSON_POWER_TYPE_PARAMETERS))
1661+ Mock(return_value=power_types))
1662 self.addCleanup(restore)
1663
1664=== modified file 'src/maasserver/clusterrpc/tests/test_power_parameters.py'
1665--- src/maasserver/clusterrpc/tests/test_power_parameters.py 2016-10-20 08:41:30 +0000
1666+++ src/maasserver/clusterrpc/tests/test_power_parameters.py 2016-12-07 15:50:52 +0000
1667@@ -14,9 +14,9 @@
1668 add_power_type_parameters,
1669 get_power_type_parameters_from_json,
1670 get_power_types,
1671- JSON_POWER_TYPE_SCHEMA,
1672+ JSON_POWER_DRIVERS_SCHEMA,
1673 make_form_field,
1674- POWER_TYPE_PARAMETER_FIELD_SCHEMA,
1675+ SETTING_PARAMETER_FIELD_SCHEMA,
1676 )
1677 from maasserver.config_forms import DictCharField
1678 from maasserver.fields import MACAddressFormField
1679@@ -25,7 +25,7 @@
1680 from maasserver.utils.forms import compose_invalid_choice_text
1681 from maastesting.matchers import MockCalledOnceWith
1682 from maastesting.testcase import MAASTestCase
1683-from provisioningserver.power.schema import make_json_field
1684+from provisioningserver.drivers import make_setting_field
1685
1686
1687 class TestGetPowerTypeParametersFromJSON(MAASServerTestCase):
1688@@ -186,15 +186,15 @@
1689 self.assertEquals(json_field['default'], django_field.initial)
1690
1691
1692-class TestMakeJSONField(MAASServerTestCase):
1693- """Test that make_json_field() creates JSON-verifiable fields."""
1694+class TestMakeSettingField(MAASServerTestCase):
1695+ """Test that make_setting_field() creates JSON-verifiable fields."""
1696
1697 def test__returns_json_verifiable_dict(self):
1698- json_field = make_json_field('some_field', 'Some Label')
1699- jsonschema.validate(json_field, POWER_TYPE_PARAMETER_FIELD_SCHEMA)
1700+ json_field = make_setting_field('some_field', 'Some Label')
1701+ jsonschema.validate(json_field, SETTING_PARAMETER_FIELD_SCHEMA)
1702
1703 def test__provides_sane_default_values(self):
1704- json_field = make_json_field('some_field', 'Some Label')
1705+ json_field = make_setting_field('some_field', 'Some Label')
1706 expected_field = {
1707 'name': 'some_field',
1708 'label': 'Some Label',
1709@@ -219,16 +219,16 @@
1710 'default': 'spam',
1711 'scope': 'bmc',
1712 }
1713- json_field = make_json_field(**expected_field)
1714+ json_field = make_setting_field(**expected_field)
1715 self.assertEqual(expected_field, json_field)
1716
1717 def test__validates_choices(self):
1718 self.assertRaises(
1719- jsonschema.ValidationError, make_json_field,
1720+ jsonschema.ValidationError, make_setting_field,
1721 'some_field', 'Some Label', choices="Nonsense")
1722
1723 def test__creates_password_fields(self):
1724- json_field = make_json_field(
1725+ json_field = make_setting_field(
1726 'some_field', 'Some Label', field_type='password')
1727 expected_field = {
1728 'name': 'some_field',
1729@@ -245,7 +245,7 @@
1730 class TestAddPowerTypeParameters(MAASServerTestCase):
1731
1732 def make_field(self):
1733- return make_json_field(
1734+ return make_setting_field(
1735 self.getUniqueString(), self.getUniqueString())
1736
1737 def test_adding_existing_types_is_a_no_op(self):
1738@@ -289,7 +289,7 @@
1739 missing_packages=[],
1740 parameters_set=parameters_set)
1741 jsonschema.validate(
1742- parameters_set, JSON_POWER_TYPE_SCHEMA)
1743+ parameters_set, JSON_POWER_DRIVERS_SCHEMA)
1744
1745
1746 class TestPowerTypes(MAASTestCase):
1747
1748=== modified file 'src/maasserver/dhcp.py'
1749--- src/maasserver/dhcp.py 2016-11-01 16:46:19 +0000
1750+++ src/maasserver/dhcp.py 2016-12-07 15:50:52 +0000
1751@@ -29,10 +29,7 @@
1752 IPRANGE_TYPE,
1753 SERVICE_STATUS,
1754 )
1755-from maasserver.exceptions import (
1756- DHCPConfigurationError,
1757- UnresolvableHost,
1758-)
1759+from maasserver.exceptions import UnresolvableHost
1760 from maasserver.models import (
1761 Config,
1762 DHCPSnippet,
1763@@ -40,6 +37,7 @@
1764 RackController,
1765 Service,
1766 StaticIPAddress,
1767+ Subnet,
1768 )
1769 from maasserver.rpc import (
1770 getAllClients,
1771@@ -88,14 +86,14 @@
1772 return key
1773
1774
1775-def split_ipv4_ipv6_subnets(subnets):
1776+def split_managed_ipv4_ipv6_subnets(subnets: Iterable[Subnet]):
1777 """Divide `subnets` into IPv4 ones and IPv6 ones.
1778
1779 :param subnets: A sequence of subnets.
1780 :return: A tuple of two separate sequences: IPv4 subnets and IPv6 subnets.
1781 """
1782 split = defaultdict(list)
1783- for subnet in subnets:
1784+ for subnet in (s for s in subnets if s.managed is True):
1785 split[subnet.get_ipnetwork().version].append(subnet)
1786 assert len(split) <= 2, (
1787 "Unexpected IP version(s): %s" % ', '.join(list(split.keys())))
1788@@ -193,18 +191,19 @@
1789 return []
1790
1791
1792-def get_managed_vlans_for(rack_controller):
1793- """Return list of `VLAN` for the `rack_controller` when DHCP is enabled and
1794+def gen_managed_vlans_for(rack_controller):
1795+ """Yeilds each `VLAN` for the `rack_controller` when DHCP is enabled and
1796 `rack_controller` is either the `primary_rack` or the `secondary_rack`.
1797 """
1798 interfaces = rack_controller.interface_set.filter(
1799 Q(vlan__dhcp_on=True) & (
1800 Q(vlan__primary_rack=rack_controller) |
1801- Q(vlan__secondary_rack=rack_controller))).select_related("vlan")
1802- return {
1803- interface.vlan
1804- for interface in interfaces
1805- }
1806+ Q(vlan__secondary_rack=rack_controller)))
1807+ interfaces = interfaces.prefetch_related("vlan__relay_vlans")
1808+ for interface in interfaces:
1809+ yield interface.vlan
1810+ for relayed_vlan in interface.vlan.relay_vlans.all():
1811+ yield relayed_vlan
1812
1813
1814 def ip_is_on_vlan(ip_address, vlan):
1815@@ -459,12 +458,6 @@
1816 interfaces = get_interfaces_with_ip_on_vlan(
1817 rack_controller, vlan, ip_version)
1818 interface = get_best_interface(interfaces)
1819- if interface is None:
1820- raise DHCPConfigurationError(
1821- "No IPv%d interface on rack controller '%s' has an IP address on "
1822- "any subnet on VLAN '%s.%d'." % (
1823- ip_version, rack_controller.hostname, vlan.fabric.name,
1824- vlan.vid))
1825
1826 # Generate the failover peer for this VLAN.
1827 if vlan.secondary_rack_id is not None:
1828@@ -496,7 +489,7 @@
1829 hosts = make_hosts_for_subnets(subnets, nodes_dhcp_snippets)
1830 return (
1831 peer_config, sorted(subnet_configs, key=itemgetter("subnet")),
1832- hosts, interface.name)
1833+ hosts, None if interface is None else interface.name)
1834
1835
1836 @synchronous
1837@@ -505,11 +498,11 @@
1838 """Return tuple with IPv4 and IPv6 configurations for the
1839 rack controller."""
1840 # Get list of all vlans that are being managed by the rack controller.
1841- vlans = get_managed_vlans_for(rack_controller)
1842+ vlans = gen_managed_vlans_for(rack_controller)
1843
1844 # Group the subnets on each VLAN into IPv4 and IPv6 subnets.
1845 vlan_subnets = {
1846- vlan: split_ipv4_ipv6_subnets(vlan.subnet_set.all())
1847+ vlan: split_managed_ipv4_ipv6_subnets(vlan.subnet_set.all())
1848 for vlan in vlans
1849 }
1850
1851@@ -561,52 +554,40 @@
1852 for vlan, (subnets_v4, subnets_v6) in vlan_subnets.items():
1853 # IPv4
1854 if len(subnets_v4) > 0:
1855- try:
1856- config = get_dhcp_configure_for(
1857- 4, rack_controller, vlan, subnets_v4, ntp_servers,
1858- default_domain, dhcp_snippets)
1859- except DHCPConfigurationError:
1860- # XXX bug #1602412: this silently breaks DHCPv4, but we cannot
1861- # allow it to crash here since DHCPv6 might be able to run.
1862- # This error may be irrelevant if there is an IPv4 network in
1863- # the MAAS model which is not configured on the rack, and the
1864- # user only wants to serve DHCPv6. But it is still something
1865- # worth noting, so log it and continue.
1866- log.err(None, "Failure configuring DHCPv4.")
1867- else:
1868- failover_peer, subnets, hosts, interface = config
1869- if failover_peer is not None:
1870- failover_peers_v4.append(failover_peer)
1871- shared_networks_v4.append({
1872- "name": "vlan-%d" % vlan.id,
1873- "subnets": subnets,
1874- })
1875- hosts_v4.extend(hosts)
1876+ config = get_dhcp_configure_for(
1877+ 4, rack_controller, vlan, subnets_v4, ntp_servers,
1878+ default_domain, dhcp_snippets)
1879+ failover_peer, subnets, hosts, interface = config
1880+ if failover_peer is not None:
1881+ failover_peers_v4.append(failover_peer)
1882+ shared_networks_v4.append({
1883+ "name": "vlan-%d" % vlan.id,
1884+ "subnets": subnets,
1885+ })
1886+ hosts_v4.extend(hosts)
1887+ if interface is not None:
1888 interfaces_v4.add(interface)
1889 # IPv6
1890 if len(subnets_v6) > 0:
1891- try:
1892- config = get_dhcp_configure_for(
1893- 6, rack_controller, vlan, subnets_v6,
1894- ntp_servers, default_domain, dhcp_snippets)
1895- except DHCPConfigurationError:
1896- # XXX bug #1602412: this silently breaks DHCPv6, but we cannot
1897- # allow it to crash here since DHCPv4 might be able to run.
1898- # This error may be irrelevant if there is an IPv6 network in
1899- # the MAAS model which is not configured on the rack, and the
1900- # user only wants to serve DHCPv4. But it is still something
1901- # worth noting, so log it and continue.
1902- log.err(None, "Failure configuring DHCPv6.")
1903- else:
1904- failover_peer, subnets, hosts, interface = config
1905- if failover_peer is not None:
1906- failover_peers_v6.append(failover_peer)
1907- shared_networks_v6.append({
1908- "name": "vlan-%d" % vlan.id,
1909- "subnets": subnets,
1910- })
1911- hosts_v6.extend(hosts)
1912+ config = get_dhcp_configure_for(
1913+ 6, rack_controller, vlan, subnets_v6,
1914+ ntp_servers, default_domain, dhcp_snippets)
1915+ failover_peer, subnets, hosts, interface = config
1916+ if failover_peer is not None:
1917+ failover_peers_v6.append(failover_peer)
1918+ shared_networks_v6.append({
1919+ "name": "vlan-%d" % vlan.id,
1920+ "subnets": subnets,
1921+ })
1922+ hosts_v6.extend(hosts)
1923+ if interface is not None:
1924 interfaces_v6.add(interface)
1925+ # When no interfaces exist for each IP version clear the shared networks
1926+ # as DHCP server cannot be started and needs to be stopped.
1927+ if len(interfaces_v4) == 0:
1928+ shared_networks_v4 = {}
1929+ if len(interfaces_v6) == 0:
1930+ shared_networks_v6 = {}
1931 return DHCPConfigurationForRack(
1932 failover_peers_v4, shared_networks_v4, hosts_v4, interfaces_v4,
1933 failover_peers_v6, shared_networks_v6, hosts_v6, interfaces_v6,
1934
1935=== renamed directory 'src/maas' => 'src/maasserver/djangosettings'
1936=== modified file 'src/maasserver/djangosettings/demo.py'
1937--- src/maas/demo.py 2016-06-07 19:59:49 +0000
1938+++ src/maasserver/djangosettings/demo.py 2016-12-07 15:50:52 +0000
1939@@ -5,7 +5,7 @@
1940
1941 from os.path import abspath
1942
1943-from maas import (
1944+from maasserver.djangosettings import (
1945 development,
1946 import_settings,
1947 settings,
1948
1949=== modified file 'src/maasserver/djangosettings/development.py'
1950--- src/maas/development.py 2016-10-18 11:21:26 +0000
1951+++ src/maasserver/djangosettings/development.py 2016-12-07 15:50:52 +0000
1952@@ -7,7 +7,7 @@
1953 from os.path import abspath
1954
1955 from formencode.validators import StringBool
1956-from maas import (
1957+from maasserver.djangosettings import (
1958 fix_up_databases,
1959 import_settings,
1960 settings,
1961
1962=== modified file 'src/maasserver/djangosettings/settings.py'
1963--- src/maas/settings.py 2016-11-22 00:53:43 +0000
1964+++ src/maasserver/djangosettings/settings.py 2016-12-07 15:50:52 +0000
1965@@ -6,9 +6,9 @@
1966 import os
1967
1968 import django.template.base
1969-from maas import fix_up_databases
1970-from maas.monkey import patch_get_script_prefix
1971 from maasserver.config import RegionConfiguration
1972+from maasserver.djangosettings import fix_up_databases
1973+from maasserver.djangosettings.monkey import patch_get_script_prefix
1974
1975
1976 def _read_timezone(tzfilename='/etc/timezone'):
1977@@ -265,7 +265,7 @@
1978
1979 )
1980
1981-ROOT_URLCONF = 'maas.urls'
1982+ROOT_URLCONF = 'maasserver.djangosettings.urls'
1983
1984 TEMPLATE_DIRS = (
1985 # Put strings here, like "/home/html/django_templates"
1986
1987=== renamed file 'src/maas/tests/test_maas.py' => 'src/maasserver/djangosettings/tests/test_settings.py'
1988--- src/maas/tests/test_maas.py 2016-06-21 10:29:11 +0000
1989+++ src/maasserver/djangosettings/tests/test_settings.py 2016-12-07 15:50:52 +0000
1990@@ -10,11 +10,11 @@
1991
1992 from django.conf import settings
1993 from django.db import connections
1994-from maas import (
1995+from maasserver.djangosettings import (
1996 find_settings,
1997 import_settings,
1998 )
1999-from maas.settings import (
2000+from maasserver.djangosettings.settings import (
2001 _get_local_timezone,
2002 _read_timezone,
2003 )
2004
2005=== modified file 'src/maasserver/enum.py'
2006--- src/maasserver/enum.py 2016-09-08 17:26:54 +0000
2007+++ src/maasserver/enum.py 2016-12-07 15:50:52 +0000
2008@@ -154,6 +154,8 @@
2009 RACK_CONTROLLER = 2
2010 REGION_CONTROLLER = 3
2011 REGION_AND_RACK_CONTROLLER = 4
2012+ CHASSIS = 5
2013+ STORAGE = 6
2014
2015
2016 # This is copied in static/js/angular/controllers/subnet_details.js. If you
2017@@ -164,6 +166,8 @@
2018 (NODE_TYPE.RACK_CONTROLLER, "Rack controller"),
2019 (NODE_TYPE.REGION_CONTROLLER, "Region controller"),
2020 (NODE_TYPE.REGION_AND_RACK_CONTROLLER, "Region and rack controller"),
2021+ (NODE_TYPE.CHASSIS, "Chassis"),
2022+ (NODE_TYPE.STORAGE, "Storage"),
2023 )
2024
2025
2026
2027=== modified file 'src/maasserver/exceptions.py'
2028--- src/maasserver/exceptions.py 2016-03-28 13:54:47 +0000
2029+++ src/maasserver/exceptions.py 2016-12-07 15:50:52 +0000
2030@@ -199,7 +199,3 @@
2031 information.
2032 """
2033 api_error = int(http.client.SERVICE_UNAVAILABLE)
2034-
2035-
2036-class DHCPConfigurationError(MAASException):
2037- """Raised when the configuration of DHCP hits a problem."""
2038
2039=== modified file 'src/maasserver/forms_commission.py'
2040--- src/maasserver/forms_commission.py 2015-12-01 18:12:59 +0000
2041+++ src/maasserver/forms_commission.py 2016-12-07 15:50:52 +0000
2042@@ -9,7 +9,6 @@
2043
2044 from django import forms
2045 from django.core.exceptions import ValidationError
2046-from maasserver.enum import POWER_STATE
2047 from maasserver.node_action import compile_node_actions
2048
2049
2050@@ -36,10 +35,6 @@
2051 raise ValidationError(
2052 "Commission is not available because of the current state "
2053 "of the node.")
2054- if self.instance.power_state == POWER_STATE.ON:
2055- raise ValidationError(
2056- "Commission is not available because of the node is currently "
2057- "powered on.")
2058 return cleaned_data
2059
2060 def save(self):
2061
2062=== modified file 'src/maasserver/forms_subnet.py'
2063--- src/maasserver/forms_subnet.py 2016-09-23 01:32:02 +0000
2064+++ src/maasserver/forms_subnet.py 2016-12-07 15:50:52 +0000
2065@@ -43,6 +43,9 @@
2066 allow_proxy = forms.BooleanField(
2067 required=False)
2068
2069+ managed = forms.BooleanField(
2070+ required=False)
2071+
2072 class Meta:
2073 model = Subnet
2074 fields = (
2075@@ -56,6 +59,7 @@
2076 'rdns_mode',
2077 'active_discovery',
2078 'allow_proxy',
2079+ 'managed',
2080 )
2081
2082 def __init__(self, *args, **kwargs):
2083@@ -64,9 +68,12 @@
2084
2085 def clean(self):
2086 cleaned_data = super(SubnetForm, self).clean()
2087- # The default value for allow_proxy is True.
2088+ # The default value for 'allow_proxy' is True.
2089 if 'allow_proxy' not in self.data:
2090 cleaned_data['allow_proxy'] = True
2091+ # The default value for 'managed' is True.
2092+ if 'managed' not in self.data:
2093+ cleaned_data['managed'] = True
2094 # The ArrayField form has a bug which leaves out the first entry.
2095 if 'dns_servers' in self.data and self.data['dns_servers'] != '':
2096 cleaned_data['dns_servers'] = self.data.getlist('dns_servers')
2097
2098=== modified file 'src/maasserver/forms_vlan.py'
2099--- src/maasserver/forms_vlan.py 2016-04-27 20:38:06 +0000
2100+++ src/maasserver/forms_vlan.py 2016-12-07 15:50:52 +0000
2101@@ -31,6 +31,7 @@
2102 'dhcp_on',
2103 'primary_rack',
2104 'secondary_rack',
2105+ 'relay_vlan',
2106 )
2107
2108 def __init__(self, *args, **kwargs):
2109@@ -40,6 +41,7 @@
2110 if instance is None and self.fabric is None:
2111 raise ValueError("Form requires either a instance or a fabric.")
2112 self._set_up_rack_fields()
2113+ self._set_up_relay_vlan()
2114
2115 def _set_up_rack_fields(self):
2116 qs = RackController.objects.filter_by_vids([self.instance.vid])
2117@@ -61,6 +63,22 @@
2118 secondary_rack = RackController.objects.get(id=secondary_rack_id)
2119 self.initial['secondary_rack'] = secondary_rack.system_id
2120
2121+ def _set_up_relay_vlan(self):
2122+ # Configure the relay_vlan fields to include only VLAN's that are
2123+ # not already on a relay_vlan. If this is an update then it cannot
2124+ # be itself or never set when dhcp_on is True.
2125+ possible_relay_vlans = VLAN.objects.filter(relay_vlan__isnull=True)
2126+ if self.instance is not None:
2127+ possible_relay_vlans = possible_relay_vlans.exclude(
2128+ id=self.instance.id)
2129+ if self.instance.dhcp_on:
2130+ possible_relay_vlans = VLAN.objects.none()
2131+ if self.instance.relay_vlan is not None:
2132+ possible_relay_vlans = VLAN.objects.filter(
2133+ id=self.instance.relay_vlan.id)
2134+ self.fields['relay_vlan'] = forms.ModelChoiceField(
2135+ queryset=possible_relay_vlans, required=False)
2136+
2137 def clean(self):
2138 cleaned_data = super(VLANForm, self).clean()
2139 # Automatically promote the secondary rack controller to the primary
2140@@ -120,5 +138,12 @@
2141 interface = super(VLANForm, self).save(commit=False)
2142 if self.fabric is not None:
2143 interface.fabric = self.fabric
2144+ if ('relay_vlan' in self.data and
2145+ not self.cleaned_data.get('relay_vlan')):
2146+ # relay_vlan is being cleared.
2147+ interface.relay_vlan = None
2148+ if interface.dhcp_on:
2149+ # relay_vlan cannot be set when dhcp is on.
2150+ interface.relay_vlan = None
2151 interface.save()
2152 return interface
2153
2154=== modified file 'src/maasserver/locks.py'
2155--- src/maasserver/locks.py 2016-09-28 14:12:23 +0000
2156+++ src/maasserver/locks.py 2016-12-07 15:50:52 +0000
2157@@ -4,6 +4,7 @@
2158 """Region-wide locks."""
2159
2160 __all__ = [
2161+ "address_allocation",
2162 "dns",
2163 "eventloop",
2164 "import_images",
2165@@ -11,7 +12,6 @@
2166 "rack_registration",
2167 "security",
2168 "startup",
2169- "staticip_acquire",
2170 ]
2171
2172 from maasserver.utils.dblocks import (
2173@@ -38,8 +38,8 @@
2174 # Lock to prevent concurrent acquisition of nodes.
2175 node_acquire = DatabaseXactLock(7)
2176
2177-# Lock to prevent concurrent allocation of StaticIPAddress
2178-staticip_acquire = DatabaseXactLock(8)
2179+# Lock to help with concurrent allocation of IP addresses.
2180+address_allocation = DatabaseLock(8)
2181
2182 # Lock to prevent concurrent registration of rack controllers. This can be a
2183 # problem because registration involves populating fabrics, VLANs, and other
2184
2185=== modified file 'src/maasserver/management/commands/dbupgrade.py'
2186--- src/maasserver/management/commands/dbupgrade.py 2016-09-04 19:57:50 +0000
2187+++ src/maasserver/management/commands/dbupgrade.py 2016-12-07 15:50:52 +0000
2188@@ -9,6 +9,7 @@
2189 __all__ = []
2190
2191 from importlib import import_module
2192+import json
2193 import optparse
2194 import os
2195 import shutil
2196@@ -34,7 +35,7 @@
2197 # Script that performs the south migrations for MAAS under django 1.6 and
2198 # python2.7.
2199 MAAS_UPGRADE_SCRIPT = """\
2200-# Copyright 2015 Canonical Ltd. This software is licensed under the
2201+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2202 # GNU Affero General Public License version 3 (see the file LICENSE).
2203
2204 from __future__ import (
2205@@ -46,12 +47,49 @@
2206 str = None
2207
2208 __metaclass__ = type
2209-__all__ = [
2210- ]
2211+__all__ = []
2212
2213 import os
2214 import sys
2215
2216+import django.conf
2217+
2218+
2219+class LazySettings(django.conf.LazySettings):
2220+ '''Prevent Django from mangling warnings settings.
2221+
2222+ At present, Django adds a single filter that surfaces all deprecation
2223+ warnings, but MAAS handles them differently. Django doesn't appear to give
2224+ a way to prevent it from doing its thing, so we must undo its changes.
2225+
2226+ Deprecation warnings in production environments are not desirable as they
2227+ are a developer tool, and not something an end user can reasonably do
2228+ something about. This brings control of warnings back into MAAS's control.
2229+ '''
2230+
2231+ def _configure_logging(self):
2232+ # This is a copy of *half* of Django's `_configure_logging`, omitting
2233+ # the problematic bits.
2234+ if self.LOGGING_CONFIG:
2235+ from django.utils.log import DEFAULT_LOGGING
2236+ from django.utils.module_loading import import_by_path
2237+ # First find the logging configuration function ...
2238+ logging_config_func = import_by_path(self.LOGGING_CONFIG)
2239+ logging_config_func(DEFAULT_LOGGING)
2240+ # ... then invoke it with the logging settings
2241+ if self.LOGGING:
2242+ logging_config_func(self.LOGGING)
2243+
2244+
2245+# Install our `LazySettings` as the Django-global settings class. First,
2246+# ensure that Django hasn't yet loaded its settings.
2247+assert not django.conf.settings.configured
2248+# This is needed because Django's `LazySettings` overrides `__setattr__`.
2249+object.__setattr__(django.conf.settings, "__class__", LazySettings)
2250+
2251+# Force Django configuration.
2252+os.environ["DJANGO_SETTINGS_MODULE"] = "maas19settings"
2253+
2254 # Inject the sys.path from the parent process so that the python path is
2255 # is similar, except that the directory that this script is running from is
2256 # already the first path in sys.path.
2257@@ -132,6 +170,11 @@
2258 tempdir = tempfile.mkdtemp(prefix='maas-upgrade-')
2259 subprocess.check_call([
2260 "tar", "zxf", path_to_tarball, "-C", tempdir])
2261+
2262+ settings_json = os.path.join(tempdir, "maas19settings.json")
2263+ with open(settings_json, "w", encoding="utf-8") as fd:
2264+ fd.write(json.dumps({"DATABASES": settings.DATABASES}))
2265+
2266 script_path = os.path.join(tempdir, "migrate.py")
2267 with open(script_path, "wb") as fp:
2268 fp.write(MAAS_UPGRADE_SCRIPT.encode("utf-8"))
2269
2270=== modified file 'src/maasserver/management/commands/tests/test_dbupgrade.py'
2271--- src/maasserver/management/commands/tests/test_dbupgrade.py 2016-03-28 13:54:47 +0000
2272+++ src/maasserver/management/commands/tests/test_dbupgrade.py 2016-12-07 15:50:52 +0000
2273@@ -64,7 +64,10 @@
2274 env = os.environ.copy()
2275 env["MAAS_PREVENT_MIGRATIONS"] = "0"
2276 mra = os.path.join(root, "bin", "maas-region")
2277- cmd = [mra, "dbupgrade", "--settings", "maas.settings"]
2278+ cmd = [
2279+ mra, "dbupgrade", "--settings",
2280+ "maasserver.djangosettings.settings",
2281+ ]
2282 if always_south:
2283 cmd.append("--always-south")
2284 self.execute(cmd, env=env)
2285
2286=== modified file 'src/maasserver/migrations/builtin/maasserver/0016_migrate_power_data_node_to_bmc.py'
2287--- src/maasserver/migrations/builtin/maasserver/0016_migrate_power_data_node_to_bmc.py 2016-05-11 19:01:48 +0000
2288+++ src/maasserver/migrations/builtin/maasserver/0016_migrate_power_data_node_to_bmc.py 2016-12-07 15:50:52 +0000
2289@@ -2,10 +2,8 @@
2290
2291 from django.db import migrations
2292 from maasserver.models import timestampedmodel
2293-from provisioningserver.power.schema import (
2294- POWER_FIELDS_BY_TYPE,
2295- POWER_PARAMETER_SCOPE,
2296-)
2297+from provisioningserver.drivers import SETTING_SCOPE
2298+from provisioningserver.drivers.power import PowerDriverRegistry
2299
2300 # Copied from BMC model.
2301 def scope_power_parameters(power_type, power_params):
2302@@ -14,16 +12,20 @@
2303 if not power_type:
2304 # If there is no power type, treat all params as node params.
2305 return ({}, power_params)
2306- power_fields = POWER_FIELDS_BY_TYPE.get(power_type)
2307+ power_driver = PowerDriverRegistry.get_item(power_type)
2308+ if power_driver is None:
2309+ # If there is no power driver, treat all params as node params.
2310+ return ({}, power_params)
2311+ power_fields = power_driver.settings
2312 if not power_fields:
2313 # If there is no parameter info, treat all params as node params.
2314 return ({}, power_params)
2315 bmc_params = {}
2316 node_params = {}
2317 for param_name in power_params:
2318- power_field = power_fields.get(param_name)
2319+ power_field = power_driver.get_setting(param_name)
2320 if power_field and power_field.get(
2321- 'scope') == POWER_PARAMETER_SCOPE.BMC:
2322+ 'scope') == SETTING_SCOPE.BMC:
2323 bmc_params[param_name] = power_params[param_name]
2324 else:
2325 node_params[param_name] = power_params[param_name]
2326
2327=== modified file 'src/maasserver/migrations/builtin/maasserver/0022_extract_ip_for_bmcs.py'
2328--- src/maasserver/migrations/builtin/maasserver/0022_extract_ip_for_bmcs.py 2016-05-11 19:01:48 +0000
2329+++ src/maasserver/migrations/builtin/maasserver/0022_extract_ip_for_bmcs.py 2016-12-07 15:50:52 +0000
2330@@ -8,7 +8,7 @@
2331 )
2332 from maasserver.enum import IPADDRESS_TYPE
2333 from maasserver.models import timestampedmodel
2334-from provisioningserver.power.schema import POWER_TYPE_PARAMETERS_BY_NAME
2335+from provisioningserver.drivers.power import PowerDriverRegistry
2336
2337 # Derived from Subnet model.
2338 def raw_subnet_id_containing_ip(ip):
2339@@ -38,10 +38,13 @@
2340 # power_address field, returns None.
2341 if not power_type or not power_parameters:
2342 return None
2343- power_type_parameters = POWER_TYPE_PARAMETERS_BY_NAME.get(power_type)
2344+ power_driver = PowerDriverRegistry.get_item(power_type)
2345+ if power_driver is None:
2346+ return None
2347+ power_type_parameters = power_driver.settings
2348 if not power_type_parameters:
2349 return None
2350- ip_extractor = power_type_parameters.get('ip_extractor')
2351+ ip_extractor = power_driver.ip_extractor
2352 if not ip_extractor:
2353 return None
2354 field_value = power_parameters.get(ip_extractor.get('field_name'))
2355
2356=== modified file 'src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py'
2357--- src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py 2016-05-11 19:01:48 +0000
2358+++ src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py 2016-12-07 15:50:52 +0000
2359@@ -37,7 +37,7 @@
2360 IPRange, subnet, ranges, created_time, range_description):
2361 unreserved_range_set = MAASIPSet(ranges)
2362 unreserved_ranges = unreserved_range_set.get_unused_ranges(
2363- subnet.cidr, comment="reserved")
2364+ subnet.cidr, purpose="reserved")
2365 for iprange in unreserved_ranges:
2366 start_ip = str(IPAddress(iprange.first))
2367 end_ip = str(IPAddress(iprange.last))
2368
2369=== modified file 'src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py'
2370--- src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-07-30 01:17:54 +0000
2371+++ src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-12-07 15:50:52 +0000
2372@@ -44,6 +44,6 @@
2373 migrations.AlterField(
2374 model_name='subnet',
2375 name='vlan',
2376- field=models.ForeignKey(to='maasserver.VLAN', default=maasserver.models.subnet.get_default_vlan, on_delete=django.db.models.deletion.PROTECT),
2377+ field=models.ForeignKey(to='maasserver.VLAN', default=None, on_delete=django.db.models.deletion.PROTECT),
2378 ),
2379 ]
2380
2381=== added file 'src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py'
2382--- src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py 1970-01-01 00:00:00 +0000
2383+++ src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py 2016-12-07 15:50:52 +0000
2384@@ -0,0 +1,22 @@
2385+# -*- coding: utf-8 -*-
2386+from __future__ import unicode_literals
2387+
2388+from django.db import (
2389+ migrations,
2390+ models,
2391+)
2392+
2393+
2394+class Migration(migrations.Migration):
2395+
2396+ dependencies = [
2397+ ('maasserver', '0093_add_rdns_model'),
2398+ ]
2399+
2400+ operations = [
2401+ migrations.AddField(
2402+ model_name='subnet',
2403+ name='managed',
2404+ field=models.BooleanField(default=True),
2405+ ),
2406+ ]
2407
2408=== added file 'src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py'
2409--- src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py 1970-01-01 00:00:00 +0000
2410+++ src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py 2016-12-07 15:50:52 +0000
2411@@ -0,0 +1,23 @@
2412+# -*- coding: utf-8 -*-
2413+from __future__ import unicode_literals
2414+
2415+from django.db import (
2416+ migrations,
2417+ models,
2418+)
2419+import django.db.models.deletion
2420+
2421+
2422+class Migration(migrations.Migration):
2423+
2424+ dependencies = [
2425+ ('maasserver', '0094_add_unmanaged_subnets'),
2426+ ]
2427+
2428+ operations = [
2429+ migrations.AddField(
2430+ model_name='vlan',
2431+ name='relay_vlan',
2432+ field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, null=True, blank=True, related_name='relay_vlans', to='maasserver.VLAN'),
2433+ ),
2434+ ]
2435
2436=== added file 'src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py'
2437--- src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py 1970-01-01 00:00:00 +0000
2438+++ src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py 2016-12-07 15:50:52 +0000
2439@@ -0,0 +1,24 @@
2440+# -*- coding: utf-8 -*-
2441+from __future__ import unicode_literals
2442+
2443+from django.db import (
2444+ migrations,
2445+ models,
2446+)
2447+import django.db.models.deletion
2448+import maasserver.models.subnet
2449+
2450+
2451+class Migration(migrations.Migration):
2452+
2453+ dependencies = [
2454+ ('maasserver', '0095_vlan_relay_vlan'),
2455+ ]
2456+
2457+ operations = [
2458+ migrations.AlterField(
2459+ model_name='subnet',
2460+ name='vlan',
2461+ field=models.ForeignKey(to='maasserver.VLAN', default=maasserver.models.subnet.get_default_vlan, on_delete=django.db.models.deletion.PROTECT),
2462+ ),
2463+ ]
2464
2465=== added file 'src/maasserver/migrations/builtin/maasserver/0097_node_chassis_storage_hints.py'
2466--- src/maasserver/migrations/builtin/maasserver/0097_node_chassis_storage_hints.py 1970-01-01 00:00:00 +0000
2467+++ src/maasserver/migrations/builtin/maasserver/0097_node_chassis_storage_hints.py 2016-12-07 15:50:52 +0000
2468@@ -0,0 +1,73 @@
2469+# -*- coding: utf-8 -*-
2470+from __future__ import unicode_literals
2471+
2472+from django.db import (
2473+ migrations,
2474+ models,
2475+)
2476+import django.db.models.deletion
2477+import maasserver.models.cleansave
2478+import maasserver.models.node
2479+
2480+
2481+class Migration(migrations.Migration):
2482+
2483+ dependencies = [
2484+ ('maasserver', '0096_set_default_vlan_field'),
2485+ ]
2486+
2487+ operations = [
2488+ migrations.CreateModel(
2489+ name='ChassisHints',
2490+ fields=[
2491+ ('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
2492+ ('cores', models.IntegerField(default=0)),
2493+ ('memory', models.IntegerField(default=0)),
2494+ ('local_storage', models.IntegerField(default=0)),
2495+ ],
2496+ bases=(maasserver.models.cleansave.CleanSave, models.Model),
2497+ ),
2498+ migrations.CreateModel(
2499+ name='Chassis',
2500+ fields=[
2501+ ],
2502+ options={
2503+ 'proxy': True,
2504+ },
2505+ bases=('maasserver.node',),
2506+ ),
2507+ migrations.CreateModel(
2508+ name='Storage',
2509+ fields=[
2510+ ],
2511+ options={
2512+ 'proxy': True,
2513+ },
2514+ bases=('maasserver.node',),
2515+ ),
2516+ migrations.AddField(
2517+ model_name='node',
2518+ name='cpu_speed',
2519+ field=models.IntegerField(default=0),
2520+ ),
2521+ migrations.AddField(
2522+ model_name='node',
2523+ name='dynamic',
2524+ field=models.BooleanField(default=False),
2525+ ),
2526+ migrations.AlterField(
2527+ model_name='node',
2528+ name='domain',
2529+ field=models.ForeignKey(to='maasserver.Domain', null=True, default=maasserver.models.node.get_default_domain, blank=True, on_delete=django.db.models.deletion.PROTECT),
2530+ ),
2531+ migrations.AlterField(
2532+ model_name='node',
2533+ name='node_type',
2534+ field=models.IntegerField(choices=[(0, 'Machine'), (1, 'Device'), (2, 'Rack controller'), (3, 'Region controller'), (4, 'Region and rack controller'), (5, 'Chassis'), (6, 'Storage')], default=0, editable=False),
2535+ ),
2536+ migrations.AddField(
2537+ model_name='chassishints',
2538+ name='chassis',
2539+ field=models.OneToOneField(to='maasserver.Node', related_name='chassis_hints'),
2540+ ),
2541+ ]
2542
2543=== renamed file 'src/maasserver/migrations/south/django16_south_maas19.tar.gz' => 'src/maasserver/migrations/south/django16_south_maas19.tar.gz.OTHER'
2544Binary files src/maasserver/migrations/south/django16_south_maas19.tar.gz 2016-11-22 00:53:43 +0000 and src/maasserver/migrations/south/django16_south_maas19.tar.gz.OTHER 2016-12-07 15:50:52 +0000 differ
2545=== modified file 'src/maasserver/models/__init__.py'
2546--- src/maasserver/models/__init__.py 2016-10-18 22:22:23 +0000
2547+++ src/maasserver/models/__init__.py 2016-12-07 15:50:52 +0000
2548@@ -16,6 +16,8 @@
2549 'BootSourceSelection',
2550 'BridgeInterface',
2551 'CacheSet',
2552+ 'Chassis',
2553+ 'ChassisHints',
2554 'ComponentError',
2555 'Config',
2556 'Controller',
2557@@ -59,6 +61,7 @@
2558 'RegionRackRPCConnection',
2559 'Service',
2560 'Space',
2561+ 'Storage',
2562 'SSHKey',
2563 'SSLKey',
2564 'StaticIPAddress',
2565@@ -98,6 +101,7 @@
2566 from maasserver.models.bootsourcecache import BootSourceCache
2567 from maasserver.models.bootsourceselection import BootSourceSelection
2568 from maasserver.models.cacheset import CacheSet
2569+from maasserver.models.chassishints import ChassisHints
2570 from maasserver.models.component_error import ComponentError
2571 from maasserver.models.config import Config
2572 from maasserver.models.dhcpsnippet import DHCPSnippet
2573@@ -133,6 +137,7 @@
2574 from maasserver.models.mdns import MDNS
2575 from maasserver.models.neighbour import Neighbour
2576 from maasserver.models.node import (
2577+ Chassis,
2578 Controller,
2579 Device,
2580 Machine,
2581@@ -140,6 +145,7 @@
2582 NodeGroupToRackController,
2583 RackController,
2584 RegionController,
2585+ Storage,
2586 )
2587 from maasserver.models.ownerdata import OwnerData
2588 from maasserver.models.packagerepository import PackageRepository
2589
2590=== modified file 'src/maasserver/models/bmc.py'
2591--- src/maasserver/models/bmc.py 2016-06-04 00:21:58 +0000
2592+++ src/maasserver/models/bmc.py 2016-12-07 15:50:52 +0000
2593@@ -25,12 +25,9 @@
2594 from maasserver.models.subnet import Subnet
2595 from maasserver.models.timestampedmodel import TimestampedModel
2596 from maasserver.rpc import getAllClients
2597+from provisioningserver.drivers import SETTING_SCOPE
2598+from provisioningserver.drivers.power import PowerDriverRegistry
2599 from provisioningserver.logger import get_maas_logger
2600-from provisioningserver.power.schema import (
2601- POWER_FIELDS_BY_TYPE,
2602- POWER_PARAMETER_SCOPE,
2603- POWER_TYPE_PARAMETERS_BY_NAME,
2604-)
2605
2606
2607 maaslog = get_maas_logger("node")
2608@@ -125,16 +122,20 @@
2609 if not power_type:
2610 # If there is no power type, treat all params as node params.
2611 return ({}, power_params)
2612- power_fields = POWER_FIELDS_BY_TYPE.get(power_type)
2613+ power_driver = PowerDriverRegistry.get_item(power_type)
2614+ if power_driver is None:
2615+ # If there is no power driver, treat all params as node params.
2616+ return ({}, power_params)
2617+ power_fields = power_driver.settings
2618 if not power_fields:
2619 # If there is no parameter info, treat all params as node params.
2620 return ({}, power_params)
2621 bmc_params = {}
2622 node_params = {}
2623 for param_name in power_params:
2624- power_field = power_fields.get(param_name)
2625+ power_field = power_driver.get_setting(param_name)
2626 if (power_field and
2627- power_field.get('scope') == POWER_PARAMETER_SCOPE.BMC):
2628+ power_field.get('scope') == SETTING_SCOPE.BMC):
2629 bmc_params[param_name] = power_params[param_name]
2630 else:
2631 node_params[param_name] = power_params[param_name]
2632@@ -148,12 +149,17 @@
2633 if not power_type or not power_parameters:
2634 # Nothing to extract.
2635 return None
2636- power_type_parameters = POWER_TYPE_PARAMETERS_BY_NAME.get(power_type)
2637+ power_driver = PowerDriverRegistry.get_item(power_type)
2638+ if power_driver is None:
2639+ maaslog.warning(
2640+ "No power driver for power type %s" % power_type)
2641+ return None
2642+ power_type_parameters = power_driver.settings
2643 if not power_type_parameters:
2644 maaslog.warning(
2645- "No POWER_TYPE_PARAMETERS for power type %s" % power_type)
2646+ "No power driver settings for power type %s" % power_type)
2647 return None
2648- ip_extractor = power_type_parameters.get('ip_extractor')
2649+ ip_extractor = power_driver.ip_extractor
2650 if not ip_extractor:
2651 maaslog.info(
2652 "No IP extractor configured for power type %s. "
2653
2654=== added file 'src/maasserver/models/chassishints.py'
2655--- src/maasserver/models/chassishints.py 1970-01-01 00:00:00 +0000
2656+++ src/maasserver/models/chassishints.py 2016-12-07 15:50:52 +0000
2657@@ -0,0 +1,33 @@
2658+# Copyright 2016 Canonical Ltd. This software is licensed under the
2659+# GNU Affero General Public License version 3 (see the file LICENSE).
2660+
2661+"""Model that holds hint information for a Chassis."""
2662+
2663+__all__ = [
2664+ 'ChassisHints',
2665+ ]
2666+
2667+
2668+from django.db.models import (
2669+ IntegerField,
2670+ Model,
2671+ OneToOneField,
2672+)
2673+from maasserver import DefaultMeta
2674+from maasserver.models.cleansave import CleanSave
2675+from maasserver.models.node import Node
2676+
2677+
2678+class ChassisHints(CleanSave, Model):
2679+ """Hint information for a chassis."""
2680+
2681+ class Meta(DefaultMeta):
2682+ """Needed for South to recognize this model."""
2683+
2684+ chassis = OneToOneField(Node, related_name="chassis_hints")
2685+
2686+ cores = IntegerField(default=0)
2687+
2688+ memory = IntegerField(default=0)
2689+
2690+ local_storage = IntegerField(default=0)
2691
2692=== modified file 'src/maasserver/models/event.py'
2693--- src/maasserver/models/event.py 2016-10-25 13:57:02 +0000
2694+++ src/maasserver/models/event.py 2016-12-07 15:50:52 +0000
2695@@ -1,4 +1,4 @@
2696-# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
2697+# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
2698 # GNU Affero General Public License version 3 (see the file LICENSE).
2699
2700 """:class:`Event` and friends."""
2701@@ -21,6 +21,7 @@
2702 from maasserver.models.timestampedmodel import TimestampedModel
2703 from provisioningserver.events import EVENT_DETAILS
2704 from provisioningserver.logger import get_maas_logger
2705+from provisioningserver.utils.env import get_maas_id
2706
2707
2708 maaslog = get_maas_logger('models.event')
2709@@ -56,6 +57,12 @@
2710 event_action=event_action,
2711 event_description=event_description)
2712
2713+ def create_region_event(self, event_type, event_description=''):
2714+ """Helper to register event and event type for the running region."""
2715+ self.create_node_event(
2716+ system_id=get_maas_id(), event_type=event_type,
2717+ event_description=event_description)
2718+
2719
2720 class Event(CleanSave, TimestampedModel):
2721 """An `Event` represents a MAAS event.
2722
2723=== modified file 'src/maasserver/models/node.py'
2724--- src/maasserver/models/node.py 2016-12-07 11:26:49 +0000
2725+++ src/maasserver/models/node.py 2016-12-07 15:50:52 +0000
2726@@ -158,12 +158,12 @@
2727 )
2728 import petname
2729 from piston3.models import Token
2730+from provisioningserver.drivers.power import PowerDriverRegistry
2731 from provisioningserver.events import (
2732 EVENT_DETAILS,
2733 EVENT_TYPES,
2734 )
2735 from provisioningserver.logger import get_maas_logger
2736-from provisioningserver.power import QUERY_POWER_TYPES
2737 from provisioningserver.refresh import (
2738 get_sys_info,
2739 refresh,
2740@@ -265,32 +265,6 @@
2741 "we could find no unused node identifiers." % attempt)
2742
2743
2744-def typecast_node(node, model):
2745- """Typecast a node object into a node type object."""
2746- assert(isinstance(node, Node))
2747- assert(issubclass(model, Node))
2748- node.__class__ = model
2749- return node
2750-
2751-
2752-def typecast_to_node_type(node):
2753- """Typecast a node object to what the node_type is set to."""
2754- if node.node_type == NODE_TYPE.MACHINE:
2755- return typecast_node(node, Machine)
2756- elif node.node_type in (
2757- NODE_TYPE.RACK_CONTROLLER,
2758- NODE_TYPE.REGION_AND_RACK_CONTROLLER):
2759- # XXX ltrager 18-02-2016 - Currently only rack controllers have
2760- # unique functionality so when combined return a rack controller
2761- return typecast_node(node, RackController)
2762- elif node.node_type == NODE_TYPE.REGION_CONTROLLER:
2763- return typecast_node(node, RegionController)
2764- elif node.node_type == NODE_TYPE.DEVICE:
2765- return typecast_node(node, Device)
2766- else:
2767- raise NotImplementedError("Unknown node type %d" % node.node_type)
2768-
2769-
2770 class NodeQueriesMixin(MAASQueriesMixin):
2771
2772 def filter_by_spaces(self, spaces):
2773@@ -520,7 +494,7 @@
2774 node = get_object_or_404(
2775 self.model, system_id=system_id, **kwargs)
2776 if user.has_perm(perm, node):
2777- return typecast_to_node_type(node)
2778+ return node.as_self()
2779 else:
2780 raise PermissionDenied()
2781
2782@@ -573,6 +547,19 @@
2783 extra_filters = {'node_type': NODE_TYPE.DEVICE}
2784
2785
2786+class ChassisManager(BaseNodeManager):
2787+ """Chassis are nodes that contain or can compose more machines or
2788+ storage."""
2789+
2790+ extra_filters = {'node_type': NODE_TYPE.CHASSIS}
2791+
2792+
2793+class StorageManager(BaseNodeManager):
2794+ """Storage are nodes that provide storage to other machines."""
2795+
2796+ extra_filters = {'node_type': NODE_TYPE.STORAGE}
2797+
2798+
2799 class ControllerManager(BaseNodeManager):
2800 """All controllers `RackController`, `RegionController`, and
2801 `RegionRackController`."""
2802@@ -746,7 +733,8 @@
2803 update_fields.append("owner")
2804 if len(update_fields) > 0:
2805 node.save(update_fields=update_fields)
2806- return typecast_node(node, self.model)
2807+ # Always cast to a region controller.
2808+ return node.as_region_controller()
2809
2810 def _create_running_controller(self):
2811 """Create a region controller for the host machine.
2812@@ -844,7 +832,7 @@
2813 # What Domain do we use for this host unless the individual StaticIPAddress
2814 # record overrides it?
2815 domain = ForeignKey(
2816- Domain, default=get_default_domain, null=False,
2817+ Domain, default=get_default_domain, null=True, blank=True,
2818 editable=True, on_delete=PROTECT)
2819
2820 # TTL for this Node's IP addresses. Since this must be the same for all
2821@@ -904,6 +892,7 @@
2822 # Juju expects the following standard constraints, which are stored here
2823 # as a basic optimisation over querying the lshw output.
2824 cpu_count = IntegerField(default=0)
2825+ cpu_speed = IntegerField(default=0) # MHz
2826 memory = IntegerField(default=0)
2827
2828 swap_size = BigIntegerField(null=True, blank=True, default=None)
2829@@ -945,6 +934,11 @@
2830
2831 license_key = CharField(max_length=30, null=True, blank=True)
2832
2833+ # Only used by Machine. Set to True when the machine was composed
2834+ # dynamically from a Chassis during allocation. When the machine is
2835+ # released it will be deleted.
2836+ dynamic = BooleanField(default=False)
2837+
2838 tags = ManyToManyField(Tag)
2839
2840 # Record the Interface the node last booted from.
2841@@ -1120,7 +1114,10 @@
2842
2843 Return the FQDN for this host.
2844 """
2845- return '%s.%s' % (self.hostname, self.domain.name)
2846+ if self.domain is not None:
2847+ return '%s.%s' % (self.hostname, self.domain.name)
2848+ else:
2849+ return self.hostname
2850
2851 def get_deployment_time(self):
2852 """Return the deployment time of this node (in seconds).
2853@@ -1704,7 +1701,9 @@
2854 # Node.start() has synchronous and asynchronous parts, so catch
2855 # exceptions arising synchronously, and chain callbacks to the
2856 # Deferred it returns for the asynchronous (post-commit) bits.
2857- starting = self._start(user, commissioning_user_data, old_status)
2858+ starting = self._start(
2859+ user, commissioning_user_data, old_status,
2860+ allow_power_cycle=True)
2861 except Exception as error:
2862 self.status = old_status
2863 self.save()
2864@@ -2098,7 +2097,11 @@
2865 else:
2866 can_be_started = True
2867 can_be_stopped = True
2868- can_be_queried = power_type in QUERY_POWER_TYPES
2869+ power_driver = PowerDriverRegistry.get_item(power_type)
2870+ if power_driver is not None:
2871+ can_be_queried = power_driver.queryable
2872+ else:
2873+ can_be_queried = False
2874 return PowerInfo(
2875 can_be_started, can_be_stopped, can_be_queried,
2876 power_type, power_params,
2877@@ -2193,7 +2196,8 @@
2878 # Node.start() has synchronous and asynchronous parts, so catch
2879 # exceptions arising synchronously, and chain callbacks to the
2880 # Deferred it returns for the asynchronous (post-commit) bits.
2881- starting = self._start(user, disk_erase_user_data, old_status)
2882+ starting = self._start(
2883+ user, disk_erase_user_data, old_status, allow_power_cycle=True)
2884 except Exception as error:
2885 # We always mark the node as failed here, although we could
2886 # potentially move it back to the state it was in previously. For
2887@@ -2378,7 +2382,7 @@
2888 if self.power_state == POWER_STATE.OFF:
2889 # The node is already powered off; we can deallocate all attached
2890 # resources and mark the node READY without delay.
2891- release_to_ready = True
2892+ finalize_release = True
2893 elif self.get_effective_power_info().can_be_queried:
2894 # Controlled power type (one for which we can query the power
2895 # state): update_power_state() will take care of making the node
2896@@ -2387,13 +2391,13 @@
2897 post_commit().addCallback(
2898 callOutToDatabase, Node._set_status_expires,
2899 self.system_id, self.get_releasing_time())
2900- release_to_ready = False
2901+ finalize_release = False
2902 else:
2903 # The node's power cannot be reliably controlled. Frankly, this
2904 # node is not suitable for use with MAAS. Deallocate all attached
2905 # resources and mark the node READY without delay because there's
2906 # not much else we can do.
2907- release_to_ready = True
2908+ finalize_release = True
2909
2910 self.status = NODE_STATUS.RELEASING
2911 self.token = None
2912@@ -2418,25 +2422,29 @@
2913 self.children.all().delete()
2914
2915 # Power was off or cannot be powered off so release to ready now.
2916- if release_to_ready:
2917- self._release_to_ready()
2918+ if finalize_release:
2919+ self._finalize_release()
2920
2921 @transactional
2922- def _release_to_ready(self):
2923- """Release all remaining resources and mark the node `READY`.
2924+ def _finalize_release(self):
2925+ """Release all remaining resources, mark the machine `READY` if not
2926+ dynamic, otherwise delete the machine.
2927
2928 Releasing a node can be straightforward or it can be a multi-step
2929 operation, which can include a reboot in order to erase disks, then a
2930 final power-down. This method should be the absolute last method
2931 called.
2932 """
2933- self.release_interface_config()
2934- self.status = NODE_STATUS.READY
2935- self.owner = None
2936- self.save()
2937+ if self.dynamic:
2938+ self.delete()
2939+ else:
2940+ self.release_interface_config()
2941+ self.status = NODE_STATUS.READY
2942+ self.owner = None
2943+ self.save()
2944
2945- # Remove all set owner data.
2946- OwnerData.objects.filter(node=self).delete()
2947+ # Remove all set owner data.
2948+ OwnerData.objects.filter(node=self).delete()
2949
2950 def release_or_erase(
2951 self, user, comment=None,
2952@@ -2551,7 +2559,7 @@
2953 if mark_ready:
2954 # Ensure the node is released when it powers down.
2955 self.status_expires = None
2956- self._release_to_ready()
2957+ self._finalize_release()
2958 if self.status == NODE_STATUS.EXITING_RESCUE_MODE:
2959 if self.previous_status == NODE_STATUS.BROKEN:
2960 if power_state == POWER_STATE.OFF:
2961@@ -3074,14 +3082,18 @@
2962 # You can't start a node you don't own unless you're an admin.
2963 raise PermissionDenied()
2964 event = EVENT_TYPES.REQUEST_NODE_START
2965+ allow_power_cycle = False
2966 # If status is ALLOCATED, this start is actually for a deployment.
2967 # (Note: this is true even when nodes are being deployed from READY
2968 # state. See node_action.py; the node is acquired and then started.)
2969+ # Power cycling is allowed when deployment is being started.
2970 if self.status == NODE_STATUS.ALLOCATED:
2971 event = EVENT_TYPES.REQUEST_NODE_START_DEPLOYMENT
2972+ allow_power_cycle = True
2973 self._register_request_event(
2974 user, event, action='start', comment=comment)
2975- return self._start(user, user_data)
2976+ return self._start(
2977+ user, user_data, allow_power_cycle=allow_power_cycle)
2978
2979 def _get_bmc_client_connection_info(self, *args, **kwargs):
2980 """Return a tuple that list the rack controllers that can communicate
2981@@ -3114,7 +3126,9 @@
2982 return client_idents, fallback_idents
2983
2984 @transactional
2985- def _start(self, user, user_data=None, old_status=None):
2986+ def _start(
2987+ self, user, user_data=None, old_status=None,
2988+ allow_power_cycle=False):
2989 """Request on given user's behalf that the node be started up.
2990
2991 :param user: Requesting user.
2992@@ -3171,7 +3185,10 @@
2993
2994 # Request that the node be powered on post-commit.
2995 d = post_commit()
2996- d = self._power_control_node(d, power_on_node, power_info)
2997+ if self.power_state == POWER_STATE.ON and allow_power_cycle:
2998+ d = self._power_control_node(d, power_cycle, power_info)
2999+ else:
3000+ d = self._power_control_node(d, power_on_node, power_info)
3001
3002 # Set the deployment timeout so the node is marked failed after
3003 # a period of time.
3004@@ -3532,6 +3549,61 @@
3005 # deal with it.
3006 raise
3007
3008+ def _as(self, model):
3009+ """Create a `model` that shares underlying storage with `self`.
3010+
3011+ In other words, the newly returned object will be an instance of
3012+ `model` and its `__dict__` will be `self.__dict__`. Not a copy, but a
3013+ reference to, so that changes to one will be reflected in the other.
3014+ """
3015+ new = object.__new__(model)
3016+ new.__dict__ = self.__dict__
3017+ return new
3018+
3019+ def as_node(self):
3020+ """Return a reference to self that behaves as a `Node`."""
3021+ return self._as(Node)
3022+
3023+ def as_machine(self):
3024+ """Return a reference to self that behaves as a `Machine`."""
3025+ return self._as(Machine)
3026+
3027+ def as_device(self):
3028+ """Return a reference to self that behaves as a `Device`."""
3029+ return self._as(Device)
3030+
3031+ def as_region_controller(self):
3032+ """Return a reference to self that behaves as a `RegionController`."""
3033+ return self._as(RegionController)
3034+
3035+ def as_rack_controller(self):
3036+ """Return a reference to self that behaves as a `RackController`."""
3037+ return self._as(RackController)
3038+
3039+ def as_chassis(self):
3040+ """Return a reference to self that behaves as a `Chassis`."""
3041+ return self._as(Chassis)
3042+
3043+ def as_storage(self):
3044+ """Return a reference to self that behaves as a `Storage`."""
3045+ return self._as(Storage)
3046+
3047+ _as_self = {
3048+ NODE_TYPE.DEVICE: as_device,
3049+ NODE_TYPE.MACHINE: as_machine,
3050+ NODE_TYPE.RACK_CONTROLLER: as_rack_controller,
3051+ # XXX ltrager 18-02-2016 - Currently only rack controllers have
3052+ # unique functionality so when combined return a rack controller
3053+ NODE_TYPE.REGION_AND_RACK_CONTROLLER: as_rack_controller,
3054+ NODE_TYPE.REGION_CONTROLLER: as_region_controller,
3055+ NODE_TYPE.CHASSIS: as_chassis,
3056+ NODE_TYPE.STORAGE: as_storage,
3057+ }
3058+
3059+ def as_self(self):
3060+ """Return a reference to self that behaves as its own type."""
3061+ return self._as_self[self.node_type](self)
3062+
3063
3064 # Piston serializes objects based on the object class.
3065 # Here we define a proxy class so that we can specialize how devices are
3066@@ -4224,7 +4296,7 @@
3067 # If the refresh is occuring on the running region execute it using
3068 # the region process. This avoids using RPC and sends the node
3069 # results back to this host when in HA.
3070- yield typecast_node(self, RegionController).refresh()
3071+ yield self.as_region_controller().refresh()
3072 return
3073
3074 client = yield getClientFor(self.system_id, timeout=1)
3075@@ -4457,10 +4529,9 @@
3076 % self.hostname)
3077
3078 if self.node_type == NODE_TYPE.REGION_AND_RACK_CONTROLLER:
3079- # typecast_to_node_type returns a RackController object when the
3080- # node is a REGION_AND_RACK_CONTROLLER. Thus the API and websocket
3081- # will transition a REGION_AND_RACK_CONTROLLER to a
3082- # REGION_CONTROLLER.
3083+ # Node.as_self() returns a RackController object when the node is
3084+ # a REGION_AND_RACK_CONTROLLER. Thus the API and websocket will
3085+ # transition a REGION_AND_RACK_CONTROLLER to a REGION_CONTROLLER.
3086 self.node_type = NODE_TYPE.RACK_CONTROLLER
3087 self.save()
3088 elif self._was_probably_machine():
3089@@ -4519,6 +4590,56 @@
3090 pass
3091
3092
3093+class Chassis(Node):
3094+ """A node that contains multiple machines and can compose new machines."""
3095+
3096+ objects = ChassisManager()
3097+
3098+ class Meta(DefaultMeta):
3099+ proxy = True
3100+
3101+ def __init__(self, *args, **kwargs):
3102+ super(Chassis, self).__init__(
3103+ node_type=NODE_TYPE.CHASSIS, *args, **kwargs)
3104+
3105+ def clean_architecture(self, prev):
3106+ # Chassis aren't required to have a defined architecture
3107+ pass
3108+
3109+ def clean_hostname_domain(self, prev):
3110+ # Chassis is never in a domain.
3111+ if self.hostname.find('.') > -1:
3112+ # They have specified an FQDN. Split up the pieces, and throw
3113+ # away the rest.
3114+ self.hostname, _ = self.hostname.split('.', 1)
3115+ self.domain = None
3116+
3117+
3118+class Storage(Node):
3119+ """A node that provides storage to other machines."""
3120+
3121+ objects = StorageManager()
3122+
3123+ class Meta(DefaultMeta):
3124+ proxy = True
3125+
3126+ def __init__(self, *args, **kwargs):
3127+ super(Storage, self).__init__(
3128+ node_type=NODE_TYPE.STORAGE, *args, **kwargs)
3129+
3130+ def clean_architecture(self, prev):
3131+ # Storage aren't required to have a defined architecture
3132+ pass
3133+
3134+ def clean_hostname_domain(self, prev):
3135+ # Storage is never in a domain.
3136+ if self.hostname.find('.') > -1:
3137+ # They have specified an FQDN. Split up the pieces, and throw
3138+ # away the rest.
3139+ self.hostname, _ = self.hostname.split('.', 1)
3140+ self.domain = None
3141+
3142+
3143 class NodeGroupToRackController(CleanSave, Model):
3144 """Store some of the old NodeGroup data so we can migrate it when a rack
3145 controller is registered.
3146
3147=== modified file 'src/maasserver/models/signals/nodes.py'
3148--- src/maasserver/models/signals/nodes.py 2016-08-16 09:31:16 +0000
3149+++ src/maasserver/models/signals/nodes.py 2016-12-07 15:50:52 +0000
3150@@ -12,8 +12,12 @@
3151 post_save,
3152 pre_save,
3153 )
3154-from maasserver.enum import NODE_STATUS
3155+from maasserver.enum import (
3156+ NODE_STATUS,
3157+ NODE_TYPE,
3158+)
3159 from maasserver.models import (
3160+ ChassisHints,
3161 Controller,
3162 Device,
3163 Machine,
3164@@ -105,5 +109,23 @@
3165 sender=klass)
3166
3167
3168+def create_chassis_hints(sender, instance, created, **kwargs):
3169+ """Create `ChassisHints` when `Chassis` is created."""
3170+ try:
3171+ chassis_hints = instance.chassis_hints
3172+ except ChassisHints.DoesNotExist:
3173+ chassis_hints = None
3174+ if instance.node_type == NODE_TYPE.CHASSIS:
3175+ if chassis_hints is None:
3176+ ChassisHints.objects.create(chassis=instance)
3177+ elif chassis_hints is not None:
3178+ chassis_hints.delete()
3179+
3180+for klass in NODE_CLASSES:
3181+ signals.watch(
3182+ post_save, create_chassis_hints,
3183+ sender=klass)
3184+
3185+
3186 # Enable all signals by default.
3187 signals.enable()
3188
3189=== modified file 'src/maasserver/models/signals/tests/test_nodes.py'
3190--- src/maasserver/models/signals/tests/test_nodes.py 2016-09-07 14:23:05 +0000
3191+++ src/maasserver/models/signals/tests/test_nodes.py 2016-12-07 15:50:52 +0000
3192@@ -11,6 +11,7 @@
3193 NODE_STATUS,
3194 NODE_TYPE,
3195 )
3196+from maasserver.models import ChassisHints
3197 from maasserver.models.service import (
3198 RACK_SERVICES,
3199 REGION_SERVICES,
3200@@ -144,3 +145,27 @@
3201 self.assertThat(
3202 {service.name for service in services},
3203 Equals(REGION_SERVICES))
3204+
3205+
3206+class TestCreateChassisHints(MAASServerTestCase):
3207+
3208+ def test_creates_hints_for_chassis(self):
3209+ chassis = factory.make_Node(node_type=NODE_TYPE.CHASSIS)
3210+ self.assertIsNotNone(chassis.chassis_hints)
3211+
3212+ def test_creates_hints_device_converted_to_chassis(self):
3213+ device = factory.make_Device()
3214+ device.node_type = NODE_TYPE.CHASSIS
3215+ device.save()
3216+ self.assertIsNotNone(device.chassis_hints)
3217+
3218+ def test_deletes_hints_when_chassis_converted_to_device(self):
3219+ chassis = factory.make_Node(node_type=NODE_TYPE.CHASSIS)
3220+ chassis.node_type = NODE_TYPE.DEVICE
3221+ chassis.save()
3222+ error = None
3223+ try:
3224+ reload_object(chassis).chassis_hints
3225+ except ChassisHints.DoesNotExist as exc:
3226+ error = exc
3227+ self.assertIsNotNone(error)
3228
3229=== modified file 'src/maasserver/models/staticipaddress.py'
3230--- src/maasserver/models/staticipaddress.py 2016-11-17 18:53:03 +0000
3231+++ src/maasserver/models/staticipaddress.py 2016-12-07 15:50:52 +0000
3232@@ -50,19 +50,10 @@
3233 from maasserver.models.domain import Domain
3234 from maasserver.models.subnet import Subnet
3235 from maasserver.models.timestampedmodel import TimestampedModel
3236+from maasserver.utils import orm
3237 from maasserver.utils.dns import get_ip_based_hostname
3238-from maasserver.utils.orm import (
3239- request_transaction_retry,
3240- transactional,
3241-)
3242-from maasserver.utils.threads import deferToDatabase
3243 from netaddr import IPAddress
3244-from provisioningserver.logger import get_maas_logger
3245 from provisioningserver.utils.enum import map_enum_reverse
3246-from provisioningserver.utils.twisted import asynchronous
3247-
3248-
3249-maaslog = get_maas_logger("node")
3250
3251
3252 class HostnameIPMapping:
3253@@ -144,15 +135,13 @@
3254 :return: `StaticIPAddress` if successful.
3255 :raise StaticIPAddressUnavailable: if the address was already taken.
3256 """
3257- ipaddress = StaticIPAddress(
3258- ip=requested_address.format(), alloc_type=alloc_type,
3259- subnet=subnet)
3260- ipaddress.set_ip_address(requested_address.format())
3261+ ipaddress = StaticIPAddress(alloc_type=alloc_type, subnet=subnet)
3262 try:
3263 # Try to save this address to the database. Do this in a nested
3264 # transaction so that we can continue using the outer transaction
3265 # even if this breaks.
3266 with transaction.atomic():
3267+ ipaddress.set_ip_address(requested_address.format())
3268 ipaddress.save()
3269 except IntegrityError:
3270 # The address is already taken.
3271@@ -168,6 +157,55 @@
3272 ipaddress.save()
3273 return ipaddress
3274
3275+ def _attempt_allocation_of_free_address(
3276+ self, requested_address, alloc_type, user=None, subnet=None):
3277+ """Attempt to allocate `requested_address`, which is known to be free.
3278+
3279+ It is known to be free *in this transaction*, so this could still
3280+ fail. If it does fail because of a `UNIQUE_VIOLATION` it will request
3281+ a retry, except while holding an addition lock. This is not perfect:
3282+ other threads could jump in before acquiring the lock and steal an
3283+ apparently free address. However, in stampede situations this appears
3284+ to be effective enough. Experiment by increasing the `count` parameter
3285+ in `test_allocate_new_works_under_extreme_concurrency`.
3286+
3287+ This method shares a lot in common with `_attempt_allocation` so check
3288+ out its documentation for more details.
3289+
3290+ :param requested_address: The address to be allocated.
3291+ :typr requested_address: IPAddress
3292+ :param alloc_type: Allocation type.
3293+ :param user: Optional user.
3294+ :return: `StaticIPAddress` if successful.
3295+ :raise RetryTransaction: if the address was already taken.
3296+ """
3297+ ipaddress = StaticIPAddress(alloc_type=alloc_type, subnet=subnet)
3298+ try:
3299+ # Try to save this address to the database. Do this in a nested
3300+ # transaction so that we can continue using the outer transaction
3301+ # even if this breaks.
3302+ with orm.savepoint():
3303+ ipaddress.set_ip_address(requested_address.format())
3304+ ipaddress.save()
3305+ except IntegrityError as error:
3306+ if orm.is_unique_violation(error):
3307+ # The address is taken. We could allow the transaction retry
3308+ # machinery to take care of this, but instead we'll ask it to
3309+ # retry with the `address_allocation` lock. We can't take it
3310+ # here because we're already in a transaction; we need to exit
3311+ # the transaction, take the lock, and only then try again.
3312+ orm.request_transaction_retry(locks.address_allocation)
3313+ else:
3314+ raise
3315+ else:
3316+ # We deliberately do *not* save the user until now because it
3317+ # might result in an IntegrityError, and we rely on the latter
3318+ # in the code above to indicate an already allocated IP
3319+ # address and nothing else.
3320+ ipaddress.user = user
3321+ ipaddress.save()
3322+ return ipaddress
3323+
3324 def allocate_new(
3325 self, subnet=None, alloc_type=IPADDRESS_TYPE.AUTO, user=None,
3326 requested_address=None, exclude_addresses=[]):
3327@@ -185,9 +223,6 @@
3328 :param exclude_addresses: A list of addresses which MUST NOT be used.
3329
3330 All IP parameters can be strings or netaddr.IPAddress.
3331-
3332- Note that this method has been designed to work even when the database
3333- is running with READ COMMITTED isolation. Try to keep it that way.
3334 """
3335 # This check for `alloc_type` is important for later on. We rely on
3336 # detecting IntegrityError as a sign than an IP address is already
3337@@ -203,21 +238,15 @@
3338 "Could not find an appropriate subnet.")
3339
3340 if requested_address is None:
3341- with locks.staticip_acquire:
3342- requested_address = self._async_find_free_ip(
3343- subnet, exclude_addresses=exclude_addresses).wait(30)
3344- try:
3345- return self._attempt_allocation(
3346- requested_address, alloc_type, user,
3347- subnet=subnet)
3348- except StaticIPAddressUnavailable:
3349- # We lost the race: another transaction has taken this IP
3350- # address. Retry this transaction from the top.
3351- request_transaction_retry()
3352+ requested_address = subnet.get_next_ip_for_allocation(
3353+ exclude_addresses=exclude_addresses)
3354+ return self._attempt_allocation_of_free_address(
3355+ requested_address, alloc_type, user=user, subnet=subnet)
3356 else:
3357 requested_address = IPAddress(requested_address)
3358 subnet.validate_static_ip(requested_address)
3359 return self._attempt_allocation(
3360+<<<<<<< TREE
3361 requested_address, alloc_type,
3362 user=user, subnet=subnet)
3363
3364@@ -375,6 +404,156 @@
3365
3366 def _find_free_ip(self, subnet, exclude_addresses=None):
3367 return subnet.get_next_ip_for_allocation(exclude_addresses)
3368+=======
3369+ requested_address, alloc_type, user=user, subnet=subnet)
3370+
3371+ def _get_special_mappings(self, domain, raw_ttl=False):
3372+ """Get the special mappings, possibly limited to a single Domain.
3373+
3374+ This function is responsible for creating these mappings:
3375+ - any USER_RESERVED IP,
3376+ - any IP not associated with a Node,
3377+ - any IP associated with a DNSResource.
3378+ The caller is responsible for addresses otherwise derived from nodes.
3379+
3380+ Because of how the get hostname_ip_mapping code works, we actually need
3381+ to fetch ALL of the entries for subnets, but forward mappings need to
3382+ be domain-specific.
3383+
3384+ :param domain: limit return to just the given Domain. If anything
3385+ other than a Domain is passed in (e.g., a Subnet or None), we
3386+ return all of the reverse mappings.
3387+ :param raw_ttl: Boolean, if True then just return the address_ttl,
3388+ otherwise, coalesce the address_ttl to be the correct answer for
3389+ zone generation.
3390+ :return: a (default) dict of hostname: HostnameIPMapping entries.
3391+ """
3392+ default_ttl = "%d" % Config.objects.get_config('default_dns_ttl')
3393+ if isinstance(domain, Domain):
3394+ # Domains are special in that we only want to have entries for the
3395+ # domain that we were asked about. And they can possibly come from
3396+ # either the child or the parent for glue.
3397+ where_clause = """
3398+ AND (
3399+ dnsrr.dom2_id = %s OR
3400+ node.dom2_id = %s OR
3401+ dnsrr.domain_id = %s OR
3402+ node.domain_id = %s
3403+ """
3404+ query_parms = [domain.id, domain.id, domain.id, domain.id]
3405+ # And the default domain is extra special, since it needs to have
3406+ # A/AAAA RRs for any USER_RESERVED addresses that have no name
3407+ # otherwise attached to them.
3408+ if domain.is_default():
3409+ where_clause += """ OR (
3410+ dnsrr.fqdn IS NULL AND
3411+ node.fqdn IS NULL)
3412+ """
3413+ where_clause += ")"
3414+ else:
3415+ # There is nothing special about the query for subnets.
3416+ domain = None
3417+ where_clause = ""
3418+ query_parms = []
3419+ # raw_ttl says that we don't coalesce, but we need to pick one, so we
3420+ # go with DNSResource if it is involved.
3421+ if raw_ttl:
3422+ ttl_clause = """COALESCE(dnsrr.address_ttl, node.address_ttl)"""
3423+ else:
3424+ ttl_clause = """
3425+ COALESCE(
3426+ dnsrr.address_ttl,
3427+ dnsrr.ttl,
3428+ node.address_ttl,
3429+ node.ttl,
3430+ %s)""" % default_ttl
3431+ # And here is the SQL query of doom. Build up inner selects to get the
3432+ # view of a DNSResource (and Node) that we need, and finally use
3433+ # domain2 to handle the case where an FQDN is also the name of a domain
3434+ # that we know.
3435+ sql_query = """
3436+ SELECT
3437+ COALESCE(dnsrr.fqdn, node.fqdn) AS fqdn,
3438+ node.system_id,
3439+ node.node_type,
3440+ """ + ttl_clause + """ AS ttl,
3441+ staticip.ip
3442+ FROM
3443+ maasserver_staticipaddress AS staticip
3444+ LEFT JOIN (
3445+ /* Create a dnsrr that has what we need. */
3446+ SELECT
3447+ CASE WHEN dnsrr.name = '@' THEN
3448+ dom.name
3449+ ELSE
3450+ CONCAT(dnsrr.name, '.', dom.name)
3451+ END AS fqdn,
3452+ dom.name as dom_name,
3453+ dnsrr.domain_id,
3454+ dnsrr.address_ttl,
3455+ dom.ttl,
3456+ dia.staticipaddress_id AS dnsrr_sip_id,
3457+ dom2.id AS dom2_id
3458+ FROM maasserver_dnsresource_ip_addresses AS dia
3459+ JOIN maasserver_dnsresource AS dnsrr ON
3460+ dia.dnsresource_id = dnsrr.id
3461+ JOIN maasserver_domain AS dom ON
3462+ dnsrr.domain_id = dom.id
3463+ LEFT JOIN maasserver_domain AS dom2 ON
3464+ CONCAT(dnsrr.name, '.', dom.name) = dom2.name OR (
3465+ dnsrr.name = '@' AND
3466+ dom.name SIMILAR TO CONCAT('[-A-Za-z0-9]*.', dom2.name)
3467+ )
3468+ ) AS dnsrr ON
3469+ dnsrr_sip_id = staticip.id
3470+ LEFT JOIN (
3471+ /* Create a node that has what we need. */
3472+ SELECT
3473+ CONCAT(nd.hostname, '.', dom.name) AS fqdn,
3474+ dom.name as dom_name,
3475+ nd.system_id,
3476+ nd.node_type,
3477+ nd.domain_id,
3478+ nd.address_ttl,
3479+ dom.ttl,
3480+ iia.staticipaddress_id AS node_sip_id,
3481+ dom2.id AS dom2_id
3482+ FROM maasserver_interface_ip_addresses AS iia
3483+ JOIN maasserver_interface AS iface ON
3484+ iia.interface_id = iface.id
3485+ JOIN maasserver_node AS nd ON
3486+ iface.node_id = nd.id
3487+ JOIN maasserver_domain AS dom ON
3488+ nd.domain_id = dom.id
3489+ LEFT JOIN maasserver_domain AS dom2 ON
3490+ CONCAT(nd.hostname, '.', dom.name) = dom2.name
3491+ ) AS node ON
3492+ node_sip_id = staticip.id
3493+ WHERE
3494+ staticip.ip IS NOT NULL AND
3495+ host(staticip.ip) != '' AND
3496+ (
3497+ staticip.alloc_type = %s OR
3498+ node.fqdn IS NULL OR
3499+ dnsrr IS NOT NULL
3500+ )""" + where_clause + """
3501+ """
3502+ default_domain = Domain.objects.get_default_domain()
3503+ mapping = defaultdict(HostnameIPMapping)
3504+ cursor = connection.cursor()
3505+ query_parms = [IPADDRESS_TYPE.USER_RESERVED] + query_parms
3506+ cursor.execute(sql_query, query_parms)
3507+ for (fqdn, system_id, node_type, ttl,
3508+ ip) in cursor.fetchall():
3509+ if fqdn is None or fqdn == '':
3510+ fqdn = "%s.%s" % (
3511+ get_ip_based_hostname(ip), default_domain.name)
3512+ mapping[fqdn].node_type = node_type
3513+ mapping[fqdn].system_id = system_id
3514+ mapping[fqdn].ttl = ttl
3515+ mapping[fqdn].ips.add(ip)
3516+ return mapping
3517+>>>>>>> MERGE-SOURCE
3518
3519 def get_hostname_ip_mapping(self, domain_or_subnet, raw_ttl=False):
3520 """Return hostname mappings for `StaticIPAddress` entries.
3521
3522=== modified file 'src/maasserver/models/subnet.py'
3523--- src/maasserver/models/subnet.py 2016-10-18 16:48:13 +0000
3524+++ src/maasserver/models/subnet.py 2016-12-07 15:50:52 +0000
3525@@ -59,6 +59,7 @@
3526 )
3527 from provisioningserver.logger import get_maas_logger
3528 from provisioningserver.utils.network import (
3529+ IPRANGE_TYPE as MAASIPRANGE_TYPE,
3530 MAASIPSet,
3531 make_ipaddress,
3532 make_iprange,
3533@@ -386,6 +387,9 @@
3534 active_discovery = BooleanField(
3535 editable=True, blank=False, null=False, default=False)
3536
3537+ managed = BooleanField(
3538+ editable=True, blank=False, null=False, default=True)
3539+
3540 @property
3541 def label(self):
3542 """Returns a human-friendly label for this subnet."""
3543@@ -474,7 +478,8 @@
3544
3545 def get_ipranges_in_use(
3546 self, exclude_addresses: IPAddressExcludeList=None,
3547- ranges_only: bool=False,
3548+ ranges_only: bool=False, include_reserved: bool=True,
3549+ with_neighbours: bool=False,
3550 ignore_discovered_ips: bool=False) -> MAASIPSet:
3551 """Returns a `MAASIPSet` of `MAASIPRange` objects which are currently
3552 in use on this `Subnet`.
3553@@ -483,6 +488,8 @@
3554 :param ignore_discovered_ips: DISCOVERED addresses are not "in use".
3555 :param ranges_only: if True, filters out gateway IPs, static routes,
3556 DNS servers, and `exclude_addresses`.
3557+ :param with_neighbours: If True, includes addresses learned from
3558+ neighbour observation.
3559 """
3560 if exclude_addresses is None:
3561 exclude_addresses = []
3562@@ -531,8 +538,11 @@
3563 for address in exclude_addresses
3564 if address in network
3565 )
3566- ranges |= self.get_reserved_maasipset()
3567+ if include_reserved:
3568+ ranges |= self.get_reserved_maasipset()
3569 ranges |= self.get_dynamic_maasipset()
3570+ if with_neighbours:
3571+ ranges |= self.get_maasipset_for_neighbours()
3572 return MAASIPSet(ranges)
3573
3574 def get_ipranges_available_for_reserved_range(self):
3575@@ -549,19 +559,71 @@
3576 """Returns a `MAASIPSet` of ranges which are currently free on this
3577 `Subnet`.
3578
3579+ :param ranges_only: if True, filters out gateway IPs, static routes,
3580+ DNS servers, and `exclude_addresses`.
3581 :param exclude_addresses: An iterable of addresses not to use.
3582 :param ignore_discovered_ips: DISCOVERED addresses are not "in use".
3583+ :param with_neighbours: If True, includes addresses learned from
3584+ neighbour observation.
3585 """
3586 if exclude_addresses is None:
3587 exclude_addresses = []
3588- ranges = self.get_ipranges_in_use(
3589+ in_use = self.get_ipranges_in_use(
3590 exclude_addresses=exclude_addresses,
3591 ranges_only=ranges_only,
3592+ with_neighbours=with_neighbours,
3593 ignore_discovered_ips=ignore_discovered_ips)
3594- if with_neighbours:
3595- ranges |= self.get_maasipset_for_neighbours()
3596- unused = ranges.get_unused_ranges(self.get_ipnetwork())
3597- return unused
3598+ if self.managed or ranges_only:
3599+ not_in_use = in_use.get_unused_ranges(self.get_ipnetwork())
3600+ else:
3601+ # The end result we want is a list of unused IP addresses *within*
3602+ # reserved ranges. To get that result, we first need the full list
3603+ # of unused IP addresses on the subnet. This is better illustrated
3604+ # visually below.
3605+ #
3606+ # Legend:
3607+ # X: in-use IP addresses
3608+ # R: reserved range
3609+ # Rx: reserved range (with allocated, in-use IP address)
3610+ #
3611+ # +----+----+----+----+----+----+
3612+ # IP address: | 1 | 2 | 3 | 4 | 5 | 6 |
3613+ # +----+----+----+----+----+----+
3614+ # Usages: | X | | R | Rx | | X |
3615+ # +----+----+----+----+----+----+
3616+ #
3617+ # We need a set that just contains `3` in this case. To get there,
3618+ # first calculate the set of all unused addresses on the subnet,
3619+ # then intersect that set with set of in-use addresses *excluding*
3620+ # the reserved range, then calculate which addresses within *that*
3621+ # set are unused:
3622+ # +----+----+----+----+----+----+
3623+ # IP address: | 1 | 2 | 3 | 4 | 5 | 6 |
3624+ # +----+----+----+----+----+----+
3625+ # unused: | | U | | | U | |
3626+ # +----+----+----+----+----+----+
3627+ # unmanaged_in_use: | u | | | u | | u |
3628+ # +----+----+----+----+----+----+
3629+ # |= unmanaged: ===============================
3630+ # +----+----+----+----+----+----+
3631+ # unmanaged_in_use: | u | U | | u | U | u |
3632+ # +----+----+----+----+----+----+
3633+ # get_unused_ranges(): ===============================
3634+ # +----+----+----+----+----+----+
3635+ # not_in_use: | | | n | | | |
3636+ # +----+----+----+----+----+----+
3637+ unused = in_use.get_unused_ranges(
3638+ self.get_ipnetwork(), purpose=MAASIPRANGE_TYPE.UNMANAGED)
3639+ unmanaged_in_use = self.get_ipranges_in_use(
3640+ exclude_addresses=exclude_addresses,
3641+ ranges_only=ranges_only,
3642+ include_reserved=False,
3643+ with_neighbours=with_neighbours,
3644+ ignore_discovered_ips=ignore_discovered_ips)
3645+ unmanaged_in_use |= unused
3646+ not_in_use = unmanaged_in_use.get_unused_ranges(
3647+ self.get_ipnetwork(), purpose=MAASIPRANGE_TYPE.UNUSED)
3648+ return not_in_use
3649
3650 def get_maasipset_for_neighbours(self) -> MAASIPSet:
3651 """Return the observed neighbours in this subnet.
3652
3653=== modified file 'src/maasserver/models/tests/test_discovery.py'
3654--- src/maasserver/models/tests/test_discovery.py 2016-11-02 20:18:07 +0000
3655+++ src/maasserver/models/tests/test_discovery.py 2016-12-07 15:50:52 +0000
3656@@ -16,6 +16,7 @@
3657 from maasserver.testing.testcase import MAASServerTestCase
3658 from maastesting.matchers import (
3659 DocTestMatches,
3660+ IsNonEmptyString,
3661 Matches,
3662 MockCalledOnceWith,
3663 MockNotCalled,
3664@@ -34,7 +35,7 @@
3665
3666 def test_mac_organization(self):
3667 discovery = factory.make_Discovery(mac_address="48:51:b7:00:00:00")
3668- self.assertThat(discovery.mac_organization, Equals("Intel Corporate"))
3669+ self.assertThat(discovery.mac_organization, IsNonEmptyString)
3670
3671 def test__ignores_duplicate_macs(self):
3672 rack1 = factory.make_RackController()
3673
3674=== modified file 'src/maasserver/models/tests/test_event.py'
3675--- src/maasserver/models/tests/test_event.py 2015-12-01 18:12:59 +0000
3676+++ src/maasserver/models/tests/test_event.py 2016-12-07 15:50:52 +0000
3677@@ -11,6 +11,7 @@
3678 from django.db import IntegrityError
3679 from maasserver.models import (
3680 Event,
3681+ event as event_module,
3682 EventType,
3683 )
3684 from maasserver.testing.factory import factory
3685@@ -84,6 +85,15 @@
3686 self.assertIsNotNone(EventType.objects.get(name=event_type))
3687 self.assertIsNotNone(Event.objects.get(node=node))
3688
3689+ def test_create_region_event_creates_region_event(self):
3690+ region = factory.make_RegionRackController()
3691+ self.patch(event_module, 'get_maas_id').return_value = region.system_id
3692+ Event.objects.create_region_event(
3693+ event_type=EVENT_TYPES.REGION_IMPORT_ERROR)
3694+ self.assertIsNotNone(
3695+ EventType.objects.get(name=EVENT_TYPES.REGION_IMPORT_ERROR))
3696+ self.assertIsNotNone(Event.objects.get(node=region))
3697+
3698 def test_register_event_and_event_type_handles_integrity_errors(self):
3699 # It's possible that two calls to
3700 # register_event_and_event_type() could arrive at more-or-less
3701
3702=== modified file 'src/maasserver/models/tests/test_neighbour.py'
3703--- src/maasserver/models/tests/test_neighbour.py 2016-08-19 11:40:52 +0000
3704+++ src/maasserver/models/tests/test_neighbour.py 2016-12-07 15:50:52 +0000
3705@@ -7,11 +7,11 @@
3706
3707 from maasserver.testing.factory import factory
3708 from maasserver.testing.testcase import MAASServerTestCase
3709-from testtools.matchers import Equals
3710+from maastesting.matchers import IsNonEmptyString
3711
3712
3713 class TestNeighbourModel(MAASServerTestCase):
3714
3715 def test_mac_organization(self):
3716 neighbour = factory.make_Neighbour(mac_address="48:51:b7:00:00:00")
3717- self.assertThat(neighbour.mac_organization, Equals("Intel Corporate"))
3718+ self.assertThat(neighbour.mac_organization, IsNonEmptyString)
3719
3720=== modified file 'src/maasserver/models/tests/test_node.py'
3721--- src/maasserver/models/tests/test_node.py 2016-12-07 11:26:49 +0000
3722+++ src/maasserver/models/tests/test_node.py 2016-12-07 15:50:52 +0000
3723@@ -60,6 +60,7 @@
3724 BondInterface,
3725 BootResource,
3726 BridgeInterface,
3727+ Chassis,
3728 Config,
3729 Controller,
3730 Device,
3731@@ -78,6 +79,7 @@
3732 RegionRackRPCConnection,
3733 Service,
3734 Space,
3735+ Storage,
3736 Subnet,
3737 UnknownInterface,
3738 VLAN,
3739@@ -95,8 +97,6 @@
3740 GatewayDefinition,
3741 generate_node_system_id,
3742 PowerInfo,
3743- typecast_node,
3744- typecast_to_node_type,
3745 )
3746 from maasserver.models.signals import power as node_query
3747 from maasserver.models.timestampedmodel import now
3748@@ -137,6 +137,7 @@
3749 from maasserver.worker_user import get_worker_user
3750 from maastesting.matchers import (
3751 DocTestMatches,
3752+ IsNonEmptyString,
3753 MockCalledOnce,
3754 MockCalledOnceWith,
3755 MockCallsMatch,
3756@@ -154,12 +155,11 @@
3757 disk_erasing,
3758 )
3759 from netaddr import IPAddress
3760+from provisioningserver.drivers.power import PowerDriverRegistry
3761 from provisioningserver.events import (
3762 EVENT_DETAILS,
3763 EVENT_TYPES,
3764 )
3765-from provisioningserver.power import QUERY_POWER_TYPES
3766-from provisioningserver.power.schema import JSON_POWER_TYPE_PARAMETERS
3767 from provisioningserver.rpc.cluster import (
3768 AddChassis,
3769 DisableAndShutoffRackd,
3770@@ -225,59 +225,86 @@
3771 "... after 1000 iterations ... no unused node identifiers."))
3772
3773
3774-class TestTypeCastNode(MAASServerTestCase):
3775- def test_all_node_types_can_be_casted(self):
3776- node = factory.make_Node()
3777- cast_to = random.choice(
3778- [Device, Machine, Node, RackController, RegionController])
3779- typecast_node(node, cast_to)
3780- self.assertIsInstance(node, cast_to)
3781-
3782- def test_rejects_casting_to_non_node_type_objects(self):
3783- node = factory.make_Node()
3784- self.assertRaises(AssertionError, typecast_node, node, object)
3785-
3786- def test_rejects_casting_non_node_type(self):
3787- node = object()
3788- cast_to = random.choice(
3789- [Device, Machine, Node, RackController, RegionController])
3790- self.assertRaises(AssertionError, typecast_node, node, cast_to)
3791-
3792- def test_sets_hostname_if_blank(self):
3793- node = factory.make_Node(hostname='')
3794- self.assertNotEqual('', node.hostname)
3795+def HasType(type_):
3796+ return AfterPreprocessing(type, Is(type_), annotate=False)
3797+
3798+
3799+def SharesStorageWith(other):
3800+ return AfterPreprocessing(
3801+ (lambda thing: thing.__dict__), Is(other.__dict__),
3802+ annotate=False)
3803
3804
3805 class TestTypeCastToNodeType(MAASServerTestCase):
3806+
3807+ def test_cast_to_self(self):
3808+ node = factory.make_Node().as_node()
3809+ node_types = set(map_enum(NODE_TYPE).values())
3810+ casts = {
3811+ NODE_TYPE.DEVICE: Device,
3812+ NODE_TYPE.MACHINE: Machine,
3813+ NODE_TYPE.RACK_CONTROLLER: RackController,
3814+ NODE_TYPE.REGION_AND_RACK_CONTROLLER: RackController,
3815+ NODE_TYPE.REGION_CONTROLLER: RegionController,
3816+ NODE_TYPE.CHASSIS: Chassis,
3817+ NODE_TYPE.STORAGE: Storage,
3818+ }
3819+ self.assertThat(casts.keys(), Equals(node_types))
3820+ for node_type, cast_type in casts.items():
3821+ node.node_type = node_type
3822+ node_as_self = node.as_self()
3823+ self.assertThat(node, HasType(Node))
3824+ self.assertThat(node_as_self, HasType(cast_type))
3825+ self.assertThat(node_as_self, SharesStorageWith(node))
3826+
3827 def test_cast_to_machine(self):
3828- node = factory.make_Node(node_type=NODE_TYPE.MACHINE)
3829- machine = typecast_to_node_type(node)
3830- self.assertIsInstance(machine, Machine)
3831+ node = factory.make_Node().as_node()
3832+ machine = node.as_machine()
3833+ self.assertThat(node, HasType(Node))
3834+ self.assertThat(machine, HasType(Machine))
3835+ self.assertThat(machine, SharesStorageWith(node))
3836
3837 def test_cast_to_rack_controller(self):
3838- node = factory.make_Node(node_type=NODE_TYPE.RACK_CONTROLLER)
3839- rack = typecast_to_node_type(node)
3840- self.assertIsInstance(rack, RackController)
3841-
3842- def test_cast_to_region_and_rack_controller(self):
3843- node = factory.make_Node(
3844- node_type=NODE_TYPE.REGION_AND_RACK_CONTROLLER)
3845- rack = typecast_to_node_type(node)
3846- self.assertIsInstance(rack, RackController)
3847+ node = factory.make_Node().as_node()
3848+ rack = node.as_rack_controller()
3849+ self.assertThat(node, HasType(Node))
3850+ self.assertThat(rack, HasType(RackController))
3851+ self.assertThat(rack, SharesStorageWith(node))
3852
3853 def test_cast_to_region_controller(self):
3854- node = factory.make_Node(node_type=NODE_TYPE.REGION_CONTROLLER)
3855- region = typecast_to_node_type(node)
3856- self.assertIsInstance(region, RegionController)
3857+ node = factory.make_Node().as_node()
3858+ region = node.as_region_controller()
3859+ self.assertThat(node, HasType(Node))
3860+ self.assertThat(region, HasType(RegionController))
3861+ self.assertThat(region, SharesStorageWith(node))
3862
3863 def test_cast_to_device(self):
3864- node = factory.make_Node(node_type=NODE_TYPE.DEVICE)
3865- device = typecast_to_node_type(node)
3866- self.assertIsInstance(device, Device)
3867-
3868- def test_throws_exception_on_unknown_type(self):
3869- node = factory.make_Node(node_type=random.randint(10, 10000))
3870- self.assertRaises(NotImplementedError, typecast_to_node_type, node)
3871+ node = factory.make_Node().as_node()
3872+ device = node.as_device()
3873+ self.assertThat(node, HasType(Node))
3874+ self.assertThat(device, HasType(Device))
3875+ self.assertThat(device, SharesStorageWith(node))
3876+
3877+ def test_cast_to_node(self):
3878+ machine = factory.make_Machine()
3879+ node = machine.as_node()
3880+ self.assertThat(machine, HasType(Machine))
3881+ self.assertThat(node, HasType(Node))
3882+ self.assertThat(node, SharesStorageWith(machine))
3883+
3884+ def test_cast_to_chassis(self):
3885+ node = factory.make_Node().as_node()
3886+ chassis = node.as_chassis()
3887+ self.assertThat(node, HasType(Node))
3888+ self.assertThat(chassis, HasType(Chassis))
3889+ self.assertThat(chassis, SharesStorageWith(node))
3890+
3891+ def test_cast_to_storage(self):
3892+ node = factory.make_Node().as_node()
3893+ storage = node.as_storage()
3894+ self.assertThat(node, HasType(Node))
3895+ self.assertThat(storage, HasType(Storage))
3896+ self.assertThat(storage, SharesStorageWith(node))
3897
3898
3899 class TestNodeManager(MAASServerTestCase):
3900@@ -1229,12 +1256,11 @@
3901 sentinel.power_parameters), node.get_effective_power_info())
3902
3903 def test_get_effective_power_info_cant_be_queried(self):
3904- all_power_types = {
3905- power_type_details['name']
3906- for power_type_details in JSON_POWER_TYPE_PARAMETERS
3907- }
3908- uncontrolled_power_types = all_power_types.difference(
3909- QUERY_POWER_TYPES)
3910+ uncontrolled_power_types = [
3911+ driver.name
3912+ for _, driver in PowerDriverRegistry
3913+ if not driver.queryable
3914+ ]
3915 for power_type in uncontrolled_power_types:
3916 node = factory.make_Node(power_type=power_type)
3917 gepp = self.patch(node, "get_effective_power_parameters")
3918@@ -1245,7 +1271,11 @@
3919 node.get_effective_power_info())
3920
3921 def test_get_effective_power_info_can_be_queried(self):
3922- power_type = random.choice(QUERY_POWER_TYPES)
3923+ power_type = random.choice([
3924+ driver.name
3925+ for _, driver in PowerDriverRegistry
3926+ if driver.queryable
3927+ ])
3928 node = factory.make_Node(power_type=power_type)
3929 gepp = self.patch(node, "get_effective_power_parameters")
3930 self.assertEqual(
3931@@ -1499,7 +1529,8 @@
3932 node_start = self.patch(node, '_start')
3933 # Return a post-commit hook from Node.start().
3934 node_start.side_effect = (
3935- lambda user, user_data, old_status: post_commit())
3936+ lambda user, user_data, old_status, allow_power_cycle: (
3937+ post_commit()))
3938 Config.objects.set_config('disk_erase_with_secure_erase', True)
3939 Config.objects.set_config('disk_erase_with_quick_erase', True)
3940 with post_commit_hooks:
3941@@ -1521,7 +1552,8 @@
3942 node_start = self.patch(node, '_start')
3943 # Return a post-commit hook from Node.start().
3944 node_start.side_effect = (
3945- lambda user, user_data, old_status: post_commit())
3946+ lambda user, user_data, old_status, allow_power_cycle: (
3947+ post_commit()))
3948 Config.objects.set_config('disk_erase_with_secure_erase', False)
3949 Config.objects.set_config('disk_erase_with_quick_erase', False)
3950 with post_commit_hooks:
3951@@ -1543,14 +1575,17 @@
3952 node_start = self.patch(node, '_start')
3953 # Return a post-commit hook from Node.start().
3954 node_start.side_effect = (
3955- lambda user, user_data, old_status: post_commit())
3956+ lambda user, user_data, old_status, allow_power_cycle: (
3957+ post_commit()))
3958 with post_commit_hooks:
3959 node.start_disk_erasing(owner)
3960 self.expectThat(node.owner, Equals(owner))
3961 self.expectThat(node.status, Equals(NODE_STATUS.DISK_ERASING))
3962 self.expectThat(node.agent_name, Equals(agent_name))
3963 self.assertThat(
3964- node_start, MockCalledOnceWith(owner, ANY, NODE_STATUS.ALLOCATED))
3965+ node_start,
3966+ MockCalledOnceWith(
3967+ owner, ANY, NODE_STATUS.ALLOCATED, allow_power_cycle=True))
3968
3969 def test_start_disk_erasing_logs_user_request(self):
3970 owner = factory.make_User()
3971@@ -1558,12 +1593,15 @@
3972 node_start = self.patch(node, '_start')
3973 # Return a post-commit hook from Node.start().
3974 node_start.side_effect = (
3975- lambda user, user_data, old_status: post_commit())
3976+ lambda user, user_data, old_status, allow_power_cycle: (
3977+ post_commit()))
3978 register_event = self.patch(node, '_register_request_event')
3979 with post_commit_hooks:
3980 node.start_disk_erasing(owner)
3981 self.assertThat(
3982- node_start, MockCalledOnceWith(owner, ANY, NODE_STATUS.ALLOCATED))
3983+ node_start,
3984+ MockCalledOnceWith(
3985+ owner, ANY, NODE_STATUS.ALLOCATED, allow_power_cycle=True))
3986 self.assertThat(register_event, MockCalledOnceWith(
3987 owner, EVENT_TYPES.REQUEST_NODE_ERASE_DISK,
3988 action='start disk erasing', comment=None))
3989@@ -1626,7 +1664,8 @@
3990
3991 self.assertThat(
3992 node_start, MockCalledOnceWith(
3993- admin, generate_user_data.return_value, NODE_STATUS.ALLOCATED))
3994+ admin, generate_user_data.return_value, NODE_STATUS.ALLOCATED,
3995+ allow_power_cycle=True))
3996 self.assertEqual(NODE_STATUS.FAILED_DISK_ERASING, node.status)
3997
3998 def test_start_disk_erasing_sets_status_on_post_commit_error(self):
3999@@ -1807,20 +1846,18 @@
4000 }
4001 # Use an "uncontrolled" power type (i.e. a power type for which we
4002 # cannot query the status of the node).
4003- all_power_types = {
4004- power_type_details['name']
4005- for power_type_details in JSON_POWER_TYPE_PARAMETERS
4006- }
4007- uncontrolled_power_types = (
4008- all_power_types.difference(QUERY_POWER_TYPES))
4009- power_type = random.choice(list(uncontrolled_power_types))
4010+ power_type = random.choice([
4011+ driver.name
4012+ for _, driver in PowerDriverRegistry
4013+ if not driver.queryable
4014+ ])
4015 rack = factory.make_RackController()
4016 node = factory.make_Node_with_Interface_on_Subnet(
4017 status=NODE_STATUS.ALLOCATED, owner=owner, owner_data=owner_data,
4018 agent_name=agent_name, power_type=power_type, primary_rack=rack)
4019 self.patch(Node, '_set_status_expires')
4020 mock_stop = self.patch(node, "_stop")
4021- mock_release_to_ready = self.patch(node, "_release_to_ready")
4022+ mock_finalize_release = self.patch(node, "_finalize_release")
4023 node.power_state = POWER_STATE.ON
4024 node.release()
4025 self.expectThat(Node._set_status_expires, MockNotCalled())
4026@@ -1833,7 +1870,7 @@
4027 self.expectThat(node.distro_series, Equals(''))
4028 self.expectThat(node.license_key, Equals(''))
4029 self.expectThat(mock_stop, MockCalledOnceWith(node.owner))
4030- self.expectThat(mock_release_to_ready, MockCalledOnceWith())
4031+ self.expectThat(mock_finalize_release, MockCalledOnceWith())
4032
4033 def test_release_node_that_has_power_off(self):
4034 agent_name = factory.make_name('agent-name')
4035@@ -1879,6 +1916,16 @@
4036 [], list(NodeResult.objects.filter(
4037 node=node, result_type=RESULT_TYPE.INSTALLATION)))
4038
4039+ def test_release_deletes_dynamic_machine(self):
4040+ agent_name = factory.make_name('agent-name')
4041+ owner = factory.make_User()
4042+ node = factory.make_Node(
4043+ status=NODE_STATUS.ALLOCATED, owner=owner, agent_name=agent_name,
4044+ dynamic=True, power_state=POWER_STATE.OFF)
4045+ with post_commit_hooks:
4046+ node.release()
4047+ self.assertIsNone(reload_object(node))
4048+
4049 def test_dynamic_ip_addresses_from_ip_address_table(self):
4050 node = factory.make_Node()
4051 interfaces = [
4052@@ -2233,7 +2280,8 @@
4053 node_start = self.patch(node, '_start')
4054 # Return a post-commit hook from Node.start().
4055 node_start.side_effect = (
4056- lambda user, user_data, old_status: post_commit())
4057+ lambda user, user_data, old_status, allow_power_cycle: (
4058+ post_commit()))
4059 admin = factory.make_admin()
4060 node.start_commissioning(admin)
4061 post_commit_hooks.reset() # Ignore these for now.
4062@@ -2243,7 +2291,7 @@
4063 }
4064 self.assertAttributes(node, expected_attrs)
4065 self.assertThat(node_start, MockCalledOnceWith(
4066- admin, ANY, NODE_STATUS.NEW))
4067+ admin, ANY, NODE_STATUS.NEW, allow_power_cycle=True))
4068
4069 def test_start_commissioning_sets_options(self):
4070 rack = factory.make_RackController()
4071@@ -2253,7 +2301,8 @@
4072 node_start = self.patch(node, '_start')
4073 # Return a post-commit hook from Node.start().
4074 node_start.side_effect = (
4075- lambda user, user_data, old_status: post_commit())
4076+ lambda user, user_data, old_status, allow_power_cycle: (
4077+ post_commit()))
4078 admin = factory.make_admin()
4079 enable_ssh = factory.pick_bool()
4080 skip_networking = factory.pick_bool()
4081@@ -2274,7 +2323,8 @@
4082 node = factory.make_Node(status=NODE_STATUS.NEW)
4083 node_start = self.patch(node, '_start')
4084 node_start.side_effect = (
4085- lambda user, user_data, old_status: post_commit())
4086+ lambda user, user_data, old_status, allow_power_cycle: (
4087+ post_commit()))
4088 user_data = factory.make_string().encode('ascii')
4089 generate_user_data = self.patch(
4090 commissioning, 'generate_user_data')
4091@@ -2283,13 +2333,14 @@
4092 node.start_commissioning(admin)
4093 post_commit_hooks.reset() # Ignore these for now.
4094 self.assertThat(node_start, MockCalledOnceWith(
4095- admin, user_data, NODE_STATUS.NEW))
4096+ admin, user_data, NODE_STATUS.NEW, allow_power_cycle=True))
4097
4098 def test_start_commissioning_sets_min_hwe_kernel(self):
4099 node = factory.make_Node(status=NODE_STATUS.NEW)
4100 node_start = self.patch(node, '_start')
4101 node_start.side_effect = (
4102- lambda user, user_data, old_status: post_commit())
4103+ lambda user, user_data, old_status, allow_power_cycle: (
4104+ post_commit()))
4105 user_data = factory.make_string().encode('ascii')
4106 generate_user_data = self.patch(
4107 commissioning, 'generate_user_data')
4108@@ -2300,11 +2351,34 @@
4109 post_commit_hooks.reset() # Ignore these for now.
4110 self.assertEqual('hwe-v', node.min_hwe_kernel)
4111
4112+ def test_start_commissioning_starts_node_if_already_on(self):
4113+ node = factory.make_Node(
4114+ interface=True, status=NODE_STATUS.NEW, power_type='manual',
4115+ power_state=POWER_STATE.ON)
4116+ node_start = self.patch(node, '_start')
4117+ # Return a post-commit hook from Node.start().
4118+ node_start.side_effect = (
4119+ lambda user, user_data, old_status, allow_power_cycle: (
4120+ post_commit()))
4121+ admin = factory.make_admin()
4122+ node.start_commissioning(admin)
4123+ post_commit_hooks.reset() # Ignore these for now.
4124+ node = reload_object(node)
4125+ expected_attrs = {
4126+ 'status': NODE_STATUS.COMMISSIONING,
4127+ 'owner': admin,
4128+ }
4129+ self.assertAttributes(node, expected_attrs)
4130+ self.expectThat(node.owner, Equals(admin))
4131+ self.assertThat(node_start, MockCalledOnceWith(
4132+ admin, ANY, NODE_STATUS.NEW, allow_power_cycle=True))
4133+
4134 def test_start_commissioning_clears_node_commissioning_results(self):
4135 node = factory.make_Node(status=NODE_STATUS.NEW)
4136 node_start = self.patch(node, '_start')
4137 node_start.side_effect = (
4138- lambda user, user_data, old_status: post_commit())
4139+ lambda user, user_data, old_status, allow_power_cycle: (
4140+ post_commit()))
4141 NodeResult.objects.store_data(
4142 node, factory.make_string(),
4143 random.randint(0, 10),
4144@@ -2318,7 +2392,8 @@
4145 node = factory.make_Node(status=NODE_STATUS.NEW)
4146 node_start = self.patch(node, '_start')
4147 node_start.side_effect = (
4148- lambda user, user_data, old_status: post_commit())
4149+ lambda user, user_data, old_status, allow_power_cycle: (
4150+ post_commit()))
4151 clear_storage = self.patch_autospec(
4152 node, '_clear_full_storage_configuration')
4153 admin = factory.make_admin()
4154@@ -2330,7 +2405,8 @@
4155 node = factory.make_Node(status=NODE_STATUS.NEW)
4156 node_start = self.patch(node, '_start')
4157 node_start.side_effect = (
4158- lambda user, user_data, old_status: post_commit())
4159+ lambda user, user_data, old_status, allow_power_cycle: (
4160+ post_commit()))
4161 clear_storage = self.patch_autospec(
4162 node, '_clear_full_storage_configuration')
4163 admin = factory.make_admin()
4164@@ -2342,7 +2418,8 @@
4165 node = factory.make_Node(status=NODE_STATUS.NEW)
4166 node_start = self.patch(node, '_start')
4167 node_start.side_effect = (
4168- lambda user, user_data, old_status: post_commit())
4169+ lambda user, user_data, old_status, allow_power_cycle: (
4170+ post_commit()))
4171 clear_networking = self.patch_autospec(
4172 node, '_clear_networking_configuration')
4173 admin = factory.make_admin()
4174@@ -2354,7 +2431,8 @@
4175 node = factory.make_Node(status=NODE_STATUS.NEW)
4176 node_start = self.patch(node, '_start')
4177 node_start.side_effect = (
4178- lambda user, user_data, old_status: post_commit())
4179+ lambda user, user_data, old_status, allow_power_cycle: (
4180+ post_commit()))
4181 clear_networking = self.patch_autospec(
4182 node, '_clear_networking_configuration')
4183 admin = factory.make_admin()
4184@@ -2398,7 +2476,8 @@
4185 self.assertThat(
4186 node_start,
4187 MockCalledOnceWith(
4188- admin, generate_user_data.return_value, NODE_STATUS.NEW))
4189+ admin, generate_user_data.return_value, NODE_STATUS.NEW,
4190+ allow_power_cycle=True))
4191 self.assertEqual(NODE_STATUS.NEW, node.status)
4192
4193 def test_start_commissioning_logs_and_raises_errors_in_starting(self):
4194@@ -2422,7 +2501,8 @@
4195 node_start = self.patch(node, '_start')
4196 # Return a post-commit hook from Node.start().
4197 node_start.side_effect = (
4198- lambda user, user_data, old_status: post_commit())
4199+ lambda user, user_data, old_status, allow_power_cycle: (
4200+ post_commit()))
4201 admin = factory.make_admin()
4202 node.start_commissioning(admin)
4203 post_commit_hooks.reset() # Ignore these for now.
4204@@ -2640,11 +2720,10 @@
4205
4206 def test_full_clean_checks_architecture_for_installable_nodes(self):
4207 device = factory.make_Device(architecture='')
4208- # Set type here so we don't cause exception while creating object
4209- node = typecast_node(device, Node)
4210- node.node_type = factory.pick_enum(
4211+ device.node_type = factory.pick_enum(
4212 NODE_TYPE, but_not=[NODE_TYPE.DEVICE])
4213- exception = self.assertRaises(ValidationError, node.full_clean)
4214+ exception = self.assertRaises(
4215+ ValidationError, device.as_node().full_clean)
4216 self.assertEqual(
4217 exception.message_dict,
4218 {'architecture':
4219@@ -2985,9 +3064,7 @@
4220 INTERFACE_TYPE.PHYSICAL, mac_address='ec:a8:6b:fd:ae:3f',
4221 node=node)
4222 node.save()
4223- self.assertEqual(
4224- "ELITEGROUP COMPUTER SYSTEMS CO., LTD.",
4225- node.get_pxe_mac_vendor())
4226+ self.assertThat(node.get_pxe_mac_vendor(), IsNonEmptyString)
4227
4228 def test_get_extra_macs_returns_all_but_boot_interface_mac(self):
4229 node = factory.make_Node()
4230@@ -4914,10 +4991,12 @@
4231 register_view("maasserver_discovery")
4232
4233 def make_acquired_node_with_interface(
4234- self, user, bmc_connected_to=None, power_type="virsh"):
4235+ self, user, bmc_connected_to=None, power_type="virsh",
4236+ power_state=POWER_STATE.OFF):
4237 node = factory.make_Node_with_Interface_on_Subnet(
4238 status=NODE_STATUS.READY, with_boot_disk=True,
4239- bmc_connected_to=bmc_connected_to, power_type=power_type)
4240+ bmc_connected_to=bmc_connected_to, power_type=power_type,
4241+ power_state=power_state)
4242 node.acquire(user)
4243 return node
4244
4245@@ -5035,6 +5114,24 @@
4246 node.system_id, status=old_status),
4247 call(callOutToDatabase, node.release_interface_config)))
4248
4249+ def test__calls_power_cycle_when_cycling_allowed(self):
4250+ user = factory.make_User()
4251+ node = self.make_acquired_node_with_interface(
4252+ user, power_state=POWER_STATE.ON)
4253+
4254+ post_commit_defer = self.patch(node_module, "post_commit")
4255+ mock_power_control = self.patch(Node, "_power_control_node")
4256+ mock_power_control.return_value = post_commit_defer
4257+
4258+ # Power cycling is allowed when starting deployment. This node is
4259+ # allocated and the power_state is ON. Power cycle should be called
4260+ # instead of power_on.
4261+ node.start(user)
4262+
4263+ # Calls _power_control_node when power_cycle.
4264+ self.assertThat(
4265+ mock_power_control, MockCalledOnceWith(ANY, power_cycle, ANY))
4266+
4267 def test_storage_layout_issues_returns_invalid_no_boot_arm64_non_efi(self):
4268 node = factory.make_Node(
4269 architecture="arm64/generic", bios_boot_method="pxe")
4270@@ -5640,8 +5737,7 @@
4271 )
4272
4273 def create_empty_controller(self):
4274- node = factory.make_Node(node_type=self.node_type)
4275- return typecast_to_node_type(node)
4276+ return factory.make_Node(node_type=self.node_type).as_self()
4277
4278 def test__order_of_calls_to_update_interface_is_always_the_same(self):
4279 controller = self.create_empty_controller()
4280@@ -8369,7 +8465,7 @@
4281 region_and_rack = factory.make_Node(
4282 node_type=NODE_TYPE.REGION_AND_RACK_CONTROLLER)
4283 system_id = region_and_rack.system_id
4284- typecast_node(region_and_rack, RackController).delete()
4285+ region_and_rack.as_rack_controller().delete()
4286 self.assertEquals(
4287 NODE_TYPE.REGION_CONTROLLER,
4288 Node.objects.get(system_id=system_id).node_type)
4289@@ -8576,7 +8672,7 @@
4290 def test_delete_converts_region_and_rack_to_rack(self):
4291 region_and_rack = factory.make_Node(
4292 node_type=NODE_TYPE.REGION_AND_RACK_CONTROLLER)
4293- typecast_node(region_and_rack, RegionController).delete()
4294+ region_and_rack.as_region_controller().delete()
4295 self.assertEquals(
4296 NODE_TYPE.RACK_CONTROLLER,
4297 Node.objects.get(system_id=region_and_rack.system_id).node_type)
4298@@ -8776,3 +8872,25 @@
4299 self.assertThat(monitoring_state, Contains('eth2'))
4300 self.assertThat(
4301 monitoring_state['eth1'], Equals(eth1.get_discovery_state()))
4302+
4303+
4304+class TestChassis(MAASServerTestCase):
4305+
4306+ def test__domain_is_always_empty(self):
4307+ hostname = factory.make_hostname()
4308+ domain = factory.make_name("domain")
4309+ chassis = factory.make_Chassis(
4310+ hostname="%s.%s" % (hostname, domain))
4311+ self.assertEquals(hostname, chassis.hostname)
4312+ self.assertIsNone(chassis.domain)
4313+
4314+
4315+class TestStorage(MAASServerTestCase):
4316+
4317+ def test__domain_is_always_empty(self):
4318+ hostname = factory.make_hostname()
4319+ domain = factory.make_name("domain")
4320+ storage = factory.make_Storage(
4321+ hostname="%s.%s" % (hostname, domain))
4322+ self.assertEquals(hostname, storage.hostname)
4323+ self.assertIsNone(storage.domain)
4324
4325=== modified file 'src/maasserver/models/tests/test_staticipaddress.py'
4326--- src/maasserver/models/tests/test_staticipaddress.py 2016-12-07 15:03:00 +0000
4327+++ src/maasserver/models/tests/test_staticipaddress.py 2016-12-07 15:50:52 +0000
4328@@ -9,11 +9,12 @@
4329 randint,
4330 shuffle,
4331 )
4332+import threading
4333 from unittest import skip
4334 from unittest.mock import sentinel
4335
4336 from django.core.exceptions import ValidationError
4337-from django.db import transaction
4338+from django.db import IntegrityError
4339 from maasserver import locks
4340 from maasserver.dbviews import register_view
4341 from maasserver.enum import (
4342@@ -41,29 +42,39 @@
4343 MAASServerTestCase,
4344 MAASTransactionServerTestCase,
4345 )
4346-from maasserver.utils.dns import get_ip_based_hostname
4347+<<<<<<< TREE
4348+from maasserver.utils.dns import get_ip_based_hostname
4349+=======
4350+from maasserver.utils import orm
4351+from maasserver.utils.dns import get_ip_based_hostname
4352+>>>>>>> MERGE-SOURCE
4353 from maasserver.utils.orm import (
4354 reload_object,
4355- RetryTransaction,
4356 transactional,
4357 )
4358 from maasserver.websockets.base import dehydrate_datetime
4359-from maastesting.matchers import (
4360- MockCalledOnceWith,
4361- MockNotCalled,
4362-)
4363 from netaddr import IPAddress
4364+from psycopg2.errorcodes import FOREIGN_KEY_VIOLATION
4365 from testtools import ExpectedException
4366 from testtools.matchers import (
4367+ AfterPreprocessing,
4368+ AllMatch,
4369 Contains,
4370 Equals,
4371 HasLength,
4372+ Is,
4373+ IsInstance,
4374 Not,
4375 )
4376+from twisted.python.failure import Failure
4377
4378
4379 class TestStaticIPAddressManager(MAASServerTestCase):
4380
4381+ def setUp(self):
4382+ super(TestStaticIPAddressManager, self).setUp()
4383+ register_view("maasserver_discovery")
4384+
4385 def test_filter_by_ip_family_ipv4(self):
4386 network_v4 = factory.make_ipv4_network()
4387 subnet_v4 = factory.make_Subnet(cidr=str(network_v4.cidr))
4388@@ -134,36 +145,21 @@
4389 StaticIPAddress.objects.filter_by_subnet_cidr_family(
4390 IPADDRESS_FAMILY.IPv6))
4391
4392-
4393-class TestStaticIPAddressManagerTransactional(MAASTransactionServerTestCase):
4394- """The following TestStaticIPAddressManager tests require
4395- MAASTransactionServerTestCase, and thus have been separated from the
4396- TestStaticIPAddressManager above.
4397- """
4398-
4399- def setUp(self):
4400- register_view("maasserver_discovery")
4401- return super().setUp()
4402-
4403 def test_allocate_new_returns_ip_in_correct_range(self):
4404- with transaction.atomic():
4405- subnet = factory.make_managed_Subnet()
4406- with transaction.atomic():
4407- ipaddress = StaticIPAddress.objects.allocate_new(subnet)
4408+ subnet = factory.make_managed_Subnet()
4409+ ipaddress = StaticIPAddress.objects.allocate_new(subnet)
4410 self.assertIsInstance(ipaddress, StaticIPAddress)
4411 self.assertTrue(
4412 subnet.is_valid_static_ip(ipaddress.ip),
4413 "%s: not valid for subnet with reserved IPs: %r" % (
4414 ipaddress.ip, subnet.get_ipranges_in_use()))
4415
4416- @transactional
4417 def test_allocate_new_allocates_IPv6_address(self):
4418- subnet = factory.make_managed_ipv6_Subnet()
4419+ subnet = factory.make_managed_Subnet(ipv6=True)
4420 ipaddress = StaticIPAddress.objects.allocate_new(subnet)
4421 self.assertIsInstance(ipaddress, StaticIPAddress)
4422 self.assertTrue(subnet.is_valid_static_ip(ipaddress.ip))
4423
4424- @transactional
4425 def test_allocate_new_sets_user(self):
4426 subnet = factory.make_managed_Subnet()
4427 user = factory.make_User()
4428@@ -171,7 +167,6 @@
4429 subnet=subnet, alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=user)
4430 self.assertEqual(user, ipaddress.user)
4431
4432- @transactional
4433 def test_allocate_new_with_user_disallows_wrong_alloc_types(self):
4434 subnet = factory.make_managed_Subnet()
4435 user = factory.make_User()
4436@@ -185,7 +180,6 @@
4437 StaticIPAddress.objects.allocate_new(
4438 subnet, user=user, alloc_type=alloc_type)
4439
4440- @transactional
4441 def test_allocate_new_with_reserved_type_requires_a_user(self):
4442 subnet = factory.make_managed_Subnet()
4443 with ExpectedException(AssertionError):
4444@@ -196,18 +190,15 @@
4445 # Django has a bug that casts IP addresses with HOST(), which
4446 # results in alphabetical comparisons of strings instead of IP
4447 # addresses. See https://bugs.launchpad.net/maas/+bug/1338452
4448- with transaction.atomic():
4449- subnet = factory.make_Subnet(
4450- cidr='10.0.0.0/24', gateway_ip='10.0.0.1')
4451- factory.make_IPRange(subnet, '10.0.0.2', '10.0.0.97')
4452- factory.make_IPRange(subnet, '10.0.0.101', '10.0.0.254')
4453- factory.make_StaticIPAddress("10.0.0.99", subnet=subnet)
4454- subnet = reload_object(subnet)
4455- with transaction.atomic():
4456- ipaddress = StaticIPAddress.objects.allocate_new(subnet)
4457- self.assertEqual(ipaddress.ip, "10.0.0.98")
4458+ subnet = factory.make_Subnet(
4459+ cidr='10.0.0.0/24', gateway_ip='10.0.0.1')
4460+ factory.make_IPRange(subnet, '10.0.0.2', '10.0.0.97')
4461+ factory.make_IPRange(subnet, '10.0.0.101', '10.0.0.254')
4462+ factory.make_StaticIPAddress("10.0.0.99", subnet=subnet)
4463+ subnet = reload_object(subnet)
4464+ ipaddress = StaticIPAddress.objects.allocate_new(subnet)
4465+ self.assertEqual(ipaddress.ip, "10.0.0.98")
4466
4467- @transactional
4468 def test_allocate_new_returns_requested_IP_if_available(self):
4469 subnet = factory.make_Subnet(cidr='10.0.0.0/24')
4470 ipaddress = StaticIPAddress.objects.allocate_new(
4471@@ -220,7 +211,6 @@
4472 requested_address='10.0.0.1')
4473 self.assertEqual('10.0.0.1', ipaddress.ip)
4474
4475- @transactional
4476 def test_allocate_new_raises_when_requested_IP_unavailable(self):
4477 subnet = factory.make_ipv4_Subnet_with_IPRanges()
4478 requested_address = StaticIPAddress.objects.allocate_new(
4479@@ -235,28 +225,6 @@
4480 StaticIPAddress.objects.allocate_new(
4481 subnet, requested_address=requested_address)
4482
4483- @transactional
4484- def test_allocate_new_requests_transaction_retry_if_ip_taken(self):
4485- subnet = factory.make_ipv4_Subnet_with_IPRanges()
4486- # Simulate a "IP already taken" error.
4487- mock_attempt_allocation = self.patch(
4488- StaticIPAddress.objects, '_attempt_allocation')
4489- mock_attempt_allocation.side_effect = StaticIPAddressUnavailable()
4490- self.assertRaises(
4491- RetryTransaction, StaticIPAddress.objects.allocate_new, subnet)
4492-
4493- @transactional
4494- def test_allocate_new_does_not_use_lock_for_requested_ip(self):
4495- # When requesting a specific IP address, there's no need to
4496- # acquire the lock.
4497- lock = self.patch(locks, 'staticip_acquire')
4498- subnet = factory.make_Subnet(cidr='10.0.0.0/24')
4499- ipaddress = StaticIPAddress.objects.allocate_new(
4500- subnet, requested_address='10.0.0.1')
4501- self.assertIsInstance(ipaddress, StaticIPAddress)
4502- self.assertThat(lock.__enter__, MockNotCalled())
4503-
4504- @transactional
4505 def test_allocate_new_raises_when_requested_IP_out_of_network(self):
4506 subnet = factory.make_Subnet(cidr='10.0.0.0/24')
4507 requested_address = '10.0.1.1'
4508@@ -275,31 +243,28 @@
4509 str(e))
4510
4511 def test_allocate_new_raises_when_requested_IP_in_dynamic_range(self):
4512- with transaction.atomic():
4513- subnet = factory.make_ipv4_Subnet_with_IPRanges()
4514- dynamic_range = subnet.get_dynamic_ranges().first()
4515- requested_address = str(IPAddress(
4516- dynamic_range.netaddr_iprange.first))
4517- dynamic_range_end = str(IPAddress(
4518- dynamic_range.netaddr_iprange.last))
4519- subnet = reload_object(subnet)
4520- with transaction.atomic():
4521- e = self.assertRaises(
4522- StaticIPAddressUnavailable,
4523- StaticIPAddress.objects.allocate_new,
4524- subnet, factory.pick_enum(
4525- IPADDRESS_TYPE, but_not=[
4526- IPADDRESS_TYPE.DHCP,
4527- IPADDRESS_TYPE.DISCOVERED,
4528- IPADDRESS_TYPE.USER_RESERVED,
4529- ]),
4530- requested_address=requested_address)
4531- self.assertEqual(
4532- "%s is within the dynamic range from %s to %s" % (
4533- requested_address, requested_address, dynamic_range_end),
4534- str(e))
4535+ subnet = factory.make_ipv4_Subnet_with_IPRanges()
4536+ dynamic_range = subnet.get_dynamic_ranges().first()
4537+ requested_address = str(IPAddress(
4538+ dynamic_range.netaddr_iprange.first))
4539+ dynamic_range_end = str(IPAddress(
4540+ dynamic_range.netaddr_iprange.last))
4541+ subnet = reload_object(subnet)
4542+ e = self.assertRaises(
4543+ StaticIPAddressUnavailable,
4544+ StaticIPAddress.objects.allocate_new,
4545+ subnet, factory.pick_enum(
4546+ IPADDRESS_TYPE, but_not=[
4547+ IPADDRESS_TYPE.DHCP,
4548+ IPADDRESS_TYPE.DISCOVERED,
4549+ IPADDRESS_TYPE.USER_RESERVED,
4550+ ]),
4551+ requested_address=requested_address)
4552+ self.assertEqual(
4553+ "%s is within the dynamic range from %s to %s" % (
4554+ requested_address, requested_address, dynamic_range_end),
4555+ str(e))
4556
4557- @transactional
4558 def test_allocate_new_raises_when_alloc_type_is_None(self):
4559 error = self.assertRaises(
4560 ValueError, StaticIPAddress.objects.allocate_new,
4561@@ -308,7 +273,6 @@
4562 "IP address type None is not allowed to use allocate_new.",
4563 str(error))
4564
4565- @transactional
4566 def test_allocate_new_raises_when_alloc_type_is_not_allowed(self):
4567 error = self.assertRaises(
4568 ValueError, StaticIPAddress.objects.allocate_new,
4569@@ -317,32 +281,98 @@
4570 "IP address type 5 is not allowed to use allocate_new.",
4571 str(error))
4572
4573- @transactional
4574- def test_allocate_new_uses_staticip_acquire_lock(self):
4575- lock = self.patch(locks, 'staticip_acquire')
4576- subnet = factory.make_ipv4_Subnet_with_IPRanges()
4577- ipaddress = StaticIPAddress.objects.allocate_new(subnet)
4578- self.assertIsInstance(ipaddress, StaticIPAddress)
4579- self.assertThat(lock.__enter__, MockCalledOnceWith())
4580- self.assertThat(
4581- lock.__exit__, MockCalledOnceWith(None, None, None))
4582-
4583 def test_allocate_new_raises_when_addresses_exhausted(self):
4584 network = "192.168.230.0/24"
4585- with transaction.atomic():
4586- subnet = factory.make_Subnet(cidr=network)
4587- factory.make_IPRange(
4588- subnet, '192.168.230.1', '192.168.230.254',
4589- type=IPRANGE_TYPE.RESERVED)
4590- with transaction.atomic():
4591- e = self.assertRaises(
4592- StaticIPAddressExhaustion,
4593- StaticIPAddress.objects.allocate_new,
4594- subnet)
4595+ subnet = factory.make_Subnet(cidr=network)
4596+ factory.make_IPRange(
4597+ subnet, '192.168.230.1', '192.168.230.254',
4598+ type=IPRANGE_TYPE.RESERVED)
4599+ e = self.assertRaises(
4600+ StaticIPAddressExhaustion,
4601+ StaticIPAddress.objects.allocate_new,
4602+ subnet)
4603 self.assertEqual(
4604 "No more IPs available in subnet: %s." % subnet.cidr,
4605 str(e))
4606
4607+ def test_allocate_new_requests_retry_when_free_address_taken(self):
4608+ set_ip_address = self.patch(StaticIPAddress, "set_ip_address")
4609+ set_ip_address.side_effect = orm.make_unique_violation()
4610+ with orm.retry_context:
4611+ # A retry has been requested.
4612+ self.assertRaises(
4613+ orm.RetryTransaction, StaticIPAddress.objects.allocate_new,
4614+ subnet=factory.make_managed_Subnet())
4615+ # Aquisition of `address_allocation` is pending.
4616+ self.assertThat(
4617+ list(orm.retry_context.stack._cm_pending),
4618+ Equals([locks.address_allocation]))
4619+
4620+ def test_allocate_new_propagates_other_integrity_errors(self):
4621+ set_ip_address = self.patch(StaticIPAddress, "set_ip_address")
4622+ set_ip_address.side_effect = orm.make_unique_violation()
4623+ set_ip_address.side_effect.__cause__.pgcode = FOREIGN_KEY_VIOLATION
4624+ with orm.retry_context:
4625+ # An integrity error that's not `UNIQUE_VIOLATION` is propagated.
4626+ self.assertRaises(
4627+ IntegrityError, StaticIPAddress.objects.allocate_new,
4628+ subnet=factory.make_managed_Subnet())
4629+ # There is no pending retry context.
4630+ self.assertThat(
4631+ orm.retry_context.stack._cm_pending,
4632+ HasLength(0))
4633+
4634+
4635+class TestStaticIPAddressManagerTransactional(MAASTransactionServerTestCase):
4636+ """Transactional tests for `StaticIPAddressManager."""
4637+
4638+ scenarios = (
4639+ ("IPv4", dict(ip_version=4)),
4640+ ("IPv6", dict(ip_version=6)),
4641+ )
4642+
4643+ def test_allocate_new_works_under_extreme_concurrency(self):
4644+ register_view("maasserver_discovery")
4645+
4646+ ipv6 = (self.ip_version == 6)
4647+ subnet = factory.make_managed_Subnet(ipv6=ipv6)
4648+ count = 20 # Allocate this number of IP addresses.
4649+ concurrency = threading.Semaphore(16)
4650+ mutex = threading.Lock()
4651+ results = []
4652+
4653+ @transactional
4654+ def allocate():
4655+ return StaticIPAddress.objects.allocate_new(subnet)
4656+
4657+ def allocate_one():
4658+ try:
4659+ with concurrency:
4660+ sip = allocate()
4661+ except:
4662+ failure = Failure()
4663+ with mutex:
4664+ results.append(failure)
4665+ else:
4666+ with mutex:
4667+ results.append(sip)
4668+
4669+ threads = [
4670+ threading.Thread(target=allocate_one)
4671+ for _ in range(count)
4672+ ]
4673+
4674+ for thread in threads:
4675+ thread.start()
4676+ for thread in threads:
4677+ thread.join()
4678+
4679+ self.assertThat(results, AllMatch(IsInstance(StaticIPAddress)))
4680+ ips = {sip.ip for sip in results}
4681+ self.assertThat(ips, HasLength(count))
4682+ self.assertThat(ips, AllMatch(
4683+ AfterPreprocessing(subnet.is_valid_static_ip, Is(True))))
4684+
4685
4686 class TestStaticIPAddressManagerMapping(MAASServerTestCase):
4687 """Tests for get_hostname_ip_mapping()."""
4688
4689=== modified file 'src/maasserver/models/tests/test_subnet.py'
4690--- src/maasserver/models/tests/test_subnet.py 2016-10-18 00:19:51 +0000
4691+++ src/maasserver/models/tests/test_subnet.py 2016-12-07 15:50:52 +0000
4692@@ -35,6 +35,7 @@
4693 from maasserver.testing.factory import factory
4694 from maasserver.testing.orm import rollback
4695 from maasserver.testing.testcase import MAASServerTestCase
4696+from maasserver.utils.orm import reload_object
4697 from maastesting.matchers import DocTestMatches
4698 from netaddr import (
4699 AddrFormatError,
4700@@ -615,7 +616,7 @@
4701 parent, subnet.get_smallest_enclosing_sane_subnet())
4702
4703 def test_cannot_delete_with_dhcp_enabled(self):
4704- subnet = factory.make_managed_Subnet(ipv6=False)
4705+ subnet = factory.make_ipv4_Subnet_with_IPRanges()
4706 with ExpectedException(ValidationError, ".*servicing a dynamic.*"):
4707 subnet.delete()
4708
4709@@ -898,13 +899,37 @@
4710
4711 class TestSubnetGetNextIPForAllocation(MAASServerTestCase):
4712
4713+ scenarios = (
4714+ ("managed", {'managed': True}),
4715+ ("unmanaged", {'managed': False}),
4716+ )
4717+
4718+ def make_Subnet(self, *args, **kwargs):
4719+ """Helper to create a subnet for this test suite.
4720+
4721+ Eclipses the entire subnet with an IPRange of type RESERVED, so that
4722+ unmanaged and managed test scenarios are expected to behave the same.
4723+ """
4724+ cidr = kwargs.get('cidr')
4725+ network = IPNetwork(cidr)
4726+ # Note: these tests assume IPv4.
4727+ first = str(IPAddress(network.first + 1))
4728+ last = str(IPAddress(network.last - 1))
4729+ subnet = factory.make_Subnet(*args, managed=self.managed, **kwargs)
4730+ if not self.managed:
4731+ factory.make_IPRange(
4732+ subnet, start_ip=first, end_ip=last,
4733+ type=IPRANGE_TYPE.RESERVED)
4734+ subnet = reload_object(subnet)
4735+ return subnet
4736+
4737 def setUp(self):
4738 register_view("maasserver_discovery")
4739 return super().setUp()
4740
4741 def test__raises_if_no_free_addresses(self):
4742 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
4743- subnet = factory.make_Subnet(
4744+ subnet = self.make_Subnet(
4745 cidr="10.0.0.0/30", gateway_ip="10.0.0.1",
4746 dns_servers=["10.0.0.2"])
4747 with ExpectedException(
4748@@ -914,35 +939,39 @@
4749
4750 def test__allocates_next_free_address(self):
4751 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
4752- subnet = factory.make_Subnet(
4753- cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)
4754+ subnet = self.make_Subnet(
4755+ cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None,
4756+ )
4757 ip = subnet.get_next_ip_for_allocation()
4758 self.assertThat(ip, Equals("10.0.0.1"))
4759
4760 def test__avoids_gateway_ip(self):
4761 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
4762- subnet = factory.make_Subnet(
4763- cidr="10.0.0.0/30", gateway_ip="10.0.0.1", dns_servers=None)
4764+ subnet = self.make_Subnet(
4765+ cidr="10.0.0.0/30", gateway_ip="10.0.0.1", dns_servers=None,
4766+ )
4767 ip = subnet.get_next_ip_for_allocation()
4768 self.assertThat(ip, Equals("10.0.0.2"))
4769
4770 def test__avoids_excluded_addresses(self):
4771 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
4772 subnet = factory.make_Subnet(
4773- cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)
4774+ cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None,
4775+ )
4776 ip = subnet.get_next_ip_for_allocation(exclude_addresses=["10.0.0.1"])
4777 self.assertThat(ip, Equals("10.0.0.2"))
4778
4779 def test__avoids_dns_servers(self):
4780 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
4781 subnet = factory.make_Subnet(
4782- cidr="10.0.0.0/30", gateway_ip=None, dns_servers=["10.0.0.1"])
4783+ cidr="10.0.0.0/30", gateway_ip=None, dns_servers=["10.0.0.1"],
4784+ )
4785 ip = subnet.get_next_ip_for_allocation()
4786 self.assertThat(ip, Equals("10.0.0.2"))
4787
4788 def test__avoids_observed_neighbours(self):
4789 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
4790- subnet = factory.make_Subnet(
4791+ subnet = self.make_Subnet(
4792 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)
4793 rackif = factory.make_Interface(vlan=subnet.vlan)
4794 factory.make_Discovery(ip="10.0.0.1", interface=rackif)
4795@@ -951,7 +980,7 @@
4796
4797 def test__logs_if_suggests_previously_observed_neighbour(self):
4798 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
4799- subnet = factory.make_Subnet(
4800+ subnet = self.make_Subnet(
4801 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)
4802 rackif = factory.make_Interface(vlan=subnet.vlan)
4803 now = datetime.now()
4804@@ -969,7 +998,7 @@
4805
4806 def test__uses_smallest_free_range_when_not_considering_neighbours(self):
4807 # Note: 10.0.0.0/29 --> 10.0.0.1 through 10.0.0.0.6 are usable.
4808- subnet = factory.make_Subnet(
4809+ subnet = self.make_Subnet(
4810 cidr="10.0.0.0/29", gateway_ip=None, dns_servers=None)
4811 # With .4 in use, the free ranges are {1, 2, 3}, {5, 6}. So MAAS should
4812 # select 10.0.0.5, since that is the first address in the smallest
4813@@ -977,3 +1006,48 @@
4814 factory.make_StaticIPAddress(ip="10.0.0.4", cidr="10.0.0.0/29")
4815 ip = subnet.get_next_ip_for_allocation()
4816 self.assertThat(ip, Equals("10.0.0.5"))
4817+
4818+
4819+class TestUnmanagedSubnets(MAASServerTestCase):
4820+ def setUp(self):
4821+ register_view("maasserver_discovery")
4822+ return super().setUp()
4823+
4824+ def test__allocation_uses_reserved_range(self):
4825+ # Note: 10.0.0.0/29 --> 10.0.0.1 through 10.0.0.0.6 are usable.
4826+ subnet = factory.make_Subnet(
4827+ cidr="10.0.0.0/29", gateway_ip=None, dns_servers=None,
4828+ managed=False)
4829+ range1 = factory.make_IPRange(
4830+ subnet, start_ip='10.0.0.1', end_ip='10.0.0.1',
4831+ type=IPRANGE_TYPE.RESERVED)
4832+ subnet = reload_object(subnet)
4833+ ip = subnet.get_next_ip_for_allocation()
4834+ self.assertThat(ip, Equals("10.0.0.1"))
4835+ range1.delete()
4836+ factory.make_IPRange(
4837+ subnet, start_ip='10.0.0.6', end_ip='10.0.0.6',
4838+ type=IPRANGE_TYPE.RESERVED)
4839+ subnet = reload_object(subnet)
4840+ ip = subnet.get_next_ip_for_allocation()
4841+ self.assertThat(ip, Equals("10.0.0.6"))
4842+
4843+ def test__allocation_uses_multiple_reserved_ranges(self):
4844+ # Note: 10.0.0.0/29 --> 10.0.0.1 through 10.0.0.0.6 are usable.
4845+ subnet = factory.make_Subnet(
4846+ cidr="10.0.0.0/29", gateway_ip=None, dns_servers=None,
4847+ managed=False)
4848+ factory.make_IPRange(
4849+ subnet, start_ip='10.0.0.3', end_ip='10.0.0.4',
4850+ type=IPRANGE_TYPE.RESERVED)
4851+ subnet = reload_object(subnet)
4852+ ip = subnet.get_next_ip_for_allocation()
4853+ self.assertThat(ip, Equals("10.0.0.3"))
4854+ factory.make_StaticIPAddress(ip)
4855+ ip = subnet.get_next_ip_for_allocation()
4856+ self.assertThat(ip, Equals("10.0.0.4"))
4857+ factory.make_StaticIPAddress(ip)
4858+ with ExpectedException(
4859+ StaticIPAddressExhaustion,
4860+ "No more IPs available in subnet: 10.0.0.0/29."):
4861+ subnet.get_next_ip_for_allocation()
4862
4863=== modified file 'src/maasserver/models/tests/test_vlan.py'
4864--- src/maasserver/models/tests/test_vlan.py 2016-10-19 18:06:01 +0000
4865+++ src/maasserver/models/tests/test_vlan.py 2016-12-07 15:50:52 +0000
4866@@ -88,6 +88,14 @@
4867
4868 class TestVLAN(MAASServerTestCase):
4869
4870+ def test_delete_relay_vlan_doesnt_delete_vlan(self):
4871+ relay_vlan = factory.make_VLAN()
4872+ vlan = factory.make_VLAN(relay_vlan=relay_vlan)
4873+ relay_vlan.delete()
4874+ vlan = reload_object(vlan)
4875+ self.assertIsNotNone(vlan)
4876+ self.assertIsNone(vlan.relay_vlan)
4877+
4878 def test_get_name_for_default_vlan_is_untagged(self):
4879 fabric = factory.make_Fabric()
4880 self.assertEqual("untagged", fabric.get_default_vlan().get_name())
4881
4882=== modified file 'src/maasserver/models/vlan.py'
4883--- src/maasserver/models/vlan.py 2016-10-20 19:39:48 +0000
4884+++ src/maasserver/models/vlan.py 2016-12-07 15:50:52 +0000
4885@@ -14,6 +14,7 @@
4886 from django.db.models import (
4887 BooleanField,
4888 CharField,
4889+ deletion,
4890 ForeignKey,
4891 IntegerField,
4892 Manager,
4893@@ -169,6 +170,10 @@
4894 'RackController', null=True, blank=True, editable=True,
4895 related_name='+')
4896
4897+ relay_vlan = ForeignKey(
4898+ 'self', null=True, blank=True, editable=True,
4899+ related_name='relay_vlans', on_delete=deletion.SET_NULL)
4900+
4901 def __str__(self):
4902 return "%s.%s" % (self.fabric.get_name(), self.get_name())
4903
4904
4905=== modified file 'src/maasserver/node_action.py'
4906--- src/maasserver/node_action.py 2016-08-16 09:31:16 +0000
4907+++ src/maasserver/node_action.py 2016-12-07 15:50:52 +0000
4908@@ -230,10 +230,6 @@
4909 self, enable_ssh=False, skip_networking=False,
4910 skip_storage=False):
4911 """See `NodeAction.execute`."""
4912- if self.node.power_state == POWER_STATE.ON:
4913- raise NodeActionError(
4914- "Unable to be commissioned because the power is currently on.")
4915-
4916 try:
4917 self.node.start_commissioning(
4918 self.user,
4919
4920=== modified file 'src/maasserver/rpc/nodes.py'
4921--- src/maasserver/rpc/nodes.py 2016-10-20 08:41:30 +0000
4922+++ src/maasserver/rpc/nodes.py 2016-12-07 15:50:52 +0000
4923@@ -30,6 +30,7 @@
4924 )
4925 from maasserver.models.timestampedmodel import now
4926 from maasserver.utils.orm import transactional
4927+from provisioningserver.drivers.power import PowerDriverRegistry
4928 from provisioningserver.rpc.exceptions import (
4929 CommissionNodeFailed,
4930 NodeAlreadyExists,
4931@@ -66,16 +67,16 @@
4932 :return: A generator yielding `dict`s.
4933 """
4934 five_minutes_ago = now() - timedelta(minutes=5)
4935-
4936- # This is meant to be temporary until all the power types support querying
4937- # the power state of a node. See the definition of QUERY_POWER_TYPES for
4938- # more information.
4939- from provisioningserver.power import QUERY_POWER_TYPES
4940+ queryable_power_types = [
4941+ driver.name
4942+ for _, driver in PowerDriverRegistry
4943+ if driver.queryable
4944+ ]
4945
4946 nodes_unchecked = (
4947 nodes
4948 .filter(power_state_queried=None)
4949- .filter(bmc__power_type__in=QUERY_POWER_TYPES)
4950+ .filter(bmc__power_type__in=queryable_power_types)
4951 .exclude(status=NODE_STATUS.BROKEN)
4952 .distinct()
4953 )
4954@@ -83,7 +84,7 @@
4955 nodes
4956 .exclude(power_state_queried=None)
4957 .exclude(power_state_queried__gt=five_minutes_ago)
4958- .filter(bmc__power_type__in=QUERY_POWER_TYPES)
4959+ .filter(bmc__power_type__in=queryable_power_types)
4960 .exclude(status=NODE_STATUS.BROKEN)
4961 .order_by("power_state_queried", "system_id")
4962 .distinct()
4963
4964=== modified file 'src/maasserver/rpc/rackcontrollers.py'
4965--- src/maasserver/rpc/rackcontrollers.py 2016-10-17 06:42:10 +0000
4966+++ src/maasserver/rpc/rackcontrollers.py 2016-12-07 15:50:52 +0000
4967@@ -26,7 +26,6 @@
4968 RegionController,
4969 StaticIPAddress,
4970 )
4971-from maasserver.models.node import typecast_node
4972 from maasserver.models.timestampedmodel import now
4973 from maasserver.utils import synchronised
4974 from maasserver.utils.orm import (
4975@@ -120,7 +119,7 @@
4976 node.node_type = NODE_TYPE.RACK_CONTROLLER
4977 node.save()
4978
4979- rackcontroller = typecast_node(node, RackController)
4980+ rackcontroller = node.as_rack_controller()
4981
4982 # Update `rackcontroller.url` from the given URL, if it has changed.
4983 update_fields = []
4984
4985=== modified file 'src/maasserver/rpc/regionservice.py'
4986--- src/maasserver/rpc/regionservice.py 2016-10-28 15:58:32 +0000
4987+++ src/maasserver/rpc/regionservice.py 2016-12-07 15:50:52 +0000
4988@@ -642,19 +642,17 @@
4989 # and into the database.
4990 self.ident = rack_controller.system_id
4991 self.factory.service._addConnectionFor(self.ident, self)
4992+
4993+ # A local rack is treated differently to one that's remote.
4994 self.host = self.transport.getHost()
4995 self.hostIsRemote = isinstance(
4996 self.host, (IPv4Address, IPv6Address))
4997
4998- # Get the region ID if we're dealing with a non-local rack; we
4999- # won't need to bother for local racks.
5000+ # Only register the connection into the database when it's a valid
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: