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
=== modified file 'HACKING.txt'
--- HACKING.txt 2016-03-28 13:54:47 +0000
+++ HACKING.txt 2016-12-07 15:50:52 +0000
@@ -138,8 +138,9 @@
138regiond.log. To enable logging of all exceptions even exceptions where MAAS138regiond.log. To enable logging of all exceptions even exceptions where MAAS
139will return the correct HTTP status code.::139will return the correct HTTP status code.::
140140
141 $ sudo sed -i 's/DEBUG = False/DEBUG = True/g' /usr/share/maas/maas/settings.py141 $ sudo sed -i 's/DEBUG = False/DEBUG = True/g' \
142 $ sudo service maas-regiond restart142 > /usr/lib/python3/dist-packages/maasserver/djangosettings/settings.py
143 $ sudo service maas-regiond restart
143144
144Run regiond in foreground145Run regiond in foreground
145^^^^^^^^^^^^^^^^^^^^^^^^^146^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -149,8 +150,10 @@
149placed a breakpoint into the code you want to inspect you can start the regiond150placed a breakpoint into the code you want to inspect you can start the regiond
150process in the foreground.::151process in the foreground.::
151152
152 $ sudo service maas-regiond stop153 $ sudo service maas-regiond stop
153 $ sudo -u maas -H DJANGO_SETTINGS_MODULE=maas.settings PYTHONPATH=/usr/share/maas twistd3 --nodaemon --pidfile= maas-regiond154 $ sudo -u maas -H \
155 > DJANGO_SETTINGS_MODULE=maasserver.djangosettings.settings \
156 > twistd3 --nodaemon --pidfile= maas-regiond
154157
155158
156.. Note::159.. Note::
@@ -175,7 +178,8 @@
175Development MAAS server setup178Development MAAS server setup
176=============================179=============================
177180
178Access to the database is configured in ``src/maas/development.py``.181Access to the database is configured in
182``src/maasserver/djangosettings/development.py``.
179183
180The ``Makefile`` or the test suite sets up a development database184The ``Makefile`` or the test suite sets up a development database
181cluster inside your branch. It lives in the ``db`` directory, which185cluster inside your branch. It lives in the ``db`` directory, which
182186
=== modified file 'Makefile'
--- Makefile 2016-10-17 06:38:56 +0000
+++ Makefile 2016-12-07 15:50:52 +0000
@@ -427,7 +427,8 @@
427 $(warning 'distclean' is deprecated; use 'clean')427 $(warning 'distclean' is deprecated; use 'clean')
428428
429harness: bin/maas-region bin/database429harness: bin/maas-region bin/database
430 $(dbrun) bin/maas-region shell --settings=maas.demo430 $(dbrun) bin/maas-region shell \
431 --settings=maasserver.djangosettings.demo
431432
432dbharness: bin/database433dbharness: bin/database
433 bin/database --preserve shell434 bin/database --preserve shell
434435
=== modified file 'buildout.cfg'
--- buildout.cfg 2016-10-12 15:26:17 +0000
+++ buildout.cfg 2016-12-07 15:50:52 +0000
@@ -104,7 +104,7 @@
104 twistd.region=twisted.scripts.twistd:run104 twistd.region=twisted.scripts.twistd:run
105initialization =105initialization =
106 ${common:initialization}106 ${common:initialization}
107 environ.setdefault("DJANGO_SETTINGS_MODULE", "maas.development")107 environ.setdefault("DJANGO_SETTINGS_MODULE", "maasserver.djangosettings.development")
108scripts =108scripts =
109 maas-region109 maas-region
110 twistd.region110 twistd.region
@@ -129,7 +129,6 @@
129 # "--with-resources",129 # "--with-resources",
130 "--with-scenarios",130 "--with-scenarios",
131 "--with-select",131 "--with-select",
132 "--select-dir=src/maas",
133 "--select-dir=src/maasserver",132 "--select-dir=src/maasserver",
134 "--select-dir=src/metadataserver",133 "--select-dir=src/metadataserver",
135 "--cover-package=maas,maasserver,metadataserver",134 "--cover-package=maas,maasserver,metadataserver",
@@ -294,7 +293,7 @@
294 from os import environ293 from os import environ
295 environ.setdefault("MAAS_RACK_DEVELOP", "TRUE")294 environ.setdefault("MAAS_RACK_DEVELOP", "TRUE")
296 environ.setdefault("MAAS_ROOT", "${buildout:directory}/run-e2e")295 environ.setdefault("MAAS_ROOT", "${buildout:directory}/run-e2e")
297 environ.setdefault("DJANGO_SETTINGS_MODULE", "maas.development")296 environ.setdefault("DJANGO_SETTINGS_MODULE", "maasserver.djangosettings.development")
298 environ.setdefault("DEV_DB_NAME", "test_maas_e2e")297 environ.setdefault("DEV_DB_NAME", "test_maas_e2e")
299 environ.setdefault("MAAS_PREVENT_MIGRATIONS", "1")298 environ.setdefault("MAAS_PREVENT_MIGRATIONS", "1")
300299
301300
=== modified file 'docs/_templates/maas/static/css/main.css'
--- docs/_templates/maas/static/css/main.css 2014-06-09 16:25:19 +0000
+++ docs/_templates/maas/static/css/main.css 2016-12-07 15:50:52 +0000
@@ -73,3 +73,17 @@
73 text-decoration: none;73 text-decoration: none;
74 border-bottom: 1px solid #6D4100;74 border-bottom: 1px solid #6D4100;
75}75}
76
77/*
78 * Custom CSS selectors for the API documentation page.
79 *
80 * Make subtitles for each API endpoint smaller, so they don't overwhelm
81 * the remainder of the documentation.
82 */
83div#maas-api div#operations h4 code.docutils {
84 font-size: 75%;
85}
86
87div#maas-api div#operations div.section h5 {
88 font-size: 90%;
89}
7690
=== modified file 'docs/conf.py'
--- docs/conf.py 2016-03-28 13:54:47 +0000
+++ docs/conf.py 2016-12-07 15:50:52 +0000
@@ -24,7 +24,8 @@
24from pytz import UTC24from pytz import UTC
2525
26# Configure MAAS's settings.26# Configure MAAS's settings.
27environ.setdefault("DJANGO_SETTINGS_MODULE", "maas.settings")27environ.setdefault(
28 "DJANGO_SETTINGS_MODULE", "maasserver.djangosettings.settings")
2829
29# If extensions (or modules to document with autodoc) are in another directory,30# If extensions (or modules to document with autodoc) are in another directory,
30# add these directories to sys.path here. If the directory is relative to the31# add these directories to sys.path here. If the directory is relative to the
3132
=== modified file 'docs/troubleshooting.rst'
--- docs/troubleshooting.rst 2014-09-10 16:20:31 +0000
+++ docs/troubleshooting.rst 2016-12-07 15:50:52 +0000
@@ -111,7 +111,7 @@
111 always point at the local server.111 always point at the local server.
112 #. If you are still getting "404 - Page not found" errors, check that the MAAS112 #. If you are still getting "404 - Page not found" errors, check that the MAAS
113 web interface has been installed in the right place. There should be a file113 web interface has been installed in the right place. There should be a file
114 present called /usr/share/maas/maas/urls.py114 called ``urls.py`` in ``/usr/lib/python3/dist-packages/maasserver/djangosettings/``.
115115
116Debugging ephemeral image116Debugging ephemeral image
117=========================117=========================
118118
=== modified file 'media/README'
--- media/README 2012-03-11 21:13:22 +0000
+++ media/README 2016-12-07 15:50:52 +0000
@@ -1,5 +1,5 @@
1This folder contains somewhat ephemeral things: subfolders serve as1This folder contains somewhat ephemeral things: subfolders serve as
2MEDIA_ROOT for maas.demo and maas.development environments. The2MEDIA_ROOT for maasserver.djangosettings.demo and .development
3media/demo directory should always exist and not be deleted, though3environments. The media/demo directory should always exist and not be
4its contents can be. The media/development directory should be created4deleted, though its contents can be. The media/development directory
5and destroyed by tests, as needed.5should be created and destroyed by tests, as needed.
66
=== modified file 'required-packages/dev'
--- required-packages/dev 2016-08-24 20:20:55 +0000
+++ required-packages/dev 2016-12-07 15:50:52 +0000
@@ -13,10 +13,10 @@
13libjs-jquery13libjs-jquery
14libjs-jquery-hotkeys14libjs-jquery-hotkeys
15libjs-yui3-full15libjs-yui3-full
16libnss-wrapper
16make17make
17nodejs-legacy18nodejs-legacy
18npm19npm
19python-pocket-lint
20python-bson20python-bson
21python-crochet21python-crochet
22python-django22python-django
@@ -26,6 +26,7 @@
26python-lxml26python-lxml
27python-netaddr27python-netaddr
28python-netifaces28python-netifaces
29python-pocket-lint
29python-psycopg230python-psycopg2
30python-simplejson31python-simplejson
31python-tempita32python-tempita
3233
=== modified file 'services/reloader/run'
--- services/reloader/run 2016-05-11 19:01:48 +0000
+++ services/reloader/run 2016-12-07 15:50:52 +0000
@@ -128,7 +128,7 @@
128 exclude_filter=lambda path: (128 exclude_filter=lambda path: (
129 "/test/" in path or "/testing/" in path or "/." in path))129 "/test/" in path or "/testing/" in path or "/." in path))
130 wm.add_watch(130 wm.add_watch(
131 ["src/maas*", "src/meta*"], TRIGGER_EVENTS,131 ["src/maasserver", "src/metadataserver"], TRIGGER_EVENTS,
132 proc_fun=handle_maas_change, rec=True, auto_add=True, do_glob=True)132 proc_fun=handle_maas_change, rec=True, auto_add=True, do_glob=True)
133 wm.add_watch(133 wm.add_watch(
134 ["src/prov*"], TRIGGER_EVENTS, proc_fun=handle_pserv_change,134 ["src/prov*"], TRIGGER_EVENTS, proc_fun=handle_pserv_change,
135135
=== modified file 'src/maascli/cli.py'
--- src/maascli/cli.py 2016-07-30 01:17:54 +0000
+++ src/maascli/cli.py 2016-12-07 15:50:52 +0000
@@ -189,8 +189,8 @@
189 # Setup and the allowed django commands into the maascli.189 # Setup and the allowed django commands into the maascli.
190 management = get_django_management()190 management = get_django_management()
191 if management is not None and is_maasserver_available():191 if management is not None and is_maasserver_available():
192 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "maas.settings")192 os.environ.setdefault(
193 sys.path.append('/usr/share/maas')193 "DJANGO_SETTINGS_MODULE", "maasserver.djangosettings.settings")
194 load_regiond_commands(management, parser)194 load_regiond_commands(management, parser)
195195
196196
197197
=== modified file 'src/maasserver/__init__.py'
--- src/maasserver/__init__.py 2016-10-20 14:45:06 +0000
+++ src/maasserver/__init__.py 2016-12-07 15:50:52 +0000
@@ -8,7 +8,7 @@
8 'DefaultViewMeta',8 'DefaultViewMeta',
9 'is_master_process',9 'is_master_process',
10 'logger',10 'logger',
11 ]11]
1212
13import logging13import logging
14from os import environ14from os import environ
1515
=== added file 'src/maasserver/api/chassis.py'
--- src/maasserver/api/chassis.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/api/chassis.py 2016-12-07 15:50:52 +0000
@@ -0,0 +1,78 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__all__ = [
5 "ChassiHandler",
6 "ChassisHandler",
7 ]
8
9from maasserver.api.nodes import (
10 NodeHandler,
11 NodesHandler,
12)
13from maasserver.enum import NODE_PERMISSION
14from maasserver.models.node import Chassis
15from piston3.utils import rc
16
17# Chassis fields exposed on the API.
18DISPLAYED_CHASSIS_FIELDS = (
19 'system_id',
20 'hostname',
21 'cpu_count',
22 'memory',
23 'chassis_type',
24 'node_type',
25 'node_type_name',
26 )
27
28
29class ChassiHandler(NodeHandler):
30 """Manage an individual chassis.
31
32 The chassis is identified by its system_id.
33 """
34 api_doc_section_name = "Chassis"
35
36 create = update = None
37 model = Chassis
38 fields = DISPLAYED_CHASSIS_FIELDS
39
40 @classmethod
41 def chassis_type(cls, chassis):
42 return chassis.power_type
43
44 def delete(self, request, system_id):
45 """Delete a specific Chassis.
46
47 Returns 404 if the chassis is not found.
48 Returns 403 if the user does not have permission to delete the chassis.
49 Returns 204 if the chassis is successfully deleted.
50 """
51 chassis = self.model.objects.get_node_or_404(
52 system_id=system_id, user=request.user,
53 perm=NODE_PERMISSION.ADMIN)
54 chassis.delete()
55 return rc.DELETED
56
57 @classmethod
58 def resource_uri(cls, chassis=None):
59 # This method is called by piston in two different contexts:
60 # - when generating an uri template to be used in the documentation
61 # (in this case, it is called with node=None).
62 # - when populating the 'resource_uri' field of an object
63 # returned by the API (in this case, node is a node object).
64 chassis_system_id = "system_id"
65 if chassis is not None:
66 chassis_system_id = chassis.system_id
67 return ('chassi_handler', (chassis_system_id,))
68
69
70class ChassisHandler(NodesHandler):
71 """Manage the collection of all the chassis in the MAAS."""
72 api_doc_section_name = "Chassis"
73 create = update = delete = None
74 base_model = Chassis
75
76 @classmethod
77 def resource_uri(cls, *args, **kwargs):
78 return ('chassis_handler', [])
079
=== modified file 'src/maasserver/api/doc.py'
--- src/maasserver/api/doc.py 2016-04-12 22:25:44 +0000
+++ src/maasserver/api/doc.py 2016-12-07 15:50:52 +0000
@@ -33,7 +33,7 @@
33from piston3.doc import generate_doc33from piston3.doc import generate_doc
34from piston3.handler import BaseHandler34from piston3.handler import BaseHandler
35from piston3.resource import Resource35from piston3.resource import Resource
36from provisioningserver.power.schema import JSON_POWER_TYPE_PARAMETERS36from provisioningserver.drivers.power import PowerDriverRegistry
3737
3838
39def accumulate_api_resources(resolver, accumulator):39def accumulate_api_resources(resolver, accumulator):
@@ -77,8 +77,7 @@
77def generate_power_types_doc():77def generate_power_types_doc():
78 """Generate ReST documentation for the supported power types.78 """Generate ReST documentation for the supported power types.
7979
80 The documentation is derived from the `JSON_POWER_TYPE_PARAMETERS`80 The documentation is derived from the `PowerDriverRegistry`.
81 object.
82 """81 """
83 output = StringIO()82 output = StringIO()
84 line = partial(print, file=output)83 line = partial(print, file=output)
@@ -92,14 +91,14 @@
92 "list if the cluster in question is from an older version of "91 "list if the cluster in question is from an older version of "
93 "MAAS.")92 "MAAS.")
94 line()93 line()
95 for item in JSON_POWER_TYPE_PARAMETERS:94 for _, driver in PowerDriverRegistry:
96 title = "%s (%s)" % (item['name'], item['description'])95 title = "%s (%s)" % (driver.name, driver.description)
97 line(title)96 line(title)
98 line('=' * len(title))97 line('=' * len(title))
99 line('')98 line('')
100 line("Power parameters:")99 line("Power parameters:")
101 line('')100 line('')
102 for field in item['fields']:101 for field in driver.settings:
103 field_description = []102 field_description = []
104 field_description.append(103 field_description.append(
105 "* %s (%s)." % (field['name'], field['label']))104 "* %s (%s)." % (field['name'], field['label']))
106105
=== modified file 'src/maasserver/api/doc_handler.py'
--- src/maasserver/api/doc_handler.py 2016-08-18 17:31:05 +0000
+++ src/maasserver/api/doc_handler.py 2016-12-07 15:50:52 +0000
@@ -9,7 +9,7 @@
99
1010
11API versions11API versions
12------------12````````````
1313
14At any given time, MAAS may support multiple versions of its API. The version14At any given time, MAAS may support multiple versions of its API. The version
15number is included in the API's URL, e.g. /api/2.0/15number is included in the API's URL, e.g. /api/2.0/
@@ -23,7 +23,7 @@
2323
2424
25HTTP methods and parameter-passing25HTTP methods and parameter-passing
26----------------------------------26``````````````````````````````````
2727
28The following HTTP methods are available for accessing the API:28The following HTTP methods are available for accessing the API:
29 * GET (for information retrieval and queries),29 * GET (for information retrieval and queries),
@@ -82,6 +82,7 @@
82# etc. whatever render_api_docs() produces, so that you can concatenate82# etc. whatever render_api_docs() produces, so that you can concatenate
83# the two.83# the two.
84api_doc_title = dedent("""84api_doc_title = dedent("""
85 :tocdepth: 3
85 .. _region-controller-api:86 .. _region-controller-api:
8687
87 ========88 ========
@@ -109,7 +110,7 @@
109 line()110 line()
110 line()111 line()
111 line('Operations')112 line('Operations')
112 line('----------')113 line('``````````')
113 line()114 line()
114115
115 def export_key(export):116 def export_key(export):
@@ -132,25 +133,24 @@
132 section_name = doc.handler.api_doc_section_name133 section_name = doc.handler.api_doc_section_name
133 line(section_name)134 line(section_name)
134 line('=' * len(section_name))135 line('=' * len(section_name))
135 line(doc.handler.__doc__.strip())136 line(dedent(doc.handler.__doc__).strip())
136 line()137 line()
137 line()138 line()
138 for (http_method, op), function in sorted(exports, key=export_key):139 for (http_method, op), function in sorted(exports, key=export_key):
139 line("``%s %s``" % (http_method, uri_template), end="")140 operation = " op=%s" % op if op is not None else ""
140 if op is not None:141 subsection = "``%s %s%s``" % (http_method, uri_template, operation)
141 line(" ``op=%s``" % op, end="")142 line("%s\n%s\n" % (subsection, '#' * len(subsection)))
142 line()143 line()
143 docstring = getdoc(function)144 docstring = getdoc(function)
144 if docstring is not None:145 if docstring is not None:
145 for docline in docstring.splitlines():146 for docline in dedent(docstring).splitlines():
146 if docline.strip() == '':147 if docline.strip() == '':
147 # Blank line. Don't indent.148 # Blank line. Don't indent.
148 line()149 line()
149 else:150 else:
150 # Print documentation line, indented.151 # Print documentation line, indented.
151 line(" ", docline, sep="")152 line(docline)
152 line()153 line()
153
154 line()154 line()
155 line()155 line()
156 line(generate_power_types_doc())156 line(generate_power_types_doc())
157157
=== modified file 'src/maasserver/api/interfaces.py'
--- src/maasserver/api/interfaces.py 2016-10-20 16:04:24 +0000
+++ src/maasserver/api/interfaces.py 2016-12-07 15:50:52 +0000
@@ -436,18 +436,18 @@
436436
437 Following are parameters specific to bonds:437 Following are parameters specific to bonds:
438438
439 :param bond-mode: The operating mode of the bond.439 :param bond_mode: The operating mode of the bond.
440 (Default: active-backup).440 (Default: active-backup).
441 :param bond-miimon: The link monitoring freqeuncy in milliseconds.441 :param bond_miimon: The link monitoring freqeuncy in milliseconds.
442 (Default: 100).442 (Default: 100).
443 :param bond-downdelay: Specifies the time, in milliseconds, to wait443 :param bond_downdelay: Specifies the time, in milliseconds, to wait
444 before disabling a slave after a link failure has been detected.444 before disabling a slave after a link failure has been detected.
445 :param bond-updelay: Specifies the time, in milliseconds, to wait445 :param bond_updelay: Specifies the time, in milliseconds, to wait
446 before enabling a slave after a link recovery has been detected.446 before enabling a slave after a link recovery has been detected.
447 :param bond-lacp_rate: Option specifying the rate in which we'll ask447 :param bond_lacp_rate: Option specifying the rate in which we'll ask
448 our link partner to transmit LACPDU packets in 802.3ad mode.448 our link partner to transmit LACPDU packets in 802.3ad mode.
449 Available options are fast or slow. (Default: slow).449 Available options are fast or slow. (Default: slow).
450 :param bond-xmit_hash_policy: The transmit hash policy to use for450 :param bond_xmit_hash_policy: The transmit hash policy to use for
451 slave selection in balance-xor, 802.3ad, and tlb modes.451 slave selection in balance-xor, 802.3ad, and tlb modes.
452452
453 Supported bonding modes (bond-mode):453 Supported bonding modes (bond-mode):
454454
=== modified file 'src/maasserver/api/nodes.py'
--- src/maasserver/api/nodes.py 2016-06-17 07:16:39 +0000
+++ src/maasserver/api/nodes.py 2016-12-07 15:50:52 +0000
@@ -47,10 +47,9 @@
47 Node,47 Node,
48 OwnerData,48 OwnerData,
49)49)
50from maasserver.models.node import typecast_to_node_type
51from maasserver.models.nodeprobeddetails import get_single_probed_details50from maasserver.models.nodeprobeddetails import get_single_probed_details
52from piston3.utils import rc51from piston3.utils import rc
53from provisioningserver.power.schema import UNKNOWN_POWER_TYPE52from provisioningserver.drivers.power import UNKNOWN_POWER_TYPE
5453
5554
56def store_node_power_parameters(node, request):55def store_node_power_parameters(node, request):
@@ -171,7 +170,7 @@
171 else:170 else:
172 # Return the specific node type object so we get the correct171 # Return the specific node type object so we get the correct
173 # listing172 # listing
174 return typecast_to_node_type(node)173 return node.as_self()
175174
176 def delete(self, request, system_id):175 def delete(self, request, system_id):
177 """Delete a specific Node.176 """Delete a specific Node.
@@ -183,7 +182,7 @@
183 node = self.model.objects.get_node_or_404(182 node = self.model.objects.get_node_or_404(
184 system_id=system_id, user=request.user,183 system_id=system_id, user=request.user,
185 perm=NODE_PERMISSION.ADMIN)184 perm=NODE_PERMISSION.ADMIN)
186 typecast_to_node_type(node).delete()185 node.as_self().delete()
187 return rc.DELETED186 return rc.DELETED
188187
189 @classmethod188 @classmethod
@@ -315,19 +314,23 @@
315314
316 if self.base_model == Node:315 if self.base_model == Node:
317 # Avoid circular dependencies316 # Avoid circular dependencies
317 from maasserver.api.chassis import ChassisHandler
318 from maasserver.api.devices import DevicesHandler318 from maasserver.api.devices import DevicesHandler
319 from maasserver.api.machines import MachinesHandler319 from maasserver.api.machines import MachinesHandler
320 from maasserver.api.rackcontrollers import RackControllersHandler320 from maasserver.api.rackcontrollers import RackControllersHandler
321 from maasserver.api.regioncontrollers import (321 from maasserver.api.regioncontrollers import (
322 RegionControllersHandler322 RegionControllersHandler
323 )323 )
324 from maasserver.api.storage import StoragesHandler
324 racks = RackControllersHandler().read(request).order_by("id")325 racks = RackControllersHandler().read(request).order_by("id")
325 nodes = list(chain(326 nodes = list(chain(
327 ChassisHandler().read(request).order_by("id"),
326 DevicesHandler().read(request).order_by("id"),328 DevicesHandler().read(request).order_by("id"),
327 MachinesHandler().read(request).order_by("id"),329 MachinesHandler().read(request).order_by("id"),
328 racks,330 racks,
329 RegionControllersHandler().read(request).exclude(331 RegionControllersHandler().read(request).exclude(
330 id__in=racks).order_by("id"),332 id__in=racks).order_by("id"),
333 StoragesHandler().read(request).order_by("id"),
331 ))334 ))
332 return nodes335 return nodes
333 else:336 else:
334337
=== modified file 'src/maasserver/api/results.py'
--- src/maasserver/api/results.py 2016-07-30 01:17:54 +0000
+++ src/maasserver/api/results.py 2016-12-07 15:50:52 +0000
@@ -14,7 +14,6 @@
14)14)
15from maasserver.enum import NODE_PERMISSION15from maasserver.enum import NODE_PERMISSION
16from maasserver.models import Node16from maasserver.models import Node
17from maasserver.models.node import typecast_to_node_type
18from metadataserver.models import NodeResult17from metadataserver.models import NodeResult
1918
2019
@@ -54,9 +53,9 @@
54 if result_type is not None:53 if result_type is not None:
55 results = results.filter(result_type__in=result_type)54 results = results.filter(result_type__in=result_type)
56 # Convert the node objects into typed node objects so we get the55 # Convert the node objects into typed node objects so we get the
57 # proper listing56 # proper listing.
58 for result in results:57 for result in results:
59 result.node = typecast_to_node_type(result.node)58 result.node = result.node.as_self()
60 return results59 return results
6160
62 @classmethod61 @classmethod
6362
=== added file 'src/maasserver/api/storage.py'
--- src/maasserver/api/storage.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/api/storage.py 2016-12-07 15:50:52 +0000
@@ -0,0 +1,76 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__all__ = [
5 "StorageHandler",
6 "StoragesHandler",
7 ]
8
9from maasserver.api.nodes import (
10 NodeHandler,
11 NodesHandler,
12)
13from maasserver.enum import NODE_PERMISSION
14from maasserver.models.node import Storage
15from piston3.utils import rc
16
17# Storage fields exposed on the API.
18DISPLAYED_STORAGE_FIELDS = (
19 'system_id',
20 'hostname',
21 'storage_type',
22 'node_type',
23 'node_type_name',
24 )
25
26
27class StorageHandler(NodeHandler):
28 """Manage an individual storage system.
29
30 The storage is identified by its system_id.
31 """
32 api_doc_section_name = "Storage"
33
34 create = update = None
35 model = Storage
36 fields = DISPLAYED_STORAGE_FIELDS
37
38 @classmethod
39 def storage_type(cls, storage):
40 return storage.power_type
41
42 def delete(self, request, system_id):
43 """Delete a specific Storage.
44
45 Returns 404 if the storage is not found.
46 Returns 403 if the user does not have permission to delete the storage.
47 Returns 204 if the storage is successfully deleted.
48 """
49 storage = self.model.objects.get_node_or_404(
50 system_id=system_id, user=request.user,
51 perm=NODE_PERMISSION.ADMIN)
52 storage.delete()
53 return rc.DELETED
54
55 @classmethod
56 def resource_uri(cls, storage=None):
57 # This method is called by piston in two different contexts:
58 # - when generating an uri template to be used in the documentation
59 # (in this case, it is called with node=None).
60 # - when populating the 'resource_uri' field of an object
61 # returned by the API (in this case, node is a node object).
62 storage_system_id = "system_id"
63 if storage is not None:
64 storage_system_id = storage.system_id
65 return ('storage_handler', (storage_system_id,))
66
67
68class StoragesHandler(NodesHandler):
69 """Manage the collection of all the storage in the MAAS."""
70 api_doc_section_name = "Storages"
71 create = update = delete = None
72 base_model = Storage
73
74 @classmethod
75 def resource_uri(cls, *args, **kwargs):
76 return ('storages_handler', [])
077
=== modified file 'src/maasserver/api/subnets.py'
--- src/maasserver/api/subnets.py 2016-09-23 01:32:02 +0000
+++ src/maasserver/api/subnets.py 2016-12-07 15:50:52 +0000
@@ -29,6 +29,7 @@
29 'rdns_mode',29 'rdns_mode',
30 'active_discovery',30 'active_discovery',
31 'allow_proxy',31 'allow_proxy',
32 'managed',
32)33)
3334
3435
@@ -49,32 +50,76 @@
4950
50 @admin_method51 @admin_method
51 def create(self, request):52 def create(self, request):
52 """Create a subnet.53 """\
5354 Create a subnet.
54 :param name: Name of the subnet.55
55 :param description: Description of the subnet.56 Required parameters
56 :param fabric: Fabric for the subnet. Defaults to the fabric the57 -------------------
57 provided VLAN belongs to or defaults to the default fabric.58
58 :param vlan: VLAN this subnet belongs to. Defaults to the default59 cidr
59 VLAN for the provided fabric or defaults to the default VLAN in60 The network CIDR for this subnet.
60 the default fabric.61
61 :param vid: VID of the VLAN this subnet belongs to. Only used when62
62 vlan is not provided. Picks the VLAN with this VID in the provided63 Optional parameters
63 fabric or the default fabric if one is not given.64 -------------------
64 :param space: Space this subnet is in. Defaults to the default space.65
65 :param cidr: The network CIDR for this subnet.66 name
66 :param gateway_ip: The gateway IP address for this subnet.67 Name of the subnet.
67 :param rdns_mode: How reverse DNS is handled for this subnet.68
68 One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled means69 description
69 no reverse zone is created; Enabled means generate the reverse70 Description of the subnet.
70 zone; RFC2317 extends Enabled to create the necessary parent zone71
71 with the appropriate CNAME resource records for the network, if the72 vlan
72 network is small enough to require the support described in73 VLAN this subnet belongs to. Defaults to the default VLAN for the
73 RFC2317.74 provided fabric or defaults to the default VLAN in the default fabric
74 :param allow_proxy: Configure maas-proxy to allow requests from this75 (if unspecified).
75 subnet.76
76 :param dns_servers: Comma-seperated list of DNS servers for this77 fabric
77 subnet.78 Fabric for the subnet. Defaults to the fabric the
79 provided VLAN belongs to, or defaults to the default fabric.
80
81 vid
82 VID of the VLAN this subnet belongs to. Only used when vlan is
83 not provided. Picks the VLAN with this VID in the provided
84 fabric or the default fabric if one is not given.
85
86 space
87 Space this subnet is in. Defaults to the default space.
88
89 gateway_ip
90 The gateway IP address for this subnet.
91
92 rdns_mode
93 How reverse DNS is handled for this subnet.
94 One of: 0 (Disabled), 1 (Enabled), or 2 (RFC2317). Disabled
95 means no reverse zone is created; Enabled means generate the
96 reverse zone; RFC2317 extends Enabled to create the necessary
97 parent zone with the appropriate CNAME resource records for the
98 network, if the network is small enough to require the support
99 described in RFC2317.
100
101 allow_proxy
102 Configure maas-proxy to allow requests from this
103 subnet.
104
105 dns_servers
106 Comma-seperated list of DNS servers for this subnet.
107
108 managed
109 In MAAS 2.0+, all subnets are assumed to be managed by default.
110
111 Only managed subnets allow DHCP to be enabled on their related
112 dynamic ranges. (Thus, dynamic ranges become "informational
113 only"; an indication that another DHCP server is currently
114 handling them, or that MAAS will handle them when the subnet is
115 enabled for management.)
116
117 Managed subnets do not allow IP allocation by default. The
118 meaning of a "reserved" IP range is reversed for an unmanaged
119 subnet. (That is, for managed subnets, "reserved" means "MAAS
120 cannot allocate any IP address within this reserved block". For
121 unmanaged subnets, "reserved" means "MAAS must allocate IP
122 addresses only from reserved IP ranges".
78 """123 """
79 form = SubnetForm(data=request.data)124 form = SubnetForm(data=request.data)
80 if form.is_valid():125 if form.is_valid():
@@ -100,7 +145,8 @@
100145
101 @classmethod146 @classmethod
102 def space(cls, subnet):147 def space(cls, subnet):
103 """Return the name of the space.148 """\
149 Return the name of the space.
104150
105 Only the name is returned because the space endpoint will return151 Only the name is returned because the space endpoint will return
106 a list of all subnets in that space. If this returned the subnet152 a list of all subnets in that space. If this returned the subnet
@@ -109,7 +155,8 @@
109 return subnet.space.get_name()155 return subnet.space.get_name()
110156
111 def read(self, request, subnet_id):157 def read(self, request, subnet_id):
112 """Read subnet.158 """\
159 Read subnet.
113160
114 Returns 404 if the subnet is not found.161 Returns 404 if the subnet is not found.
115 """162 """
@@ -117,19 +164,44 @@
117 subnet_id, request.user, NODE_PERMISSION.VIEW)164 subnet_id, request.user, NODE_PERMISSION.VIEW)
118165
119 def update(self, request, subnet_id):166 def update(self, request, subnet_id):
120 """Update subnet.167 """\
121168 Update the specified subnet.
122 :param name: Name of the subnet.169
123 :param description: Description of the subnet.170 Please see the documentation for the 'create' operation for detailed
124 :param vlan: VLAN this subnet belongs to.171 descriptions of each parameter.
125 :param space: Space this subnet is in.172
126 :param cidr: The network CIDR for this subnet.173 Optional parameters
127 :param gateway_ip: The gateway IP address for this subnet.174 -------------------
128 :param rdns_mode: How reverse DNS is handled for this subnet.175
129 :param allow_proxy: Configure maas-proxy to allow requests from this \176 name
130 subnet.177 Name of the subnet.
131 :param dns_servers: Comma-seperated list of DNS servers for this \178
132 subnet.179 description
180 Description of the subnet.
181
182 vlan
183 VLAN this subnet belongs to.
184
185 space
186 Space this subnet is in.
187
188 cidr
189 The network CIDR for this subnet.
190
191 gateway_ip
192 The gateway IP address for this subnet.
193
194 rdns_mode
195 How reverse DNS is handled for this subnet.
196
197 allow_proxy
198 Configure maas-proxy to allow requests from this subnet.
199
200 dns_servers
201 Comma-seperated list of DNS servers for this subnet.
202
203 managed
204 If False, MAAS should not manage this subnet. (Default: True)
133205
134 Returns 404 if the subnet is not found.206 Returns 404 if the subnet is not found.
135 """207 """
@@ -142,7 +214,8 @@
142 raise MAASAPIValidationError(form.errors)214 raise MAASAPIValidationError(form.errors)
143215
144 def delete(self, request, subnet_id):216 def delete(self, request, subnet_id):
145 """Delete subnet.217 """\
218 Delete subnet.
146219
147 Returns 404 if the subnet is not found.220 Returns 404 if the subnet is not found.
148 """221 """
@@ -153,7 +226,8 @@
153226
154 @operation(idempotent=True)227 @operation(idempotent=True)
155 def reserved_ip_ranges(self, request, subnet_id):228 def reserved_ip_ranges(self, request, subnet_id):
156 """Lists IP ranges currently reserved in the subnet.229 """\
230 Lists IP ranges currently reserved in the subnet.
157231
158 Returns 404 if the subnet is not found.232 Returns 404 if the subnet is not found.
159 """233 """
@@ -163,7 +237,8 @@
163237
164 @operation(idempotent=True)238 @operation(idempotent=True)
165 def unreserved_ip_ranges(self, request, subnet_id):239 def unreserved_ip_ranges(self, request, subnet_id):
166 """Lists IP ranges currently unreserved in the subnet.240 """\
241 Lists IP ranges currently unreserved in the subnet.
167242
168 Returns 404 if the subnet is not found.243 Returns 404 if the subnet is not found.
169 """244 """
@@ -174,22 +249,27 @@
174249
175 @operation(idempotent=True)250 @operation(idempotent=True)
176 def statistics(self, request, subnet_id):251 def statistics(self, request, subnet_id):
177 """252 """\
178 Returns statistics for the specified subnet, including:253 Returns statistics for the specified subnet, including:
179254
180 num_available - the number of available IP addresses255 num_available: the number of available IP addresses
181 largest_available - the largest number of contiguous free IP addresses256 largest_available: the largest number of contiguous free IP addresses
182 num_unavailable - the number of unavailable IP addresses257 num_unavailable: the number of unavailable IP addresses
183 total_addresses - the sum of the available plus unavailable addresses258 total_addresses: the sum of the available plus unavailable addresses
184 usage - the (floating point) usage percentage of this subnet259 usage: the (floating point) usage percentage of this subnet
185 usage_string - the (formatted unicode) usage percentage of this subnet260 usage_string: the (formatted unicode) usage percentage of this subnet
186 ranges - the specific IP ranges present in ths subnet (if specified)261 ranges: the specific IP ranges present in ths subnet (if specified)
187262
188 Optional arguments:263 Optional parameters
189 include_ranges: if True, includes detailed information264 -------------------
190 about the usage of this range.265
191 include_suggestions: if True, includes the suggested gateway and266 include_ranges
192 dynamic range for this subnet, if it were to be configured.267 If True, includes detailed information
268 about the usage of this range.
269
270 include_suggestions
271 If True, includes the suggested gateway and dynamic range for this
272 subnet, if it were to be configured.
193273
194 Returns 404 if the subnet is not found.274 Returns 404 if the subnet is not found.
195 """275 """
@@ -208,14 +288,19 @@
208288
209 @operation(idempotent=True)289 @operation(idempotent=True)
210 def ip_addresses(self, request, subnet_id):290 def ip_addresses(self, request, subnet_id):
211 """291 """\
212 Returns a summary of IP addresses assigned to this subnet.292 Returns a summary of IP addresses assigned to this subnet.
213293
214 Optional arguments:294 Optional parameters
215 with_username: (default=True) if False, suppresses the display295 -------------------
216 of usernames associated with each address.296
217 with_node_summary: (default=True) if False, suppresses the display297 with_username
218 of any node associated with each address.298 If False, suppresses the display of usernames associated with each
299 address. (Default: True)
300
301 with_node_summary
302 If False, suppresses the display of any node associated with each
303 address. (Default: True)
219 """304 """
220 subnet = Subnet.objects.get_subnet_or_404(305 subnet = Subnet.objects.get_subnet_or_404(
221 subnet_id, request.user, NODE_PERMISSION.VIEW)306 subnet_id, request.user, NODE_PERMISSION.VIEW)
222307
=== modified file 'src/maasserver/api/tags.py'
--- src/maasserver/api/tags.py 2016-04-27 00:55:47 +0000
+++ src/maasserver/api/tags.py 2016-12-07 15:50:52 +0000
@@ -37,7 +37,6 @@
37 RegionController,37 RegionController,
38 Tag,38 Tag,
39)39)
40from maasserver.models.node import typecast_to_node_type
41from maasserver.models.user import get_auth_tokens40from maasserver.models.user import get_auth_tokens
42from maasserver.utils.orm import get_one41from maasserver.utils.orm import get_one
43from piston3.utils import rc42from piston3.utils import rc
@@ -137,7 +136,7 @@
137 self.fields = None136 self.fields = None
138 tag = Tag.objects.get_tag_or_404(name=name, user=request.user)137 tag = Tag.objects.get_tag_or_404(name=name, user=request.user)
139 return [138 return [
140 typecast_to_node_type(node)139 node.as_self()
141 for node in model.objects.get_nodes(140 for node in model.objects.get_nodes(
142 request.user, NODE_PERMISSION.VIEW,141 request.user, NODE_PERMISSION.VIEW,
143 from_nodes=tag.node_set.all())142 from_nodes=tag.node_set.all())
144143
=== added file 'src/maasserver/api/tests/test_chassis.py'
--- src/maasserver/api/tests/test_chassis.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/api/tests/test_chassis.py 2016-12-07 15:50:52 +0000
@@ -0,0 +1,127 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for chassis API."""
5
6__all__ = []
7
8import http.client
9
10from django.core.urlresolvers import reverse
11from maasserver.enum import (
12 NODE_STATUS,
13 NODE_TYPE,
14)
15from maasserver.testing.api import APITestCase
16from maasserver.testing.factory import factory
17from maasserver.utils.converters import json_load_bytes
18from maasserver.utils.orm import reload_object
19
20
21class TestChassisAPI(APITestCase.ForUser):
22
23 def test_handler_path(self):
24 self.assertEqual(
25 '/api/2.0/chassis/', reverse('chassis_handler'))
26
27 def create_chassis(self, owner, nb=3):
28 return [
29 factory.make_Node(
30 interface=True, node_type=NODE_TYPE.CHASSIS, owner=owner)
31 for _ in range(nb)
32 ]
33
34 def test_read_lists_chassis(self):
35 # The api allows for fetching the list of chassis.
36 chassis = self.create_chassis(owner=self.user)
37 factory.make_Node(
38 status=NODE_STATUS.ALLOCATED, owner=self.user)
39 response = self.client.get(reverse('chassis_handler'))
40 parsed_result = json_load_bytes(response.content)
41
42 self.assertEqual(http.client.OK, response.status_code)
43 self.assertItemsEqual(
44 [chassi.system_id for chassi in chassis],
45 [chassi.get('system_id') for chassi in parsed_result])
46
47 def test_read_ignores_nodes(self):
48 factory.make_Node(
49 status=NODE_STATUS.ALLOCATED, owner=self.user)
50 response = self.client.get(reverse('chassis_handler'))
51 parsed_result = json_load_bytes(response.content)
52
53 self.assertEqual(http.client.OK, response.status_code)
54 self.assertEqual(
55 [],
56 [chassi.get('system_id') for chassi in parsed_result])
57
58 def test_read_with_id_returns_matching_chassis(self):
59 # The "list" operation takes optional "id" parameters. Only
60 # chassis with matching ids will be returned.
61 chassis = self.create_chassis(owner=self.user)
62 ids = [chassi.system_id for chassi in chassis]
63 matching_id = ids[0]
64 response = self.client.get(reverse('chassis_handler'), {
65 'id': [matching_id],
66 })
67 parsed_result = json_load_bytes(response.content)
68 self.assertItemsEqual(
69 [matching_id],
70 [chassi.get('system_id') for chassi in parsed_result])
71
72 def test_read_returns_limited_fields(self):
73 self.create_chassis(owner=self.user)
74 response = self.client.get(reverse('chassis_handler'))
75 parsed_result = json_load_bytes(response.content)
76 self.assertItemsEqual(
77 [
78 'hostname',
79 'system_id',
80 'cpu_count',
81 'memory',
82 'chassis_type',
83 'node_type',
84 'node_type_name',
85 'resource_uri',
86 ],
87 list(parsed_result[0]))
88
89
90def get_chassi_uri(chassis):
91 """Return a chassis URI on the API."""
92 return reverse('chassi_handler', args=[chassis.system_id])
93
94
95class TestChassiAPI(APITestCase.ForUser):
96
97 def test_handler_path(self):
98 system_id = factory.make_name('system-id')
99 self.assertEqual(
100 '/api/2.0/chassis/%s/' % system_id,
101 reverse('chassi_handler', args=[system_id]))
102
103 def test_GET_reads_chassis(self):
104 chassis = factory.make_Node(
105 node_type=NODE_TYPE.CHASSIS, owner=self.user)
106
107 response = self.client.get(get_chassi_uri(chassis))
108 self.assertEqual(
109 http.client.OK, response.status_code, response.content)
110 parsed_chassis = json_load_bytes(response.content)
111 self.assertEqual(chassis.system_id, parsed_chassis["system_id"])
112
113 def test_DELETE_removes_chassis(self):
114 self.become_admin()
115 chassis = factory.make_Node(
116 node_type=NODE_TYPE.CHASSIS, owner=self.user)
117 response = self.client.delete(get_chassi_uri(chassis))
118 self.assertEqual(
119 http.client.NO_CONTENT, response.status_code, response.content)
120 self.assertIsNone(reload_object(chassis))
121
122 def test_DELETE_rejects_deletion_if_not_permitted(self):
123 chassis = factory.make_Node(
124 node_type=NODE_TYPE.CHASSIS, owner=factory.make_User())
125 response = self.client.delete(get_chassi_uri(chassis))
126 self.assertEqual(http.client.FORBIDDEN, response.status_code)
127 self.assertEqual(chassis, reload_object(chassis))
0128
=== modified file 'src/maasserver/api/tests/test_doc.py'
--- src/maasserver/api/tests/test_doc.py 2016-08-31 13:52:59 +0000
+++ src/maasserver/api/tests/test_doc.py 2016-12-07 15:50:52 +0000
@@ -8,6 +8,7 @@
8import http.client8import http.client
9from inspect import getdoc9from inspect import getdoc
10from io import StringIO10from io import StringIO
11import random
11import sys12import sys
12import types13import types
13from unittest.mock import sentinel14from unittest.mock import sentinel
@@ -49,7 +50,7 @@
49from piston3.doc import HandlerDocumentation50from piston3.doc import HandlerDocumentation
50from piston3.handler import BaseHandler51from piston3.handler import BaseHandler
51from piston3.resource import Resource52from piston3.resource import Resource
52from provisioningserver.power.schema import make_json_field53from provisioningserver.drivers.power import PowerDriverRegistry
53from testtools.matchers import (54from testtools.matchers import (
54 AfterPreprocessing,55 AfterPreprocessing,
55 AllMatch,56 AllMatch,
@@ -416,22 +417,19 @@
416 self.assertThat(doc, ContainsAll(["Power types", "IPMI"]))417 self.assertThat(doc, ContainsAll(["Power types", "IPMI"]))
417418
418 def test__generate_power_types_doc_generates_describes_power_type(self):419 def test__generate_power_types_doc_generates_describes_power_type(self):
419 name = factory.make_name('name')420 power_driver = random.choice([
420 description = factory.make_name('description')421 driver
421 param_name = factory.make_name('param_name')422 for _, driver in PowerDriverRegistry
422 param_description = factory.make_name('param_description')423 if len(driver.settings) > 0
423 json_fields = [{424 ])
424 'name': name,
425 'description': description,
426 'fields': [
427 make_json_field(param_name, param_description),
428 ],
429 }]
430 self.patch(doc_module, "JSON_POWER_TYPE_PARAMETERS", json_fields)
431 doc = generate_power_types_doc()425 doc = generate_power_types_doc()
432 self.assertThat(426 self.assertThat(
433 doc,427 doc,
434 ContainsAll([name, description, param_name, param_description]))428 ContainsAll([
429 power_driver.name,
430 power_driver.description,
431 power_driver.settings[0]['name'],
432 power_driver.settings[0]['label']]))
435433
436434
437class TestDescribeCanonical(MAASTestCase):435class TestDescribeCanonical(MAASTestCase):
438436
=== modified file 'src/maasserver/api/tests/test_nodes.py'
--- src/maasserver/api/tests/test_nodes.py 2016-10-28 08:43:09 +0000
+++ src/maasserver/api/tests/test_nodes.py 2016-12-07 15:50:52 +0000
@@ -304,7 +304,6 @@
304 response = self.client.get(reverse('nodes_handler'))304 response = self.client.get(reverse('nodes_handler'))
305 parsed_result = json.loads(305 parsed_result = json.loads(
306 response.content.decode(settings.DEFAULT_CHARSET))306 response.content.decode(settings.DEFAULT_CHARSET))
307
308 self.assertEqual(http.client.OK, response.status_code)307 self.assertEqual(http.client.OK, response.status_code)
309 self.assertItemsEqual(system_ids, extract_system_ids(parsed_result))308 self.assertItemsEqual(system_ids, extract_system_ids(parsed_result))
310309
311310
=== added file 'src/maasserver/api/tests/test_storage.py'
--- src/maasserver/api/tests/test_storage.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/api/tests/test_storage.py 2016-12-07 15:50:52 +0000
@@ -0,0 +1,125 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for storage API."""
5
6__all__ = []
7
8import http.client
9
10from django.core.urlresolvers import reverse
11from maasserver.enum import (
12 NODE_STATUS,
13 NODE_TYPE,
14)
15from maasserver.testing.api import APITestCase
16from maasserver.testing.factory import factory
17from maasserver.utils.converters import json_load_bytes
18from maasserver.utils.orm import reload_object
19
20
21class TestStoragesAPI(APITestCase.ForUser):
22
23 def test_handler_path(self):
24 self.assertEqual(
25 '/api/2.0/storages/', reverse('storages_handler'))
26
27 def create_storages(self, owner, nb=3):
28 return [
29 factory.make_Node(
30 interface=True, node_type=NODE_TYPE.STORAGE, owner=owner)
31 for _ in range(nb)
32 ]
33
34 def test_read_lists_storage(self):
35 # The api allows for fetching the list of storages.
36 storages = self.create_storages(owner=self.user)
37 factory.make_Node(
38 status=NODE_STATUS.ALLOCATED, owner=self.user)
39 response = self.client.get(reverse('storages_handler'))
40 parsed_result = json_load_bytes(response.content)
41
42 self.assertEqual(http.client.OK, response.status_code)
43 self.assertItemsEqual(
44 [storage.system_id for storage in storages],
45 [storage.get('system_id') for storage in parsed_result])
46
47 def test_read_ignores_nodes(self):
48 factory.make_Node(
49 status=NODE_STATUS.ALLOCATED, owner=self.user)
50 response = self.client.get(reverse('storages_handler'))
51 parsed_result = json_load_bytes(response.content)
52
53 self.assertEqual(http.client.OK, response.status_code)
54 self.assertEqual(
55 [],
56 [storage.get('system_id') for storage in parsed_result])
57
58 def test_read_with_id_returns_matching_storage(self):
59 # The "list" operation takes optional "id" parameters. Only
60 # storages with matching ids will be returned.
61 storages = self.create_storages(owner=self.user)
62 ids = [storage.system_id for storage in storages]
63 matching_id = ids[0]
64 response = self.client.get(reverse('storages_handler'), {
65 'id': [matching_id],
66 })
67 parsed_result = json_load_bytes(response.content)
68 self.assertItemsEqual(
69 [matching_id],
70 [storage.get('system_id') for storage in parsed_result])
71
72 def test_read_returns_limited_fields(self):
73 self.create_storages(owner=self.user)
74 response = self.client.get(reverse('storages_handler'))
75 parsed_result = json_load_bytes(response.content)
76 self.assertItemsEqual(
77 [
78 'hostname',
79 'system_id',
80 'storage_type',
81 'node_type',
82 'node_type_name',
83 'resource_uri',
84 ],
85 list(parsed_result[0]))
86
87
88def get_storage_uri(storage):
89 """Return a storage's URI on the API."""
90 return reverse('storage_handler', args=[storage.system_id])
91
92
93class TestStorageAPI(APITestCase.ForUser):
94
95 def test_handler_path(self):
96 system_id = factory.make_name('system-id')
97 self.assertEqual(
98 '/api/2.0/storages/%s/' % system_id,
99 reverse('storage_handler', args=[system_id]))
100
101 def test_GET_reads_storage(self):
102 storage = factory.make_Node(
103 node_type=NODE_TYPE.STORAGE, owner=self.user)
104
105 response = self.client.get(get_storage_uri(storage))
106 self.assertEqual(
107 http.client.OK, response.status_code, response.content)
108 parsed_storage = json_load_bytes(response.content)
109 self.assertEqual(storage.system_id, parsed_storage["system_id"])
110
111 def test_DELETE_removes_storage(self):
112 self.become_admin()
113 storage = factory.make_Node(
114 node_type=NODE_TYPE.STORAGE, owner=self.user)
115 response = self.client.delete(get_storage_uri(storage))
116 self.assertEqual(
117 http.client.NO_CONTENT, response.status_code, response.content)
118 self.assertIsNone(reload_object(storage))
119
120 def test_DELETE_rejects_deletion_if_not_permitted(self):
121 storage = factory.make_Node(
122 node_type=NODE_TYPE.STORAGE, owner=factory.make_User())
123 response = self.client.delete(get_storage_uri(storage))
124 self.assertEqual(http.client.FORBIDDEN, response.status_code)
125 self.assertEqual(storage, reload_object(storage))
0126
=== modified file 'src/maasserver/api/tests/test_subnets.py'
--- src/maasserver/api/tests/test_subnets.py 2016-10-20 21:30:58 +0000
+++ src/maasserver/api/tests/test_subnets.py 2016-12-07 15:50:52 +0000
@@ -87,6 +87,7 @@
87 rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)87 rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)
88 allow_proxy = factory.pick_bool()88 allow_proxy = factory.pick_bool()
89 gateway_ip = factory.pick_ip_in_network(network)89 gateway_ip = factory.pick_ip_in_network(network)
90 managed = factory.pick_bool()
90 dns_servers = []91 dns_servers = []
91 for _ in range(2):92 for _ in range(2):
92 dns_servers.append(93 dns_servers.append(
@@ -102,6 +103,7 @@
102 "dns_servers": ','.join(dns_servers),103 "dns_servers": ','.join(dns_servers),
103 "rdns_mode": rdns_mode,104 "rdns_mode": rdns_mode,
104 "allow_proxy": allow_proxy,105 "allow_proxy": allow_proxy,
106 "managed": managed,
105 })107 })
106 self.assertEqual(108 self.assertEqual(
107 http.client.OK, response.status_code, response.content)109 http.client.OK, response.status_code, response.content)
@@ -115,6 +117,7 @@
115 self.assertEqual(dns_servers, created_subnet['dns_servers'])117 self.assertEqual(dns_servers, created_subnet['dns_servers'])
116 self.assertEqual(rdns_mode, created_subnet['rdns_mode'])118 self.assertEqual(rdns_mode, created_subnet['rdns_mode'])
117 self.assertEqual(allow_proxy, created_subnet['allow_proxy'])119 self.assertEqual(allow_proxy, created_subnet['allow_proxy'])
120 self.assertEqual(managed, created_subnet['managed'])
118121
119 def test_create_defaults_to_allow_proxy(self):122 def test_create_defaults_to_allow_proxy(self):
120 self.become_admin()123 self.become_admin()
@@ -151,7 +154,43 @@
151 self.assertEqual(gateway_ip, created_subnet['gateway_ip'])154 self.assertEqual(gateway_ip, created_subnet['gateway_ip'])
152 self.assertEqual(dns_servers, created_subnet['dns_servers'])155 self.assertEqual(dns_servers, created_subnet['dns_servers'])
153 self.assertEqual(rdns_mode, created_subnet['rdns_mode'])156 self.assertEqual(rdns_mode, created_subnet['rdns_mode'])
154 self.assertEqual(True, created_subnet['allow_proxy'])157
158 def test_create_defaults_to_managed(self):
159 self.become_admin()
160 subnet_name = factory.make_name("subnet")
161 vlan = factory.make_VLAN()
162 space = factory.make_Space()
163 network = factory.make_ip4_or_6_network()
164 cidr = str(network.cidr)
165 rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)
166 gateway_ip = factory.pick_ip_in_network(network)
167 dns_servers = []
168 for _ in range(2):
169 dns_servers.append(
170 factory.pick_ip_in_network(
171 network, but_not=[gateway_ip] + dns_servers))
172 uri = get_subnets_uri()
173 response = self.client.post(uri, {
174 "name": subnet_name,
175 "vlan": vlan.id,
176 "space": space.id,
177 "cidr": cidr,
178 "gateway_ip": gateway_ip,
179 "dns_servers": ','.join(dns_servers),
180 "rdns_mode": rdns_mode,
181 })
182 self.assertEqual(
183 http.client.OK, response.status_code, response.content)
184 created_subnet = json.loads(
185 response.content.decode(settings.DEFAULT_CHARSET))
186 self.assertEqual(subnet_name, created_subnet['name'])
187 self.assertEqual(vlan.vid, created_subnet['vlan']['vid'])
188 self.assertEqual(space.get_name(), created_subnet['space'])
189 self.assertEqual(cidr, created_subnet['cidr'])
190 self.assertEqual(gateway_ip, created_subnet['gateway_ip'])
191 self.assertEqual(dns_servers, created_subnet['dns_servers'])
192 self.assertEqual(rdns_mode, created_subnet['rdns_mode'])
193 self.assertEqual(True, created_subnet['managed'])
155194
156 def test_create_admin_only(self):195 def test_create_admin_only(self):
157 subnet_name = factory.make_name("subnet")196 subnet_name = factory.make_name("subnet")
@@ -200,6 +239,7 @@
200 "cidr": Equals(subnet.cidr),239 "cidr": Equals(subnet.cidr),
201 "gateway_ip": Equals(subnet.gateway_ip),240 "gateway_ip": Equals(subnet.gateway_ip),
202 "dns_servers": Equals(subnet.dns_servers),241 "dns_servers": Equals(subnet.dns_servers),
242 "managed": Equals(subnet.managed),
203 }))243 }))
204244
205 def test_read_404_when_bad_id(self):245 def test_read_404_when_bad_id(self):
@@ -232,20 +272,24 @@
232 new_name = factory.make_name("subnet")272 new_name = factory.make_name("subnet")
233 new_rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)273 new_rdns_mode = factory.pick_choice(RDNS_MODE_CHOICES)
234 new_allow_proxy = factory.pick_bool()274 new_allow_proxy = factory.pick_bool()
275 new_managed = factory.pick_bool()
235 uri = get_subnet_uri(subnet)276 uri = get_subnet_uri(subnet)
236 response = self.client.put(uri, {277 response = self.client.put(uri, {
237 "name": new_name,278 "name": new_name,
238 "rdns_mode": new_rdns_mode,279 "rdns_mode": new_rdns_mode,
239 "allow_proxy": new_allow_proxy,280 "allow_proxy": new_allow_proxy,
281 "managed": new_managed,
240 })282 })
241 self.assertEqual(283 self.assertEqual(
242 http.client.OK, response.status_code, response.content)284 http.client.OK, response.status_code, response.content)
243 self.assertEqual(285 self.assertEqual(
244 new_name, json.loads(286 new_name, json.loads(
245 response.content.decode(settings.DEFAULT_CHARSET))['name'])287 response.content.decode(settings.DEFAULT_CHARSET))['name'])
246 self.assertEqual(new_name, reload_object(subnet).name)288 subnet = reload_object(subnet)
247 self.assertEqual(new_rdns_mode, reload_object(subnet).rdns_mode)289 self.assertEqual(new_name, subnet.name)
248 self.assertEqual(new_allow_proxy, reload_object(subnet).allow_proxy)290 self.assertEqual(new_rdns_mode, subnet.rdns_mode)
291 self.assertEqual(new_allow_proxy, subnet.allow_proxy)
292 self.assertEqual(new_managed, subnet.managed)
249293
250 def test_update_admin_only(self):294 def test_update_admin_only(self):
251 subnet = factory.make_Subnet()295 subnet = factory.make_Subnet()
252296
=== modified file 'src/maasserver/api/tests/test_vlans.py'
--- src/maasserver/api/tests/test_vlans.py 2016-05-24 21:29:53 +0000
+++ src/maasserver/api/tests/test_vlans.py 2016-12-07 15:50:52 +0000
@@ -84,6 +84,29 @@
84 self.assertEqual(vid, response_data['vid'])84 self.assertEqual(vid, response_data['vid'])
85 self.assertEqual(mtu, response_data['mtu'])85 self.assertEqual(mtu, response_data['mtu'])
8686
87 def test_create_with_relay_vlan(self):
88 self.become_admin()
89 fabric = factory.make_Fabric()
90 vlan_name = factory.make_name("fabric")
91 vid = random.randint(1, 1000)
92 mtu = random.randint(552, 1500)
93 relay_vlan = factory.make_VLAN()
94 uri = get_vlans_uri(fabric)
95 response = self.client.post(uri, {
96 "name": vlan_name,
97 "vid": vid,
98 "mtu": mtu,
99 "relay_vlan": relay_vlan.id,
100 })
101 self.assertEqual(
102 http.client.OK, response.status_code, response.content)
103 response_data = json.loads(
104 response.content.decode(settings.DEFAULT_CHARSET))
105 self.assertEqual(vlan_name, response_data['name'])
106 self.assertEqual(vid, response_data['vid'])
107 self.assertEqual(mtu, response_data['mtu'])
108 self.assertEqual(relay_vlan.vid, response_data['relay_vlan']['vid'])
109
87 def test_create_admin_only(self):110 def test_create_admin_only(self):
88 fabric = factory.make_Fabric()111 fabric = factory.make_Fabric()
89 vlan_name = factory.make_name("fabric")112 vlan_name = factory.make_name("fabric")
@@ -182,6 +205,23 @@
182 self.assertEqual(new_vid, parsed_vlan['vid'])205 self.assertEqual(new_vid, parsed_vlan['vid'])
183 self.assertEqual(new_vid, vlan.vid)206 self.assertEqual(new_vid, vlan.vid)
184207
208 def test_update_sets_relay_vlan(self):
209 self.become_admin()
210 fabric = factory.make_Fabric()
211 vlan = factory.make_VLAN(fabric=fabric)
212 uri = get_vlan_uri(vlan)
213 relay_vlan = factory.make_VLAN()
214 response = self.client.put(uri, {
215 "relay_vlan": relay_vlan.id,
216 })
217 self.assertEqual(
218 http.client.OK, response.status_code, response.content)
219 parsed_vlan = json.loads(
220 response.content.decode(settings.DEFAULT_CHARSET))
221 vlan = reload_object(vlan)
222 self.assertEqual(relay_vlan.vid, parsed_vlan['relay_vlan']['vid'])
223 self.assertEqual(relay_vlan, vlan.relay_vlan)
224
185 def test_update_with_fabric(self):225 def test_update_with_fabric(self):
186 self.become_admin()226 self.become_admin()
187 fabric = factory.make_Fabric()227 fabric = factory.make_Fabric()
188228
=== modified file 'src/maasserver/api/vlans.py'
--- src/maasserver/api/vlans.py 2016-04-27 20:40:24 +0000
+++ src/maasserver/api/vlans.py 2016-12-07 15:50:52 +0000
@@ -26,6 +26,7 @@
26 'secondary_rack',26 'secondary_rack',
27 'dhcp_on',27 'dhcp_on',
28 'external_dhcp',28 'external_dhcp',
29 'relay_vlan',
29)30)
3031
3132
@@ -165,12 +166,18 @@
165 :type vid: integer166 :type vid: integer
166 :param mtu: The MTU to use on the VLAN.167 :param mtu: The MTU to use on the VLAN.
167 :type mtu: integer168 :type mtu: integer
168 :Param dhcp_on: Whether or not DHCP should be managed on the VLAN.169 :param dhcp_on: Whether or not DHCP should be managed on the VLAN.
169 :type dhcp_on: boolean170 :type dhcp_on: boolean
170 :param primary_rack: The primary rack controller managing the VLAN.171 :param primary_rack: The primary rack controller managing the VLAN.
171 :type primary_rack: system_id172 :type primary_rack: system_id
172 :param secondary_rack: The secondary rack controller manging the VLAN.173 :param secondary_rack: The secondary rack controller manging the VLAN.
173 :type secondary_rack: system_id174 :type secondary_rack: system_id
175 :param relay_vlan: Only set when this VLAN will be using a DHCP relay
176 to forward DHCP requests to another VLAN that MAAS is or will run
177 the DHCP server. MAAS will not run the DHCP relay itself, it must
178 be configured to proxy reqests to the primary and/or secondary
179 rack controller interfaces for the VLAN specified in this field.
180 :type relay_vlan: ID of VLAN
174181
175 Returns 404 if the fabric or VLAN is not found.182 Returns 404 if the fabric or VLAN is not found.
176 """183 """
177184
=== modified file 'src/maasserver/bootresources.py'
--- src/maasserver/bootresources.py 2016-10-28 15:58:32 +0000
+++ src/maasserver/bootresources.py 2016-12-07 15:50:52 +0000
@@ -60,6 +60,7 @@
60 BootResourceSet,60 BootResourceSet,
61 BootSourceSelection,61 BootSourceSelection,
62 Config,62 Config,
63 Event,
63 LargeFile,64 LargeFile,
64)65)
65from maasserver.rpc import getAllClients66from maasserver.rpc import getAllClients
@@ -78,6 +79,7 @@
78from maasserver.utils.threads import deferToDatabase79from maasserver.utils.threads import deferToDatabase
79from maasserver.utils.version import get_maas_version_ui80from maasserver.utils.version import get_maas_version_ui
80from provisioningserver.config import is_dev_environment81from provisioningserver.config import is_dev_environment
82from provisioningserver.events import EVENT_TYPES
81from provisioningserver.import_images.download_descriptions import (83from provisioningserver.import_images.download_descriptions import (
82 download_all_image_descriptions,84 download_all_image_descriptions,
83 image_passes_filter,85 image_passes_filter,
@@ -661,10 +663,13 @@
661 # not allowed.663 # not allowed.
662 prev_largefile = largefile664 prev_largefile = largefile
663 largefile = None665 largefile = None
664 maaslog.warning(666 msg = (
665 "Hash mismatch for prev_file=%s resourceset=%s "667 "Hash mismatch for prev_file=%s resourceset=%s "
666 "resource=%s",668 "resource=%s" % (prev_largefile, resource_set, resource)
667 prev_largefile, resource_set, resource)669 )
670 Event.objects.create_region_event(
671 EVENT_TYPES.REGION_IMPORT_WARNING, msg)
672 maaslog.warning(msg)
668673
669 if largefile is None:674 if largefile is None:
670 # The resource file current does not have a largefile linked. Lets675 # The resource file current does not have a largefile linked. Lets
@@ -695,8 +700,10 @@
695 is_resource_initially_complete and700 is_resource_initially_complete and
696 resource.get_latest_complete_set() is None)701 resource.get_latest_complete_set() is None)
697 if is_resource_broken:702 if is_resource_broken:
698 maaslog.error(703 msg = "Resource %s has no complete resource set!" % resource
699 "Resource %s has no complete resource set!", resource)704 Event.objects.create_region_event(
705 EVENT_TYPES.REGION_IMPORT_ERROR, msg)
706 maaslog.error(msg)
700707
701 if prev_largefile is not None:708 if prev_largefile is not None:
702 # If the previous largefile had a miss matching sha256 then it709 # If the previous largefile had a miss matching sha256 then it
@@ -773,11 +780,15 @@
773 # Calculated sha256 hash from the data does not match, what780 # Calculated sha256 hash from the data does not match, what
774 # simplestreams is telling us it should be. This resource file781 # simplestreams is telling us it should be. This resource file
775 # will be deleted since it is corrupt.782 # will be deleted since it is corrupt.
776 maaslog.error(783 msg = (
777 "Failed to finalize boot image %s. Unexpected "784 "Failed to finalize boot image %s. Unexpected "
778 "checksum '%s' (found: %s expected: %s)",785 "checksum '%s' (found: %s expected: %s)" %
779 ident, cksummer.algorithm,786 (
780 cksummer.hexdigest(), cksummer.expected)787 ident, cksummer.algorithm, cksummer.hexdigest(),
788 cksummer.expected))
789 Event.objects.create_region_event(
790 EVENT_TYPES.REGION_IMPORT_ERROR, msg)
791 maaslog.error(msg)
781 transactional(rfile.delete)()792 transactional(rfile.delete)()
782 else:793 else:
783 maaslog.debug('Finalized boot image %s.', ident)794 maaslog.debug('Finalized boot image %s.', ident)
@@ -877,11 +888,15 @@
877 self.get_resource_identity(delete_resource))888 self.get_resource_identity(delete_resource))
878 delete_resource.delete()889 delete_resource.delete()
879 else:890 else:
880 maaslog.info(891 msg = (
881 "Boot image %s no longer exists in stream, but "892 "Boot image %s no longer exists in stream, but "
882 "remains in selections. To delete this image "893 "remains in selections. To delete this image "
883 "remove its selection.",894 "remove its selection." %
884 self.get_resource_identity(delete_resource))895 self.get_resource_identity(delete_resource)
896 )
897 Event.objects.create_region_event(
898 EVENT_TYPES.REGION_IMPORT_INFO, msg)
899 maaslog.info(msg)
885 else:900 else:
886 # No resource set on the boot resource so it should be901 # No resource set on the boot resource so it should be
887 # removed as it has not files.902 # removed as it has not files.
@@ -960,6 +975,8 @@
960 "Finalization of imported images skipped, "975 "Finalization of imported images skipped, "
961 "or all %s synced images would be deleted." % (976 "or all %s synced images would be deleted." % (
962 self._resources_to_delete))977 self._resources_to_delete))
978 Event.objects.create_region_event(
979 EVENT_TYPES.REGION_IMPORT_ERROR, error_msg)
963 maaslog.error(error_msg)980 maaslog.error(error_msg)
964 if notify is not None:981 if notify is not None:
965 failure = Failure(Exception(error_msg))982 failure = Failure(Exception(error_msg))
@@ -1192,7 +1209,10 @@
11921209
1193 # Download all of the metadata first.1210 # Download all of the metadata first.
1194 for source in sources:1211 for source in sources:
1195 maaslog.info("Importing images from source: %s", source['url'])1212 msg = "Importing images from source: %s" % source['url']
1213 Event.objects.create_region_event(
1214 EVENT_TYPES.REGION_IMPORT_INFO, msg)
1215 maaslog.info(msg)
1196 download_boot_resources(1216 download_boot_resources(
1197 source['url'], store, product_mapping,1217 source['url'], store, product_mapping,
1198 keyring_file=source.get('keyring'))1218 keyring_file=source.get('keyring'))
@@ -1318,15 +1338,21 @@
1318 with tempdir('keyrings') as keyrings_path:1338 with tempdir('keyrings') as keyrings_path:
1319 sources = get_boot_sources()1339 sources = get_boot_sources()
1320 sources = write_all_keyrings(keyrings_path, sources)1340 sources = write_all_keyrings(keyrings_path, sources)
1321 maaslog.info(1341 msg = (
1322 "Started importing of boot images from %d source(s).",1342 "Started importing of boot images from %d source(s)." %
1323 len(sources))1343 len(sources))
1344 Event.objects.create_region_event(EVENT_TYPES.REGION_IMPORT_INFO, msg)
1345 maaslog.info(msg)
13241346
1325 image_descriptions = download_all_image_descriptions(sources)1347 image_descriptions = download_all_image_descriptions(sources)
1326 if image_descriptions.is_empty():1348 if image_descriptions.is_empty():
1327 maaslog.warning(1349 msg = (
1328 "Unable to import boot images, no image "1350 "Unable to import boot images, no image "
1329 "descriptions avaliable.")1351 "descriptions avaliable."
1352 )
1353 Event.objects.create_region_event(
1354 EVENT_TYPES.REGION_IMPORT_WARNING, msg)
1355 maaslog.warning(msg)
1330 return1356 return
1331 product_mapping = map_products(image_descriptions)1357 product_mapping = map_products(image_descriptions)
13321358
13331359
=== modified file 'src/maasserver/clusterrpc/power_parameters.py'
--- src/maasserver/clusterrpc/power_parameters.py 2016-10-20 19:41:25 +0000
+++ src/maasserver/clusterrpc/power_parameters.py 2016-12-07 15:50:52 +0000
@@ -14,7 +14,7 @@
14power type with a set of power parameters.14power type with a set of power parameters.
1515
16The power types are retrieved from the cluster controllers using the json16The power types are retrieved from the cluster controllers using the json
17schema provisioningserver.power_schema.JSON_POWER_TYPE_SCHEMA. To add new17schema provisioningserver.drivers.power.JSON_POWER_DRIVERS_SCHEMA. To add new
18parameters requires changes to hardware drivers that run in the cluster18parameters requires changes to hardware drivers that run in the cluster
19controllers.19controllers.
20"""20"""
@@ -33,10 +33,8 @@
33from maasserver.config_forms import DictCharField33from maasserver.config_forms import DictCharField
34from maasserver.fields import MACAddressFormField34from maasserver.fields import MACAddressFormField
35from maasserver.utils.forms import compose_invalid_choice_text35from maasserver.utils.forms import compose_invalid_choice_text
36from provisioningserver.power.schema import (36from provisioningserver.drivers import SETTING_PARAMETER_FIELD_SCHEMA
37 JSON_POWER_TYPE_SCHEMA,37from provisioningserver.drivers.power import JSON_POWER_DRIVERS_SCHEMA
38 POWER_TYPE_PARAMETER_FIELD_SCHEMA,
39)
40from provisioningserver.rpc import cluster38from provisioningserver.rpc import cluster
4139
4240
@@ -93,10 +91,10 @@
93 :type description: string91 :type description: string
94 :param fields: The fields that make up the parameters for the power92 :param fields: The fields that make up the parameters for the power
95 type. Will be validated against93 type. Will be validated against
96 POWER_TYPE_PARAMETER_FIELD_SCHEMA.94 SETTING_PARAMETER_FIELD_SCHEMA.
97 :param missing_packages: System packages that must be installed on95 :param missing_packages: System packages that must be installed on
98 the cluster before the power type can be used.96 the cluster before the power type can be used.
99 :type fields: list of `make_json_field` results.97 :type fields: list of `make_setting_field` results.
100 :param parameters_set: An existing list of power type parameters to98 :param parameters_set: An existing list of power type parameters to
101 mutate.99 mutate.
102 :type parameters_set: list100 :type parameters_set: list
@@ -107,7 +105,7 @@
107 field_set_schema = {105 field_set_schema = {
108 'title': "Power type parameters field set schema",106 'title': "Power type parameters field set schema",
109 'type': 'array',107 'type': 'array',
110 'items': POWER_TYPE_PARAMETER_FIELD_SCHEMA,108 'items': SETTING_PARAMETER_FIELD_SCHEMA,
111 }109 }
112 validate(fields, field_set_schema)110 validate(fields, field_set_schema)
113 parameters_set.append(111 parameters_set.append(
@@ -132,7 +130,7 @@
132 :return: A dict of power parameters for all power types, indexed by130 :return: A dict of power parameters for all power types, indexed by
133 power type name.131 power type name.
134 """132 """
135 validate(json_power_type_parameters, JSON_POWER_TYPE_SCHEMA)133 validate(json_power_type_parameters, JSON_POWER_DRIVERS_SCHEMA)
136 power_parameters = {134 power_parameters = {
137 # Empty type, for the case where nothing is entered in the form yet.135 # Empty type, for the case where nothing is entered in the form yet.
138 '': DictCharField(136 '': DictCharField(
@@ -197,7 +195,7 @@
197 """Query every cluster controller and obtain all known power types.195 """Query every cluster controller and obtain all known power types.
198196
199 :return: a list of power types matching the schema197 :return: a list of power types matching the schema
200 provisioningserver.power_schema.JSON_POWER_TYPE_PARAMETERS_SCHEMA198 provisioningserver.drivers.power.JSON_POWER_DRIVERS_SCHEMA
201 """199 """
202 merged_types = []200 merged_types = []
203 responses = call_clusters(201 responses = call_clusters(
204202
=== modified file 'src/maasserver/clusterrpc/testing/power_parameters.py'
--- src/maasserver/clusterrpc/testing/power_parameters.py 2016-06-22 17:03:02 +0000
+++ src/maasserver/clusterrpc/testing/power_parameters.py 2016-12-07 15:50:52 +0000
@@ -11,7 +11,7 @@
1111
12from fixtures import Fixture12from fixtures import Fixture
13from maasserver.clusterrpc import power_parameters13from maasserver.clusterrpc import power_parameters
14from provisioningserver.power import schema14from provisioningserver.drivers.power import PowerDriverRegistry
15from testtools import monkey15from testtools import monkey
1616
1717
@@ -26,7 +26,9 @@
26 super(StaticPowerTypesFixture, self).setUp()26 super(StaticPowerTypesFixture, self).setUp()
27 # This patch prevents communication with a non-existent cluster27 # This patch prevents communication with a non-existent cluster
28 # controller when fetching power types.28 # controller when fetching power types.
29 power_types = PowerDriverRegistry.get_schema(
30 detect_missing_packages=False)
29 restore = monkey.patch(31 restore = monkey.patch(
30 power_parameters, 'get_all_power_types_from_clusters',32 power_parameters, 'get_all_power_types_from_clusters',
31 Mock(return_value=schema.JSON_POWER_TYPE_PARAMETERS))33 Mock(return_value=power_types))
32 self.addCleanup(restore)34 self.addCleanup(restore)
3335
=== modified file 'src/maasserver/clusterrpc/tests/test_power_parameters.py'
--- src/maasserver/clusterrpc/tests/test_power_parameters.py 2016-10-20 08:41:30 +0000
+++ src/maasserver/clusterrpc/tests/test_power_parameters.py 2016-12-07 15:50:52 +0000
@@ -14,9 +14,9 @@
14 add_power_type_parameters,14 add_power_type_parameters,
15 get_power_type_parameters_from_json,15 get_power_type_parameters_from_json,
16 get_power_types,16 get_power_types,
17 JSON_POWER_TYPE_SCHEMA,17 JSON_POWER_DRIVERS_SCHEMA,
18 make_form_field,18 make_form_field,
19 POWER_TYPE_PARAMETER_FIELD_SCHEMA,19 SETTING_PARAMETER_FIELD_SCHEMA,
20)20)
21from maasserver.config_forms import DictCharField21from maasserver.config_forms import DictCharField
22from maasserver.fields import MACAddressFormField22from maasserver.fields import MACAddressFormField
@@ -25,7 +25,7 @@
25from maasserver.utils.forms import compose_invalid_choice_text25from maasserver.utils.forms import compose_invalid_choice_text
26from maastesting.matchers import MockCalledOnceWith26from maastesting.matchers import MockCalledOnceWith
27from maastesting.testcase import MAASTestCase27from maastesting.testcase import MAASTestCase
28from provisioningserver.power.schema import make_json_field28from provisioningserver.drivers import make_setting_field
2929
3030
31class TestGetPowerTypeParametersFromJSON(MAASServerTestCase):31class TestGetPowerTypeParametersFromJSON(MAASServerTestCase):
@@ -186,15 +186,15 @@
186 self.assertEquals(json_field['default'], django_field.initial)186 self.assertEquals(json_field['default'], django_field.initial)
187187
188188
189class TestMakeJSONField(MAASServerTestCase):189class TestMakeSettingField(MAASServerTestCase):
190 """Test that make_json_field() creates JSON-verifiable fields."""190 """Test that make_setting_field() creates JSON-verifiable fields."""
191191
192 def test__returns_json_verifiable_dict(self):192 def test__returns_json_verifiable_dict(self):
193 json_field = make_json_field('some_field', 'Some Label')193 json_field = make_setting_field('some_field', 'Some Label')
194 jsonschema.validate(json_field, POWER_TYPE_PARAMETER_FIELD_SCHEMA)194 jsonschema.validate(json_field, SETTING_PARAMETER_FIELD_SCHEMA)
195195
196 def test__provides_sane_default_values(self):196 def test__provides_sane_default_values(self):
197 json_field = make_json_field('some_field', 'Some Label')197 json_field = make_setting_field('some_field', 'Some Label')
198 expected_field = {198 expected_field = {
199 'name': 'some_field',199 'name': 'some_field',
200 'label': 'Some Label',200 'label': 'Some Label',
@@ -219,16 +219,16 @@
219 'default': 'spam',219 'default': 'spam',
220 'scope': 'bmc',220 'scope': 'bmc',
221 }221 }
222 json_field = make_json_field(**expected_field)222 json_field = make_setting_field(**expected_field)
223 self.assertEqual(expected_field, json_field)223 self.assertEqual(expected_field, json_field)
224224
225 def test__validates_choices(self):225 def test__validates_choices(self):
226 self.assertRaises(226 self.assertRaises(
227 jsonschema.ValidationError, make_json_field,227 jsonschema.ValidationError, make_setting_field,
228 'some_field', 'Some Label', choices="Nonsense")228 'some_field', 'Some Label', choices="Nonsense")
229229
230 def test__creates_password_fields(self):230 def test__creates_password_fields(self):
231 json_field = make_json_field(231 json_field = make_setting_field(
232 'some_field', 'Some Label', field_type='password')232 'some_field', 'Some Label', field_type='password')
233 expected_field = {233 expected_field = {
234 'name': 'some_field',234 'name': 'some_field',
@@ -245,7 +245,7 @@
245class TestAddPowerTypeParameters(MAASServerTestCase):245class TestAddPowerTypeParameters(MAASServerTestCase):
246246
247 def make_field(self):247 def make_field(self):
248 return make_json_field(248 return make_setting_field(
249 self.getUniqueString(), self.getUniqueString())249 self.getUniqueString(), self.getUniqueString())
250250
251 def test_adding_existing_types_is_a_no_op(self):251 def test_adding_existing_types_is_a_no_op(self):
@@ -289,7 +289,7 @@
289 missing_packages=[],289 missing_packages=[],
290 parameters_set=parameters_set)290 parameters_set=parameters_set)
291 jsonschema.validate(291 jsonschema.validate(
292 parameters_set, JSON_POWER_TYPE_SCHEMA)292 parameters_set, JSON_POWER_DRIVERS_SCHEMA)
293293
294294
295class TestPowerTypes(MAASTestCase):295class TestPowerTypes(MAASTestCase):
296296
=== modified file 'src/maasserver/dhcp.py'
--- src/maasserver/dhcp.py 2016-11-01 16:46:19 +0000
+++ src/maasserver/dhcp.py 2016-12-07 15:50:52 +0000
@@ -29,10 +29,7 @@
29 IPRANGE_TYPE,29 IPRANGE_TYPE,
30 SERVICE_STATUS,30 SERVICE_STATUS,
31)31)
32from maasserver.exceptions import (32from maasserver.exceptions import UnresolvableHost
33 DHCPConfigurationError,
34 UnresolvableHost,
35)
36from maasserver.models import (33from maasserver.models import (
37 Config,34 Config,
38 DHCPSnippet,35 DHCPSnippet,
@@ -40,6 +37,7 @@
40 RackController,37 RackController,
41 Service,38 Service,
42 StaticIPAddress,39 StaticIPAddress,
40 Subnet,
43)41)
44from maasserver.rpc import (42from maasserver.rpc import (
45 getAllClients,43 getAllClients,
@@ -88,14 +86,14 @@
88 return key86 return key
8987
9088
91def split_ipv4_ipv6_subnets(subnets):89def split_managed_ipv4_ipv6_subnets(subnets: Iterable[Subnet]):
92 """Divide `subnets` into IPv4 ones and IPv6 ones.90 """Divide `subnets` into IPv4 ones and IPv6 ones.
9391
94 :param subnets: A sequence of subnets.92 :param subnets: A sequence of subnets.
95 :return: A tuple of two separate sequences: IPv4 subnets and IPv6 subnets.93 :return: A tuple of two separate sequences: IPv4 subnets and IPv6 subnets.
96 """94 """
97 split = defaultdict(list)95 split = defaultdict(list)
98 for subnet in subnets:96 for subnet in (s for s in subnets if s.managed is True):
99 split[subnet.get_ipnetwork().version].append(subnet)97 split[subnet.get_ipnetwork().version].append(subnet)
100 assert len(split) <= 2, (98 assert len(split) <= 2, (
101 "Unexpected IP version(s): %s" % ', '.join(list(split.keys())))99 "Unexpected IP version(s): %s" % ', '.join(list(split.keys())))
@@ -193,18 +191,19 @@
193 return []191 return []
194192
195193
196def get_managed_vlans_for(rack_controller):194def gen_managed_vlans_for(rack_controller):
197 """Return list of `VLAN` for the `rack_controller` when DHCP is enabled and195 """Yeilds each `VLAN` for the `rack_controller` when DHCP is enabled and
198 `rack_controller` is either the `primary_rack` or the `secondary_rack`.196 `rack_controller` is either the `primary_rack` or the `secondary_rack`.
199 """197 """
200 interfaces = rack_controller.interface_set.filter(198 interfaces = rack_controller.interface_set.filter(
201 Q(vlan__dhcp_on=True) & (199 Q(vlan__dhcp_on=True) & (
202 Q(vlan__primary_rack=rack_controller) |200 Q(vlan__primary_rack=rack_controller) |
203 Q(vlan__secondary_rack=rack_controller))).select_related("vlan")201 Q(vlan__secondary_rack=rack_controller)))
204 return {202 interfaces = interfaces.prefetch_related("vlan__relay_vlans")
205 interface.vlan203 for interface in interfaces:
206 for interface in interfaces204 yield interface.vlan
207 }205 for relayed_vlan in interface.vlan.relay_vlans.all():
206 yield relayed_vlan
208207
209208
210def ip_is_on_vlan(ip_address, vlan):209def ip_is_on_vlan(ip_address, vlan):
@@ -459,12 +458,6 @@
459 interfaces = get_interfaces_with_ip_on_vlan(458 interfaces = get_interfaces_with_ip_on_vlan(
460 rack_controller, vlan, ip_version)459 rack_controller, vlan, ip_version)
461 interface = get_best_interface(interfaces)460 interface = get_best_interface(interfaces)
462 if interface is None:
463 raise DHCPConfigurationError(
464 "No IPv%d interface on rack controller '%s' has an IP address on "
465 "any subnet on VLAN '%s.%d'." % (
466 ip_version, rack_controller.hostname, vlan.fabric.name,
467 vlan.vid))
468461
469 # Generate the failover peer for this VLAN.462 # Generate the failover peer for this VLAN.
470 if vlan.secondary_rack_id is not None:463 if vlan.secondary_rack_id is not None:
@@ -496,7 +489,7 @@
496 hosts = make_hosts_for_subnets(subnets, nodes_dhcp_snippets)489 hosts = make_hosts_for_subnets(subnets, nodes_dhcp_snippets)
497 return (490 return (
498 peer_config, sorted(subnet_configs, key=itemgetter("subnet")),491 peer_config, sorted(subnet_configs, key=itemgetter("subnet")),
499 hosts, interface.name)492 hosts, None if interface is None else interface.name)
500493
501494
502@synchronous495@synchronous
@@ -505,11 +498,11 @@
505 """Return tuple with IPv4 and IPv6 configurations for the498 """Return tuple with IPv4 and IPv6 configurations for the
506 rack controller."""499 rack controller."""
507 # Get list of all vlans that are being managed by the rack controller.500 # Get list of all vlans that are being managed by the rack controller.
508 vlans = get_managed_vlans_for(rack_controller)501 vlans = gen_managed_vlans_for(rack_controller)
509502
510 # Group the subnets on each VLAN into IPv4 and IPv6 subnets.503 # Group the subnets on each VLAN into IPv4 and IPv6 subnets.
511 vlan_subnets = {504 vlan_subnets = {
512 vlan: split_ipv4_ipv6_subnets(vlan.subnet_set.all())505 vlan: split_managed_ipv4_ipv6_subnets(vlan.subnet_set.all())
513 for vlan in vlans506 for vlan in vlans
514 }507 }
515508
@@ -561,52 +554,40 @@
561 for vlan, (subnets_v4, subnets_v6) in vlan_subnets.items():554 for vlan, (subnets_v4, subnets_v6) in vlan_subnets.items():
562 # IPv4555 # IPv4
563 if len(subnets_v4) > 0:556 if len(subnets_v4) > 0:
564 try:557 config = get_dhcp_configure_for(
565 config = get_dhcp_configure_for(558 4, rack_controller, vlan, subnets_v4, ntp_servers,
566 4, rack_controller, vlan, subnets_v4, ntp_servers,559 default_domain, dhcp_snippets)
567 default_domain, dhcp_snippets)560 failover_peer, subnets, hosts, interface = config
568 except DHCPConfigurationError:561 if failover_peer is not None:
569 # XXX bug #1602412: this silently breaks DHCPv4, but we cannot562 failover_peers_v4.append(failover_peer)
570 # allow it to crash here since DHCPv6 might be able to run.563 shared_networks_v4.append({
571 # This error may be irrelevant if there is an IPv4 network in564 "name": "vlan-%d" % vlan.id,
572 # the MAAS model which is not configured on the rack, and the565 "subnets": subnets,
573 # user only wants to serve DHCPv6. But it is still something566 })
574 # worth noting, so log it and continue.567 hosts_v4.extend(hosts)
575 log.err(None, "Failure configuring DHCPv4.")568 if interface is not None:
576 else:
577 failover_peer, subnets, hosts, interface = config
578 if failover_peer is not None:
579 failover_peers_v4.append(failover_peer)
580 shared_networks_v4.append({
581 "name": "vlan-%d" % vlan.id,
582 "subnets": subnets,
583 })
584 hosts_v4.extend(hosts)
585 interfaces_v4.add(interface)569 interfaces_v4.add(interface)
586 # IPv6570 # IPv6
587 if len(subnets_v6) > 0:571 if len(subnets_v6) > 0:
588 try:572 config = get_dhcp_configure_for(
589 config = get_dhcp_configure_for(573 6, rack_controller, vlan, subnets_v6,
590 6, rack_controller, vlan, subnets_v6,574 ntp_servers, default_domain, dhcp_snippets)
591 ntp_servers, default_domain, dhcp_snippets)575 failover_peer, subnets, hosts, interface = config
592 except DHCPConfigurationError:576 if failover_peer is not None:
593 # XXX bug #1602412: this silently breaks DHCPv6, but we cannot577 failover_peers_v6.append(failover_peer)
594 # allow it to crash here since DHCPv4 might be able to run.578 shared_networks_v6.append({
595 # This error may be irrelevant if there is an IPv6 network in579 "name": "vlan-%d" % vlan.id,
596 # the MAAS model which is not configured on the rack, and the580 "subnets": subnets,
597 # user only wants to serve DHCPv4. But it is still something581 })
598 # worth noting, so log it and continue.582 hosts_v6.extend(hosts)
599 log.err(None, "Failure configuring DHCPv6.")583 if interface is not None:
600 else:
601 failover_peer, subnets, hosts, interface = config
602 if failover_peer is not None:
603 failover_peers_v6.append(failover_peer)
604 shared_networks_v6.append({
605 "name": "vlan-%d" % vlan.id,
606 "subnets": subnets,
607 })
608 hosts_v6.extend(hosts)
609 interfaces_v6.add(interface)584 interfaces_v6.add(interface)
585 # When no interfaces exist for each IP version clear the shared networks
586 # as DHCP server cannot be started and needs to be stopped.
587 if len(interfaces_v4) == 0:
588 shared_networks_v4 = {}
589 if len(interfaces_v6) == 0:
590 shared_networks_v6 = {}
610 return DHCPConfigurationForRack(591 return DHCPConfigurationForRack(
611 failover_peers_v4, shared_networks_v4, hosts_v4, interfaces_v4,592 failover_peers_v4, shared_networks_v4, hosts_v4, interfaces_v4,
612 failover_peers_v6, shared_networks_v6, hosts_v6, interfaces_v6,593 failover_peers_v6, shared_networks_v6, hosts_v6, interfaces_v6,
613594
=== renamed directory 'src/maas' => 'src/maasserver/djangosettings'
=== modified file 'src/maasserver/djangosettings/demo.py'
--- src/maas/demo.py 2016-06-07 19:59:49 +0000
+++ src/maasserver/djangosettings/demo.py 2016-12-07 15:50:52 +0000
@@ -5,7 +5,7 @@
55
6from os.path import abspath6from os.path import abspath
77
8from maas import (8from maasserver.djangosettings import (
9 development,9 development,
10 import_settings,10 import_settings,
11 settings,11 settings,
1212
=== modified file 'src/maasserver/djangosettings/development.py'
--- src/maas/development.py 2016-10-18 11:21:26 +0000
+++ src/maasserver/djangosettings/development.py 2016-12-07 15:50:52 +0000
@@ -7,7 +7,7 @@
7from os.path import abspath7from os.path import abspath
88
9from formencode.validators import StringBool9from formencode.validators import StringBool
10from maas import (10from maasserver.djangosettings import (
11 fix_up_databases,11 fix_up_databases,
12 import_settings,12 import_settings,
13 settings,13 settings,
1414
=== modified file 'src/maasserver/djangosettings/settings.py'
--- src/maas/settings.py 2016-11-22 00:53:43 +0000
+++ src/maasserver/djangosettings/settings.py 2016-12-07 15:50:52 +0000
@@ -6,9 +6,9 @@
6import os6import os
77
8import django.template.base8import django.template.base
9from maas import fix_up_databases
10from maas.monkey import patch_get_script_prefix
11from maasserver.config import RegionConfiguration9from maasserver.config import RegionConfiguration
10from maasserver.djangosettings import fix_up_databases
11from maasserver.djangosettings.monkey import patch_get_script_prefix
1212
1313
14def _read_timezone(tzfilename='/etc/timezone'):14def _read_timezone(tzfilename='/etc/timezone'):
@@ -265,7 +265,7 @@
265265
266)266)
267267
268ROOT_URLCONF = 'maas.urls'268ROOT_URLCONF = 'maasserver.djangosettings.urls'
269269
270TEMPLATE_DIRS = (270TEMPLATE_DIRS = (
271 # Put strings here, like "/home/html/django_templates"271 # Put strings here, like "/home/html/django_templates"
272272
=== renamed file 'src/maas/tests/test_maas.py' => 'src/maasserver/djangosettings/tests/test_settings.py'
--- src/maas/tests/test_maas.py 2016-06-21 10:29:11 +0000
+++ src/maasserver/djangosettings/tests/test_settings.py 2016-12-07 15:50:52 +0000
@@ -10,11 +10,11 @@
1010
11from django.conf import settings11from django.conf import settings
12from django.db import connections12from django.db import connections
13from maas import (13from maasserver.djangosettings import (
14 find_settings,14 find_settings,
15 import_settings,15 import_settings,
16)16)
17from maas.settings import (17from maasserver.djangosettings.settings import (
18 _get_local_timezone,18 _get_local_timezone,
19 _read_timezone,19 _read_timezone,
20)20)
2121
=== modified file 'src/maasserver/enum.py'
--- src/maasserver/enum.py 2016-09-08 17:26:54 +0000
+++ src/maasserver/enum.py 2016-12-07 15:50:52 +0000
@@ -154,6 +154,8 @@
154 RACK_CONTROLLER = 2154 RACK_CONTROLLER = 2
155 REGION_CONTROLLER = 3155 REGION_CONTROLLER = 3
156 REGION_AND_RACK_CONTROLLER = 4156 REGION_AND_RACK_CONTROLLER = 4
157 CHASSIS = 5
158 STORAGE = 6
157159
158160
159# This is copied in static/js/angular/controllers/subnet_details.js. If you161# This is copied in static/js/angular/controllers/subnet_details.js. If you
@@ -164,6 +166,8 @@
164 (NODE_TYPE.RACK_CONTROLLER, "Rack controller"),166 (NODE_TYPE.RACK_CONTROLLER, "Rack controller"),
165 (NODE_TYPE.REGION_CONTROLLER, "Region controller"),167 (NODE_TYPE.REGION_CONTROLLER, "Region controller"),
166 (NODE_TYPE.REGION_AND_RACK_CONTROLLER, "Region and rack controller"),168 (NODE_TYPE.REGION_AND_RACK_CONTROLLER, "Region and rack controller"),
169 (NODE_TYPE.CHASSIS, "Chassis"),
170 (NODE_TYPE.STORAGE, "Storage"),
167)171)
168172
169173
170174
=== modified file 'src/maasserver/exceptions.py'
--- src/maasserver/exceptions.py 2016-03-28 13:54:47 +0000
+++ src/maasserver/exceptions.py 2016-12-07 15:50:52 +0000
@@ -199,7 +199,3 @@
199 information.199 information.
200 """200 """
201 api_error = int(http.client.SERVICE_UNAVAILABLE)201 api_error = int(http.client.SERVICE_UNAVAILABLE)
202
203
204class DHCPConfigurationError(MAASException):
205 """Raised when the configuration of DHCP hits a problem."""
206202
=== modified file 'src/maasserver/forms_commission.py'
--- src/maasserver/forms_commission.py 2015-12-01 18:12:59 +0000
+++ src/maasserver/forms_commission.py 2016-12-07 15:50:52 +0000
@@ -9,7 +9,6 @@
99
10from django import forms10from django import forms
11from django.core.exceptions import ValidationError11from django.core.exceptions import ValidationError
12from maasserver.enum import POWER_STATE
13from maasserver.node_action import compile_node_actions12from maasserver.node_action import compile_node_actions
1413
1514
@@ -36,10 +35,6 @@
36 raise ValidationError(35 raise ValidationError(
37 "Commission is not available because of the current state "36 "Commission is not available because of the current state "
38 "of the node.")37 "of the node.")
39 if self.instance.power_state == POWER_STATE.ON:
40 raise ValidationError(
41 "Commission is not available because of the node is currently "
42 "powered on.")
43 return cleaned_data38 return cleaned_data
4439
45 def save(self):40 def save(self):
4641
=== modified file 'src/maasserver/forms_subnet.py'
--- src/maasserver/forms_subnet.py 2016-09-23 01:32:02 +0000
+++ src/maasserver/forms_subnet.py 2016-12-07 15:50:52 +0000
@@ -43,6 +43,9 @@
43 allow_proxy = forms.BooleanField(43 allow_proxy = forms.BooleanField(
44 required=False)44 required=False)
4545
46 managed = forms.BooleanField(
47 required=False)
48
46 class Meta:49 class Meta:
47 model = Subnet50 model = Subnet
48 fields = (51 fields = (
@@ -56,6 +59,7 @@
56 'rdns_mode',59 'rdns_mode',
57 'active_discovery',60 'active_discovery',
58 'allow_proxy',61 'allow_proxy',
62 'managed',
59 )63 )
6064
61 def __init__(self, *args, **kwargs):65 def __init__(self, *args, **kwargs):
@@ -64,9 +68,12 @@
6468
65 def clean(self):69 def clean(self):
66 cleaned_data = super(SubnetForm, self).clean()70 cleaned_data = super(SubnetForm, self).clean()
67 # The default value for allow_proxy is True.71 # The default value for 'allow_proxy' is True.
68 if 'allow_proxy' not in self.data:72 if 'allow_proxy' not in self.data:
69 cleaned_data['allow_proxy'] = True73 cleaned_data['allow_proxy'] = True
74 # The default value for 'managed' is True.
75 if 'managed' not in self.data:
76 cleaned_data['managed'] = True
70 # The ArrayField form has a bug which leaves out the first entry.77 # The ArrayField form has a bug which leaves out the first entry.
71 if 'dns_servers' in self.data and self.data['dns_servers'] != '':78 if 'dns_servers' in self.data and self.data['dns_servers'] != '':
72 cleaned_data['dns_servers'] = self.data.getlist('dns_servers')79 cleaned_data['dns_servers'] = self.data.getlist('dns_servers')
7380
=== modified file 'src/maasserver/forms_vlan.py'
--- src/maasserver/forms_vlan.py 2016-04-27 20:38:06 +0000
+++ src/maasserver/forms_vlan.py 2016-12-07 15:50:52 +0000
@@ -31,6 +31,7 @@
31 'dhcp_on',31 'dhcp_on',
32 'primary_rack',32 'primary_rack',
33 'secondary_rack',33 'secondary_rack',
34 'relay_vlan',
34 )35 )
3536
36 def __init__(self, *args, **kwargs):37 def __init__(self, *args, **kwargs):
@@ -40,6 +41,7 @@
40 if instance is None and self.fabric is None:41 if instance is None and self.fabric is None:
41 raise ValueError("Form requires either a instance or a fabric.")42 raise ValueError("Form requires either a instance or a fabric.")
42 self._set_up_rack_fields()43 self._set_up_rack_fields()
44 self._set_up_relay_vlan()
4345
44 def _set_up_rack_fields(self):46 def _set_up_rack_fields(self):
45 qs = RackController.objects.filter_by_vids([self.instance.vid])47 qs = RackController.objects.filter_by_vids([self.instance.vid])
@@ -61,6 +63,22 @@
61 secondary_rack = RackController.objects.get(id=secondary_rack_id)63 secondary_rack = RackController.objects.get(id=secondary_rack_id)
62 self.initial['secondary_rack'] = secondary_rack.system_id64 self.initial['secondary_rack'] = secondary_rack.system_id
6365
66 def _set_up_relay_vlan(self):
67 # Configure the relay_vlan fields to include only VLAN's that are
68 # not already on a relay_vlan. If this is an update then it cannot
69 # be itself or never set when dhcp_on is True.
70 possible_relay_vlans = VLAN.objects.filter(relay_vlan__isnull=True)
71 if self.instance is not None:
72 possible_relay_vlans = possible_relay_vlans.exclude(
73 id=self.instance.id)
74 if self.instance.dhcp_on:
75 possible_relay_vlans = VLAN.objects.none()
76 if self.instance.relay_vlan is not None:
77 possible_relay_vlans = VLAN.objects.filter(
78 id=self.instance.relay_vlan.id)
79 self.fields['relay_vlan'] = forms.ModelChoiceField(
80 queryset=possible_relay_vlans, required=False)
81
64 def clean(self):82 def clean(self):
65 cleaned_data = super(VLANForm, self).clean()83 cleaned_data = super(VLANForm, self).clean()
66 # Automatically promote the secondary rack controller to the primary84 # Automatically promote the secondary rack controller to the primary
@@ -120,5 +138,12 @@
120 interface = super(VLANForm, self).save(commit=False)138 interface = super(VLANForm, self).save(commit=False)
121 if self.fabric is not None:139 if self.fabric is not None:
122 interface.fabric = self.fabric140 interface.fabric = self.fabric
141 if ('relay_vlan' in self.data and
142 not self.cleaned_data.get('relay_vlan')):
143 # relay_vlan is being cleared.
144 interface.relay_vlan = None
145 if interface.dhcp_on:
146 # relay_vlan cannot be set when dhcp is on.
147 interface.relay_vlan = None
123 interface.save()148 interface.save()
124 return interface149 return interface
125150
=== modified file 'src/maasserver/locks.py'
--- src/maasserver/locks.py 2016-09-28 14:12:23 +0000
+++ src/maasserver/locks.py 2016-12-07 15:50:52 +0000
@@ -4,6 +4,7 @@
4"""Region-wide locks."""4"""Region-wide locks."""
55
6__all__ = [6__all__ = [
7 "address_allocation",
7 "dns",8 "dns",
8 "eventloop",9 "eventloop",
9 "import_images",10 "import_images",
@@ -11,7 +12,6 @@
11 "rack_registration",12 "rack_registration",
12 "security",13 "security",
13 "startup",14 "startup",
14 "staticip_acquire",
15]15]
1616
17from maasserver.utils.dblocks import (17from maasserver.utils.dblocks import (
@@ -38,8 +38,8 @@
38# Lock to prevent concurrent acquisition of nodes.38# Lock to prevent concurrent acquisition of nodes.
39node_acquire = DatabaseXactLock(7)39node_acquire = DatabaseXactLock(7)
4040
41# Lock to prevent concurrent allocation of StaticIPAddress41# Lock to help with concurrent allocation of IP addresses.
42staticip_acquire = DatabaseXactLock(8)42address_allocation = DatabaseLock(8)
4343
44# Lock to prevent concurrent registration of rack controllers. This can be a44# Lock to prevent concurrent registration of rack controllers. This can be a
45# problem because registration involves populating fabrics, VLANs, and other45# problem because registration involves populating fabrics, VLANs, and other
4646
=== modified file 'src/maasserver/management/commands/dbupgrade.py'
--- src/maasserver/management/commands/dbupgrade.py 2016-09-04 19:57:50 +0000
+++ src/maasserver/management/commands/dbupgrade.py 2016-12-07 15:50:52 +0000
@@ -9,6 +9,7 @@
9__all__ = []9__all__ = []
1010
11from importlib import import_module11from importlib import import_module
12import json
12import optparse13import optparse
13import os14import os
14import shutil15import shutil
@@ -34,7 +35,7 @@
34# Script that performs the south migrations for MAAS under django 1.6 and35# Script that performs the south migrations for MAAS under django 1.6 and
35# python2.7.36# python2.7.
36MAAS_UPGRADE_SCRIPT = """\37MAAS_UPGRADE_SCRIPT = """\
37# Copyright 2015 Canonical Ltd. This software is licensed under the38# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
38# GNU Affero General Public License version 3 (see the file LICENSE).39# GNU Affero General Public License version 3 (see the file LICENSE).
3940
40from __future__ import (41from __future__ import (
@@ -46,12 +47,49 @@
46str = None47str = None
4748
48__metaclass__ = type49__metaclass__ = type
49__all__ = [50__all__ = []
50 ]
5151
52import os52import os
53import sys53import sys
5454
55import django.conf
56
57
58class LazySettings(django.conf.LazySettings):
59 '''Prevent Django from mangling warnings settings.
60
61 At present, Django adds a single filter that surfaces all deprecation
62 warnings, but MAAS handles them differently. Django doesn't appear to give
63 a way to prevent it from doing its thing, so we must undo its changes.
64
65 Deprecation warnings in production environments are not desirable as they
66 are a developer tool, and not something an end user can reasonably do
67 something about. This brings control of warnings back into MAAS's control.
68 '''
69
70 def _configure_logging(self):
71 # This is a copy of *half* of Django's `_configure_logging`, omitting
72 # the problematic bits.
73 if self.LOGGING_CONFIG:
74 from django.utils.log import DEFAULT_LOGGING
75 from django.utils.module_loading import import_by_path
76 # First find the logging configuration function ...
77 logging_config_func = import_by_path(self.LOGGING_CONFIG)
78 logging_config_func(DEFAULT_LOGGING)
79 # ... then invoke it with the logging settings
80 if self.LOGGING:
81 logging_config_func(self.LOGGING)
82
83
84# Install our `LazySettings` as the Django-global settings class. First,
85# ensure that Django hasn't yet loaded its settings.
86assert not django.conf.settings.configured
87# This is needed because Django's `LazySettings` overrides `__setattr__`.
88object.__setattr__(django.conf.settings, "__class__", LazySettings)
89
90# Force Django configuration.
91os.environ["DJANGO_SETTINGS_MODULE"] = "maas19settings"
92
55# Inject the sys.path from the parent process so that the python path is93# Inject the sys.path from the parent process so that the python path is
56# is similar, except that the directory that this script is running from is94# is similar, except that the directory that this script is running from is
57# already the first path in sys.path.95# already the first path in sys.path.
@@ -132,6 +170,11 @@
132 tempdir = tempfile.mkdtemp(prefix='maas-upgrade-')170 tempdir = tempfile.mkdtemp(prefix='maas-upgrade-')
133 subprocess.check_call([171 subprocess.check_call([
134 "tar", "zxf", path_to_tarball, "-C", tempdir])172 "tar", "zxf", path_to_tarball, "-C", tempdir])
173
174 settings_json = os.path.join(tempdir, "maas19settings.json")
175 with open(settings_json, "w", encoding="utf-8") as fd:
176 fd.write(json.dumps({"DATABASES": settings.DATABASES}))
177
135 script_path = os.path.join(tempdir, "migrate.py")178 script_path = os.path.join(tempdir, "migrate.py")
136 with open(script_path, "wb") as fp:179 with open(script_path, "wb") as fp:
137 fp.write(MAAS_UPGRADE_SCRIPT.encode("utf-8"))180 fp.write(MAAS_UPGRADE_SCRIPT.encode("utf-8"))
138181
=== modified file 'src/maasserver/management/commands/tests/test_dbupgrade.py'
--- src/maasserver/management/commands/tests/test_dbupgrade.py 2016-03-28 13:54:47 +0000
+++ src/maasserver/management/commands/tests/test_dbupgrade.py 2016-12-07 15:50:52 +0000
@@ -64,7 +64,10 @@
64 env = os.environ.copy()64 env = os.environ.copy()
65 env["MAAS_PREVENT_MIGRATIONS"] = "0"65 env["MAAS_PREVENT_MIGRATIONS"] = "0"
66 mra = os.path.join(root, "bin", "maas-region")66 mra = os.path.join(root, "bin", "maas-region")
67 cmd = [mra, "dbupgrade", "--settings", "maas.settings"]67 cmd = [
68 mra, "dbupgrade", "--settings",
69 "maasserver.djangosettings.settings",
70 ]
68 if always_south:71 if always_south:
69 cmd.append("--always-south")72 cmd.append("--always-south")
70 self.execute(cmd, env=env)73 self.execute(cmd, env=env)
7174
=== modified file 'src/maasserver/migrations/builtin/maasserver/0016_migrate_power_data_node_to_bmc.py'
--- src/maasserver/migrations/builtin/maasserver/0016_migrate_power_data_node_to_bmc.py 2016-05-11 19:01:48 +0000
+++ src/maasserver/migrations/builtin/maasserver/0016_migrate_power_data_node_to_bmc.py 2016-12-07 15:50:52 +0000
@@ -2,10 +2,8 @@
22
3from django.db import migrations3from django.db import migrations
4from maasserver.models import timestampedmodel4from maasserver.models import timestampedmodel
5from provisioningserver.power.schema import (5from provisioningserver.drivers import SETTING_SCOPE
6 POWER_FIELDS_BY_TYPE,6from provisioningserver.drivers.power import PowerDriverRegistry
7 POWER_PARAMETER_SCOPE,
8)
97
10# Copied from BMC model.8# Copied from BMC model.
11def scope_power_parameters(power_type, power_params):9def scope_power_parameters(power_type, power_params):
@@ -14,16 +12,20 @@
14 if not power_type:12 if not power_type:
15 # If there is no power type, treat all params as node params.13 # If there is no power type, treat all params as node params.
16 return ({}, power_params)14 return ({}, power_params)
17 power_fields = POWER_FIELDS_BY_TYPE.get(power_type)15 power_driver = PowerDriverRegistry.get_item(power_type)
16 if power_driver is None:
17 # If there is no power driver, treat all params as node params.
18 return ({}, power_params)
19 power_fields = power_driver.settings
18 if not power_fields:20 if not power_fields:
19 # If there is no parameter info, treat all params as node params.21 # If there is no parameter info, treat all params as node params.
20 return ({}, power_params)22 return ({}, power_params)
21 bmc_params = {}23 bmc_params = {}
22 node_params = {}24 node_params = {}
23 for param_name in power_params:25 for param_name in power_params:
24 power_field = power_fields.get(param_name)26 power_field = power_driver.get_setting(param_name)
25 if power_field and power_field.get(27 if power_field and power_field.get(
26 'scope') == POWER_PARAMETER_SCOPE.BMC:28 'scope') == SETTING_SCOPE.BMC:
27 bmc_params[param_name] = power_params[param_name]29 bmc_params[param_name] = power_params[param_name]
28 else:30 else:
29 node_params[param_name] = power_params[param_name]31 node_params[param_name] = power_params[param_name]
3032
=== modified file 'src/maasserver/migrations/builtin/maasserver/0022_extract_ip_for_bmcs.py'
--- src/maasserver/migrations/builtin/maasserver/0022_extract_ip_for_bmcs.py 2016-05-11 19:01:48 +0000
+++ src/maasserver/migrations/builtin/maasserver/0022_extract_ip_for_bmcs.py 2016-12-07 15:50:52 +0000
@@ -8,7 +8,7 @@
8)8)
9from maasserver.enum import IPADDRESS_TYPE9from maasserver.enum import IPADDRESS_TYPE
10from maasserver.models import timestampedmodel10from maasserver.models import timestampedmodel
11from provisioningserver.power.schema import POWER_TYPE_PARAMETERS_BY_NAME11from provisioningserver.drivers.power import PowerDriverRegistry
1212
13# Derived from Subnet model.13# Derived from Subnet model.
14def raw_subnet_id_containing_ip(ip):14def raw_subnet_id_containing_ip(ip):
@@ -38,10 +38,13 @@
38 # power_address field, returns None.38 # power_address field, returns None.
39 if not power_type or not power_parameters:39 if not power_type or not power_parameters:
40 return None40 return None
41 power_type_parameters = POWER_TYPE_PARAMETERS_BY_NAME.get(power_type)41 power_driver = PowerDriverRegistry.get_item(power_type)
42 if power_driver is None:
43 return None
44 power_type_parameters = power_driver.settings
42 if not power_type_parameters:45 if not power_type_parameters:
43 return None46 return None
44 ip_extractor = power_type_parameters.get('ip_extractor')47 ip_extractor = power_driver.ip_extractor
45 if not ip_extractor:48 if not ip_extractor:
46 return None49 return None
47 field_value = power_parameters.get(ip_extractor.get('field_name'))50 field_value = power_parameters.get(ip_extractor.get('field_name'))
4851
=== modified file 'src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py'
--- src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py 2016-05-11 19:01:48 +0000
+++ src/maasserver/migrations/builtin/maasserver/0027_replace_static_range_with_admin_reserved_ranges.py 2016-12-07 15:50:52 +0000
@@ -37,7 +37,7 @@
37 IPRange, subnet, ranges, created_time, range_description):37 IPRange, subnet, ranges, created_time, range_description):
38 unreserved_range_set = MAASIPSet(ranges)38 unreserved_range_set = MAASIPSet(ranges)
39 unreserved_ranges = unreserved_range_set.get_unused_ranges(39 unreserved_ranges = unreserved_range_set.get_unused_ranges(
40 subnet.cidr, comment="reserved")40 subnet.cidr, purpose="reserved")
41 for iprange in unreserved_ranges:41 for iprange in unreserved_ranges:
42 start_ip = str(IPAddress(iprange.first))42 start_ip = str(IPAddress(iprange.first))
43 end_ip = str(IPAddress(iprange.last))43 end_ip = str(IPAddress(iprange.last))
4444
=== modified file 'src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py'
--- src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-07-30 01:17:54 +0000
+++ src/maasserver/migrations/builtin/maasserver/0056_add_description_to_fabric_and_space.py 2016-12-07 15:50:52 +0000
@@ -44,6 +44,6 @@
44 migrations.AlterField(44 migrations.AlterField(
45 model_name='subnet',45 model_name='subnet',
46 name='vlan',46 name='vlan',
47 field=models.ForeignKey(to='maasserver.VLAN', default=maasserver.models.subnet.get_default_vlan, on_delete=django.db.models.deletion.PROTECT),47 field=models.ForeignKey(to='maasserver.VLAN', default=None, on_delete=django.db.models.deletion.PROTECT),
48 ),48 ),
49 ]49 ]
5050
=== added file 'src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py'
--- src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/builtin/maasserver/0094_add_unmanaged_subnets.py 2016-12-07 15:50:52 +0000
@@ -0,0 +1,22 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import (
5 migrations,
6 models,
7)
8
9
10class Migration(migrations.Migration):
11
12 dependencies = [
13 ('maasserver', '0093_add_rdns_model'),
14 ]
15
16 operations = [
17 migrations.AddField(
18 model_name='subnet',
19 name='managed',
20 field=models.BooleanField(default=True),
21 ),
22 ]
023
=== added file 'src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py'
--- src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/builtin/maasserver/0095_vlan_relay_vlan.py 2016-12-07 15:50:52 +0000
@@ -0,0 +1,23 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import (
5 migrations,
6 models,
7)
8import django.db.models.deletion
9
10
11class Migration(migrations.Migration):
12
13 dependencies = [
14 ('maasserver', '0094_add_unmanaged_subnets'),
15 ]
16
17 operations = [
18 migrations.AddField(
19 model_name='vlan',
20 name='relay_vlan',
21 field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, null=True, blank=True, related_name='relay_vlans', to='maasserver.VLAN'),
22 ),
23 ]
024
=== added file 'src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py'
--- src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/builtin/maasserver/0096_set_default_vlan_field.py 2016-12-07 15:50:52 +0000
@@ -0,0 +1,24 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import (
5 migrations,
6 models,
7)
8import django.db.models.deletion
9import maasserver.models.subnet
10
11
12class Migration(migrations.Migration):
13
14 dependencies = [
15 ('maasserver', '0095_vlan_relay_vlan'),
16 ]
17
18 operations = [
19 migrations.AlterField(
20 model_name='subnet',
21 name='vlan',
22 field=models.ForeignKey(to='maasserver.VLAN', default=maasserver.models.subnet.get_default_vlan, on_delete=django.db.models.deletion.PROTECT),
23 ),
24 ]
025
=== added file 'src/maasserver/migrations/builtin/maasserver/0097_node_chassis_storage_hints.py'
--- src/maasserver/migrations/builtin/maasserver/0097_node_chassis_storage_hints.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/migrations/builtin/maasserver/0097_node_chassis_storage_hints.py 2016-12-07 15:50:52 +0000
@@ -0,0 +1,73 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import (
5 migrations,
6 models,
7)
8import django.db.models.deletion
9import maasserver.models.cleansave
10import maasserver.models.node
11
12
13class Migration(migrations.Migration):
14
15 dependencies = [
16 ('maasserver', '0096_set_default_vlan_field'),
17 ]
18
19 operations = [
20 migrations.CreateModel(
21 name='ChassisHints',
22 fields=[
23 ('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
24 ('cores', models.IntegerField(default=0)),
25 ('memory', models.IntegerField(default=0)),
26 ('local_storage', models.IntegerField(default=0)),
27 ],
28 bases=(maasserver.models.cleansave.CleanSave, models.Model),
29 ),
30 migrations.CreateModel(
31 name='Chassis',
32 fields=[
33 ],
34 options={
35 'proxy': True,
36 },
37 bases=('maasserver.node',),
38 ),
39 migrations.CreateModel(
40 name='Storage',
41 fields=[
42 ],
43 options={
44 'proxy': True,
45 },
46 bases=('maasserver.node',),
47 ),
48 migrations.AddField(
49 model_name='node',
50 name='cpu_speed',
51 field=models.IntegerField(default=0),
52 ),
53 migrations.AddField(
54 model_name='node',
55 name='dynamic',
56 field=models.BooleanField(default=False),
57 ),
58 migrations.AlterField(
59 model_name='node',
60 name='domain',
61 field=models.ForeignKey(to='maasserver.Domain', null=True, default=maasserver.models.node.get_default_domain, blank=True, on_delete=django.db.models.deletion.PROTECT),
62 ),
63 migrations.AlterField(
64 model_name='node',
65 name='node_type',
66 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),
67 ),
68 migrations.AddField(
69 model_name='chassishints',
70 name='chassis',
71 field=models.OneToOneField(to='maasserver.Node', related_name='chassis_hints'),
72 ),
73 ]
074
=== renamed file 'src/maasserver/migrations/south/django16_south_maas19.tar.gz' => 'src/maasserver/migrations/south/django16_south_maas19.tar.gz.OTHER'
1Binary 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 differ75Binary 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
=== modified file 'src/maasserver/models/__init__.py'
--- src/maasserver/models/__init__.py 2016-10-18 22:22:23 +0000
+++ src/maasserver/models/__init__.py 2016-12-07 15:50:52 +0000
@@ -16,6 +16,8 @@
16 'BootSourceSelection',16 'BootSourceSelection',
17 'BridgeInterface',17 'BridgeInterface',
18 'CacheSet',18 'CacheSet',
19 'Chassis',
20 'ChassisHints',
19 'ComponentError',21 'ComponentError',
20 'Config',22 'Config',
21 'Controller',23 'Controller',
@@ -59,6 +61,7 @@
59 'RegionRackRPCConnection',61 'RegionRackRPCConnection',
60 'Service',62 'Service',
61 'Space',63 'Space',
64 'Storage',
62 'SSHKey',65 'SSHKey',
63 'SSLKey',66 'SSLKey',
64 'StaticIPAddress',67 'StaticIPAddress',
@@ -98,6 +101,7 @@
98from maasserver.models.bootsourcecache import BootSourceCache101from maasserver.models.bootsourcecache import BootSourceCache
99from maasserver.models.bootsourceselection import BootSourceSelection102from maasserver.models.bootsourceselection import BootSourceSelection
100from maasserver.models.cacheset import CacheSet103from maasserver.models.cacheset import CacheSet
104from maasserver.models.chassishints import ChassisHints
101from maasserver.models.component_error import ComponentError105from maasserver.models.component_error import ComponentError
102from maasserver.models.config import Config106from maasserver.models.config import Config
103from maasserver.models.dhcpsnippet import DHCPSnippet107from maasserver.models.dhcpsnippet import DHCPSnippet
@@ -133,6 +137,7 @@
133from maasserver.models.mdns import MDNS137from maasserver.models.mdns import MDNS
134from maasserver.models.neighbour import Neighbour138from maasserver.models.neighbour import Neighbour
135from maasserver.models.node import (139from maasserver.models.node import (
140 Chassis,
136 Controller,141 Controller,
137 Device,142 Device,
138 Machine,143 Machine,
@@ -140,6 +145,7 @@
140 NodeGroupToRackController,145 NodeGroupToRackController,
141 RackController,146 RackController,
142 RegionController,147 RegionController,
148 Storage,
143)149)
144from maasserver.models.ownerdata import OwnerData150from maasserver.models.ownerdata import OwnerData
145from maasserver.models.packagerepository import PackageRepository151from maasserver.models.packagerepository import PackageRepository
146152
=== modified file 'src/maasserver/models/bmc.py'
--- src/maasserver/models/bmc.py 2016-06-04 00:21:58 +0000
+++ src/maasserver/models/bmc.py 2016-12-07 15:50:52 +0000
@@ -25,12 +25,9 @@
25from maasserver.models.subnet import Subnet25from maasserver.models.subnet import Subnet
26from maasserver.models.timestampedmodel import TimestampedModel26from maasserver.models.timestampedmodel import TimestampedModel
27from maasserver.rpc import getAllClients27from maasserver.rpc import getAllClients
28from provisioningserver.drivers import SETTING_SCOPE
29from provisioningserver.drivers.power import PowerDriverRegistry
28from provisioningserver.logger import get_maas_logger30from provisioningserver.logger import get_maas_logger
29from provisioningserver.power.schema import (
30 POWER_FIELDS_BY_TYPE,
31 POWER_PARAMETER_SCOPE,
32 POWER_TYPE_PARAMETERS_BY_NAME,
33)
3431
3532
36maaslog = get_maas_logger("node")33maaslog = get_maas_logger("node")
@@ -125,16 +122,20 @@
125 if not power_type:122 if not power_type:
126 # If there is no power type, treat all params as node params.123 # If there is no power type, treat all params as node params.
127 return ({}, power_params)124 return ({}, power_params)
128 power_fields = POWER_FIELDS_BY_TYPE.get(power_type)125 power_driver = PowerDriverRegistry.get_item(power_type)
126 if power_driver is None:
127 # If there is no power driver, treat all params as node params.
128 return ({}, power_params)
129 power_fields = power_driver.settings
129 if not power_fields:130 if not power_fields:
130 # If there is no parameter info, treat all params as node params.131 # If there is no parameter info, treat all params as node params.
131 return ({}, power_params)132 return ({}, power_params)
132 bmc_params = {}133 bmc_params = {}
133 node_params = {}134 node_params = {}
134 for param_name in power_params:135 for param_name in power_params:
135 power_field = power_fields.get(param_name)136 power_field = power_driver.get_setting(param_name)
136 if (power_field and137 if (power_field and
137 power_field.get('scope') == POWER_PARAMETER_SCOPE.BMC):138 power_field.get('scope') == SETTING_SCOPE.BMC):
138 bmc_params[param_name] = power_params[param_name]139 bmc_params[param_name] = power_params[param_name]
139 else:140 else:
140 node_params[param_name] = power_params[param_name]141 node_params[param_name] = power_params[param_name]
@@ -148,12 +149,17 @@
148 if not power_type or not power_parameters:149 if not power_type or not power_parameters:
149 # Nothing to extract.150 # Nothing to extract.
150 return None151 return None
151 power_type_parameters = POWER_TYPE_PARAMETERS_BY_NAME.get(power_type)152 power_driver = PowerDriverRegistry.get_item(power_type)
153 if power_driver is None:
154 maaslog.warning(
155 "No power driver for power type %s" % power_type)
156 return None
157 power_type_parameters = power_driver.settings
152 if not power_type_parameters:158 if not power_type_parameters:
153 maaslog.warning(159 maaslog.warning(
154 "No POWER_TYPE_PARAMETERS for power type %s" % power_type)160 "No power driver settings for power type %s" % power_type)
155 return None161 return None
156 ip_extractor = power_type_parameters.get('ip_extractor')162 ip_extractor = power_driver.ip_extractor
157 if not ip_extractor:163 if not ip_extractor:
158 maaslog.info(164 maaslog.info(
159 "No IP extractor configured for power type %s. "165 "No IP extractor configured for power type %s. "
160166
=== added file 'src/maasserver/models/chassishints.py'
--- src/maasserver/models/chassishints.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/models/chassishints.py 2016-12-07 15:50:52 +0000
@@ -0,0 +1,33 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Model that holds hint information for a Chassis."""
5
6__all__ = [
7 'ChassisHints',
8 ]
9
10
11from django.db.models import (
12 IntegerField,
13 Model,
14 OneToOneField,
15)
16from maasserver import DefaultMeta
17from maasserver.models.cleansave import CleanSave
18from maasserver.models.node import Node
19
20
21class ChassisHints(CleanSave, Model):
22 """Hint information for a chassis."""
23
24 class Meta(DefaultMeta):
25 """Needed for South to recognize this model."""
26
27 chassis = OneToOneField(Node, related_name="chassis_hints")
28
29 cores = IntegerField(default=0)
30
31 memory = IntegerField(default=0)
32
33 local_storage = IntegerField(default=0)
034
=== modified file 'src/maasserver/models/event.py'
--- src/maasserver/models/event.py 2016-10-25 13:57:02 +0000
+++ src/maasserver/models/event.py 2016-12-07 15:50:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2014-2015 Canonical Ltd. This software is licensed under the1# Copyright 2014-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4""":class:`Event` and friends."""4""":class:`Event` and friends."""
@@ -21,6 +21,7 @@
21from maasserver.models.timestampedmodel import TimestampedModel21from maasserver.models.timestampedmodel import TimestampedModel
22from provisioningserver.events import EVENT_DETAILS22from provisioningserver.events import EVENT_DETAILS
23from provisioningserver.logger import get_maas_logger23from provisioningserver.logger import get_maas_logger
24from provisioningserver.utils.env import get_maas_id
2425
2526
26maaslog = get_maas_logger('models.event')27maaslog = get_maas_logger('models.event')
@@ -56,6 +57,12 @@
56 event_action=event_action,57 event_action=event_action,
57 event_description=event_description)58 event_description=event_description)
5859
60 def create_region_event(self, event_type, event_description=''):
61 """Helper to register event and event type for the running region."""
62 self.create_node_event(
63 system_id=get_maas_id(), event_type=event_type,
64 event_description=event_description)
65
5966
60class Event(CleanSave, TimestampedModel):67class Event(CleanSave, TimestampedModel):
61 """An `Event` represents a MAAS event.68 """An `Event` represents a MAAS event.
6269
=== modified file 'src/maasserver/models/node.py'
--- src/maasserver/models/node.py 2016-12-07 11:26:49 +0000
+++ src/maasserver/models/node.py 2016-12-07 15:50:52 +0000
@@ -158,12 +158,12 @@
158)158)
159import petname159import petname
160from piston3.models import Token160from piston3.models import Token
161from provisioningserver.drivers.power import PowerDriverRegistry
161from provisioningserver.events import (162from provisioningserver.events import (
162 EVENT_DETAILS,163 EVENT_DETAILS,
163 EVENT_TYPES,164 EVENT_TYPES,
164)165)
165from provisioningserver.logger import get_maas_logger166from provisioningserver.logger import get_maas_logger
166from provisioningserver.power import QUERY_POWER_TYPES
167from provisioningserver.refresh import (167from provisioningserver.refresh import (
168 get_sys_info,168 get_sys_info,
169 refresh,169 refresh,
@@ -265,32 +265,6 @@
265 "we could find no unused node identifiers." % attempt)265 "we could find no unused node identifiers." % attempt)
266266
267267
268def typecast_node(node, model):
269 """Typecast a node object into a node type object."""
270 assert(isinstance(node, Node))
271 assert(issubclass(model, Node))
272 node.__class__ = model
273 return node
274
275
276def typecast_to_node_type(node):
277 """Typecast a node object to what the node_type is set to."""
278 if node.node_type == NODE_TYPE.MACHINE:
279 return typecast_node(node, Machine)
280 elif node.node_type in (
281 NODE_TYPE.RACK_CONTROLLER,
282 NODE_TYPE.REGION_AND_RACK_CONTROLLER):
283 # XXX ltrager 18-02-2016 - Currently only rack controllers have
284 # unique functionality so when combined return a rack controller
285 return typecast_node(node, RackController)
286 elif node.node_type == NODE_TYPE.REGION_CONTROLLER:
287 return typecast_node(node, RegionController)
288 elif node.node_type == NODE_TYPE.DEVICE:
289 return typecast_node(node, Device)
290 else:
291 raise NotImplementedError("Unknown node type %d" % node.node_type)
292
293
294class NodeQueriesMixin(MAASQueriesMixin):268class NodeQueriesMixin(MAASQueriesMixin):
295269
296 def filter_by_spaces(self, spaces):270 def filter_by_spaces(self, spaces):
@@ -520,7 +494,7 @@
520 node = get_object_or_404(494 node = get_object_or_404(
521 self.model, system_id=system_id, **kwargs)495 self.model, system_id=system_id, **kwargs)
522 if user.has_perm(perm, node):496 if user.has_perm(perm, node):
523 return typecast_to_node_type(node)497 return node.as_self()
524 else:498 else:
525 raise PermissionDenied()499 raise PermissionDenied()
526500
@@ -573,6 +547,19 @@
573 extra_filters = {'node_type': NODE_TYPE.DEVICE}547 extra_filters = {'node_type': NODE_TYPE.DEVICE}
574548
575549
550class ChassisManager(BaseNodeManager):
551 """Chassis are nodes that contain or can compose more machines or
552 storage."""
553
554 extra_filters = {'node_type': NODE_TYPE.CHASSIS}
555
556
557class StorageManager(BaseNodeManager):
558 """Storage are nodes that provide storage to other machines."""
559
560 extra_filters = {'node_type': NODE_TYPE.STORAGE}
561
562
576class ControllerManager(BaseNodeManager):563class ControllerManager(BaseNodeManager):
577 """All controllers `RackController`, `RegionController`, and564 """All controllers `RackController`, `RegionController`, and
578 `RegionRackController`."""565 `RegionRackController`."""
@@ -746,7 +733,8 @@
746 update_fields.append("owner")733 update_fields.append("owner")
747 if len(update_fields) > 0:734 if len(update_fields) > 0:
748 node.save(update_fields=update_fields)735 node.save(update_fields=update_fields)
749 return typecast_node(node, self.model)736 # Always cast to a region controller.
737 return node.as_region_controller()
750738
751 def _create_running_controller(self):739 def _create_running_controller(self):
752 """Create a region controller for the host machine.740 """Create a region controller for the host machine.
@@ -844,7 +832,7 @@
844 # What Domain do we use for this host unless the individual StaticIPAddress832 # What Domain do we use for this host unless the individual StaticIPAddress
845 # record overrides it?833 # record overrides it?
846 domain = ForeignKey(834 domain = ForeignKey(
847 Domain, default=get_default_domain, null=False,835 Domain, default=get_default_domain, null=True, blank=True,
848 editable=True, on_delete=PROTECT)836 editable=True, on_delete=PROTECT)
849837
850 # TTL for this Node's IP addresses. Since this must be the same for all838 # TTL for this Node's IP addresses. Since this must be the same for all
@@ -904,6 +892,7 @@
904 # Juju expects the following standard constraints, which are stored here892 # Juju expects the following standard constraints, which are stored here
905 # as a basic optimisation over querying the lshw output.893 # as a basic optimisation over querying the lshw output.
906 cpu_count = IntegerField(default=0)894 cpu_count = IntegerField(default=0)
895 cpu_speed = IntegerField(default=0) # MHz
907 memory = IntegerField(default=0)896 memory = IntegerField(default=0)
908897
909 swap_size = BigIntegerField(null=True, blank=True, default=None)898 swap_size = BigIntegerField(null=True, blank=True, default=None)
@@ -945,6 +934,11 @@
945934
946 license_key = CharField(max_length=30, null=True, blank=True)935 license_key = CharField(max_length=30, null=True, blank=True)
947936
937 # Only used by Machine. Set to True when the machine was composed
938 # dynamically from a Chassis during allocation. When the machine is
939 # released it will be deleted.
940 dynamic = BooleanField(default=False)
941
948 tags = ManyToManyField(Tag)942 tags = ManyToManyField(Tag)
949943
950 # Record the Interface the node last booted from.944 # Record the Interface the node last booted from.
@@ -1120,7 +1114,10 @@
11201114
1121 Return the FQDN for this host.1115 Return the FQDN for this host.
1122 """1116 """
1123 return '%s.%s' % (self.hostname, self.domain.name)1117 if self.domain is not None:
1118 return '%s.%s' % (self.hostname, self.domain.name)
1119 else:
1120 return self.hostname
11241121
1125 def get_deployment_time(self):1122 def get_deployment_time(self):
1126 """Return the deployment time of this node (in seconds).1123 """Return the deployment time of this node (in seconds).
@@ -1704,7 +1701,9 @@
1704 # Node.start() has synchronous and asynchronous parts, so catch1701 # Node.start() has synchronous and asynchronous parts, so catch
1705 # exceptions arising synchronously, and chain callbacks to the1702 # exceptions arising synchronously, and chain callbacks to the
1706 # Deferred it returns for the asynchronous (post-commit) bits.1703 # Deferred it returns for the asynchronous (post-commit) bits.
1707 starting = self._start(user, commissioning_user_data, old_status)1704 starting = self._start(
1705 user, commissioning_user_data, old_status,
1706 allow_power_cycle=True)
1708 except Exception as error:1707 except Exception as error:
1709 self.status = old_status1708 self.status = old_status
1710 self.save()1709 self.save()
@@ -2098,7 +2097,11 @@
2098 else:2097 else:
2099 can_be_started = True2098 can_be_started = True
2100 can_be_stopped = True2099 can_be_stopped = True
2101 can_be_queried = power_type in QUERY_POWER_TYPES2100 power_driver = PowerDriverRegistry.get_item(power_type)
2101 if power_driver is not None:
2102 can_be_queried = power_driver.queryable
2103 else:
2104 can_be_queried = False
2102 return PowerInfo(2105 return PowerInfo(
2103 can_be_started, can_be_stopped, can_be_queried,2106 can_be_started, can_be_stopped, can_be_queried,
2104 power_type, power_params,2107 power_type, power_params,
@@ -2193,7 +2196,8 @@
2193 # Node.start() has synchronous and asynchronous parts, so catch2196 # Node.start() has synchronous and asynchronous parts, so catch
2194 # exceptions arising synchronously, and chain callbacks to the2197 # exceptions arising synchronously, and chain callbacks to the
2195 # Deferred it returns for the asynchronous (post-commit) bits.2198 # Deferred it returns for the asynchronous (post-commit) bits.
2196 starting = self._start(user, disk_erase_user_data, old_status)2199 starting = self._start(
2200 user, disk_erase_user_data, old_status, allow_power_cycle=True)
2197 except Exception as error:2201 except Exception as error:
2198 # We always mark the node as failed here, although we could2202 # We always mark the node as failed here, although we could
2199 # potentially move it back to the state it was in previously. For2203 # potentially move it back to the state it was in previously. For
@@ -2378,7 +2382,7 @@
2378 if self.power_state == POWER_STATE.OFF:2382 if self.power_state == POWER_STATE.OFF:
2379 # The node is already powered off; we can deallocate all attached2383 # The node is already powered off; we can deallocate all attached
2380 # resources and mark the node READY without delay.2384 # resources and mark the node READY without delay.
2381 release_to_ready = True2385 finalize_release = True
2382 elif self.get_effective_power_info().can_be_queried:2386 elif self.get_effective_power_info().can_be_queried:
2383 # Controlled power type (one for which we can query the power2387 # Controlled power type (one for which we can query the power
2384 # state): update_power_state() will take care of making the node2388 # state): update_power_state() will take care of making the node
@@ -2387,13 +2391,13 @@
2387 post_commit().addCallback(2391 post_commit().addCallback(
2388 callOutToDatabase, Node._set_status_expires,2392 callOutToDatabase, Node._set_status_expires,
2389 self.system_id, self.get_releasing_time())2393 self.system_id, self.get_releasing_time())
2390 release_to_ready = False2394 finalize_release = False
2391 else:2395 else:
2392 # The node's power cannot be reliably controlled. Frankly, this2396 # The node's power cannot be reliably controlled. Frankly, this
2393 # node is not suitable for use with MAAS. Deallocate all attached2397 # node is not suitable for use with MAAS. Deallocate all attached
2394 # resources and mark the node READY without delay because there's2398 # resources and mark the node READY without delay because there's
2395 # not much else we can do.2399 # not much else we can do.
2396 release_to_ready = True2400 finalize_release = True
23972401
2398 self.status = NODE_STATUS.RELEASING2402 self.status = NODE_STATUS.RELEASING
2399 self.token = None2403 self.token = None
@@ -2418,25 +2422,29 @@
2418 self.children.all().delete()2422 self.children.all().delete()
24192423
2420 # Power was off or cannot be powered off so release to ready now.2424 # Power was off or cannot be powered off so release to ready now.
2421 if release_to_ready:2425 if finalize_release:
2422 self._release_to_ready()2426 self._finalize_release()
24232427
2424 @transactional2428 @transactional
2425 def _release_to_ready(self):2429 def _finalize_release(self):
2426 """Release all remaining resources and mark the node `READY`.2430 """Release all remaining resources, mark the machine `READY` if not
2431 dynamic, otherwise delete the machine.
24272432
2428 Releasing a node can be straightforward or it can be a multi-step2433 Releasing a node can be straightforward or it can be a multi-step
2429 operation, which can include a reboot in order to erase disks, then a2434 operation, which can include a reboot in order to erase disks, then a
2430 final power-down. This method should be the absolute last method2435 final power-down. This method should be the absolute last method
2431 called.2436 called.
2432 """2437 """
2433 self.release_interface_config()2438 if self.dynamic:
2434 self.status = NODE_STATUS.READY2439 self.delete()
2435 self.owner = None2440 else:
2436 self.save()2441 self.release_interface_config()
2442 self.status = NODE_STATUS.READY
2443 self.owner = None
2444 self.save()
24372445
2438 # Remove all set owner data.2446 # Remove all set owner data.
2439 OwnerData.objects.filter(node=self).delete()2447 OwnerData.objects.filter(node=self).delete()
24402448
2441 def release_or_erase(2449 def release_or_erase(
2442 self, user, comment=None,2450 self, user, comment=None,
@@ -2551,7 +2559,7 @@
2551 if mark_ready:2559 if mark_ready:
2552 # Ensure the node is released when it powers down.2560 # Ensure the node is released when it powers down.
2553 self.status_expires = None2561 self.status_expires = None
2554 self._release_to_ready()2562 self._finalize_release()
2555 if self.status == NODE_STATUS.EXITING_RESCUE_MODE:2563 if self.status == NODE_STATUS.EXITING_RESCUE_MODE:
2556 if self.previous_status == NODE_STATUS.BROKEN:2564 if self.previous_status == NODE_STATUS.BROKEN:
2557 if power_state == POWER_STATE.OFF:2565 if power_state == POWER_STATE.OFF:
@@ -3074,14 +3082,18 @@
3074 # You can't start a node you don't own unless you're an admin.3082 # You can't start a node you don't own unless you're an admin.
3075 raise PermissionDenied()3083 raise PermissionDenied()
3076 event = EVENT_TYPES.REQUEST_NODE_START3084 event = EVENT_TYPES.REQUEST_NODE_START
3085 allow_power_cycle = False
3077 # If status is ALLOCATED, this start is actually for a deployment.3086 # If status is ALLOCATED, this start is actually for a deployment.
3078 # (Note: this is true even when nodes are being deployed from READY3087 # (Note: this is true even when nodes are being deployed from READY
3079 # state. See node_action.py; the node is acquired and then started.)3088 # state. See node_action.py; the node is acquired and then started.)
3089 # Power cycling is allowed when deployment is being started.
3080 if self.status == NODE_STATUS.ALLOCATED:3090 if self.status == NODE_STATUS.ALLOCATED:
3081 event = EVENT_TYPES.REQUEST_NODE_START_DEPLOYMENT3091 event = EVENT_TYPES.REQUEST_NODE_START_DEPLOYMENT
3092 allow_power_cycle = True
3082 self._register_request_event(3093 self._register_request_event(
3083 user, event, action='start', comment=comment)3094 user, event, action='start', comment=comment)
3084 return self._start(user, user_data)3095 return self._start(
3096 user, user_data, allow_power_cycle=allow_power_cycle)
30853097
3086 def _get_bmc_client_connection_info(self, *args, **kwargs):3098 def _get_bmc_client_connection_info(self, *args, **kwargs):
3087 """Return a tuple that list the rack controllers that can communicate3099 """Return a tuple that list the rack controllers that can communicate
@@ -3114,7 +3126,9 @@
3114 return client_idents, fallback_idents3126 return client_idents, fallback_idents
31153127
3116 @transactional3128 @transactional
3117 def _start(self, user, user_data=None, old_status=None):3129 def _start(
3130 self, user, user_data=None, old_status=None,
3131 allow_power_cycle=False):
3118 """Request on given user's behalf that the node be started up.3132 """Request on given user's behalf that the node be started up.
31193133
3120 :param user: Requesting user.3134 :param user: Requesting user.
@@ -3171,7 +3185,10 @@
31713185
3172 # Request that the node be powered on post-commit.3186 # Request that the node be powered on post-commit.
3173 d = post_commit()3187 d = post_commit()
3174 d = self._power_control_node(d, power_on_node, power_info)3188 if self.power_state == POWER_STATE.ON and allow_power_cycle:
3189 d = self._power_control_node(d, power_cycle, power_info)
3190 else:
3191 d = self._power_control_node(d, power_on_node, power_info)
31753192
3176 # Set the deployment timeout so the node is marked failed after3193 # Set the deployment timeout so the node is marked failed after
3177 # a period of time.3194 # a period of time.
@@ -3532,6 +3549,61 @@
3532 # deal with it.3549 # deal with it.
3533 raise3550 raise
35343551
3552 def _as(self, model):
3553 """Create a `model` that shares underlying storage with `self`.
3554
3555 In other words, the newly returned object will be an instance of
3556 `model` and its `__dict__` will be `self.__dict__`. Not a copy, but a
3557 reference to, so that changes to one will be reflected in the other.
3558 """
3559 new = object.__new__(model)
3560 new.__dict__ = self.__dict__
3561 return new
3562
3563 def as_node(self):
3564 """Return a reference to self that behaves as a `Node`."""
3565 return self._as(Node)
3566
3567 def as_machine(self):
3568 """Return a reference to self that behaves as a `Machine`."""
3569 return self._as(Machine)
3570
3571 def as_device(self):
3572 """Return a reference to self that behaves as a `Device`."""
3573 return self._as(Device)
3574
3575 def as_region_controller(self):
3576 """Return a reference to self that behaves as a `RegionController`."""
3577 return self._as(RegionController)
3578
3579 def as_rack_controller(self):
3580 """Return a reference to self that behaves as a `RackController`."""
3581 return self._as(RackController)
3582
3583 def as_chassis(self):
3584 """Return a reference to self that behaves as a `Chassis`."""
3585 return self._as(Chassis)
3586
3587 def as_storage(self):
3588 """Return a reference to self that behaves as a `Storage`."""
3589 return self._as(Storage)
3590
3591 _as_self = {
3592 NODE_TYPE.DEVICE: as_device,
3593 NODE_TYPE.MACHINE: as_machine,
3594 NODE_TYPE.RACK_CONTROLLER: as_rack_controller,
3595 # XXX ltrager 18-02-2016 - Currently only rack controllers have
3596 # unique functionality so when combined return a rack controller
3597 NODE_TYPE.REGION_AND_RACK_CONTROLLER: as_rack_controller,
3598 NODE_TYPE.REGION_CONTROLLER: as_region_controller,
3599 NODE_TYPE.CHASSIS: as_chassis,
3600 NODE_TYPE.STORAGE: as_storage,
3601 }
3602
3603 def as_self(self):
3604 """Return a reference to self that behaves as its own type."""
3605 return self._as_self[self.node_type](self)
3606
35353607
3536# Piston serializes objects based on the object class.3608# Piston serializes objects based on the object class.
3537# Here we define a proxy class so that we can specialize how devices are3609# Here we define a proxy class so that we can specialize how devices are
@@ -4224,7 +4296,7 @@
4224 # If the refresh is occuring on the running region execute it using4296 # If the refresh is occuring on the running region execute it using
4225 # the region process. This avoids using RPC and sends the node4297 # the region process. This avoids using RPC and sends the node
4226 # results back to this host when in HA.4298 # results back to this host when in HA.
4227 yield typecast_node(self, RegionController).refresh()4299 yield self.as_region_controller().refresh()
4228 return4300 return
42294301
4230 client = yield getClientFor(self.system_id, timeout=1)4302 client = yield getClientFor(self.system_id, timeout=1)
@@ -4457,10 +4529,9 @@
4457 % self.hostname)4529 % self.hostname)
44584530
4459 if self.node_type == NODE_TYPE.REGION_AND_RACK_CONTROLLER:4531 if self.node_type == NODE_TYPE.REGION_AND_RACK_CONTROLLER:
4460 # typecast_to_node_type returns a RackController object when the4532 # Node.as_self() returns a RackController object when the node is
4461 # node is a REGION_AND_RACK_CONTROLLER. Thus the API and websocket4533 # a REGION_AND_RACK_CONTROLLER. Thus the API and websocket will
4462 # will transition a REGION_AND_RACK_CONTROLLER to a4534 # transition a REGION_AND_RACK_CONTROLLER to a REGION_CONTROLLER.
4463 # REGION_CONTROLLER.
4464 self.node_type = NODE_TYPE.RACK_CONTROLLER4535 self.node_type = NODE_TYPE.RACK_CONTROLLER
4465 self.save()4536 self.save()
4466 elif self._was_probably_machine():4537 elif self._was_probably_machine():
@@ -4519,6 +4590,56 @@
4519 pass4590 pass
45204591
45214592
4593class Chassis(Node):
4594 """A node that contains multiple machines and can compose new machines."""
4595
4596 objects = ChassisManager()
4597
4598 class Meta(DefaultMeta):
4599 proxy = True
4600
4601 def __init__(self, *args, **kwargs):
4602 super(Chassis, self).__init__(
4603 node_type=NODE_TYPE.CHASSIS, *args, **kwargs)
4604
4605 def clean_architecture(self, prev):
4606 # Chassis aren't required to have a defined architecture
4607 pass
4608
4609 def clean_hostname_domain(self, prev):
4610 # Chassis is never in a domain.
4611 if self.hostname.find('.') > -1:
4612 # They have specified an FQDN. Split up the pieces, and throw
4613 # away the rest.
4614 self.hostname, _ = self.hostname.split('.', 1)
4615 self.domain = None
4616
4617
4618class Storage(Node):
4619 """A node that provides storage to other machines."""
4620
4621 objects = StorageManager()
4622
4623 class Meta(DefaultMeta):
4624 proxy = True
4625
4626 def __init__(self, *args, **kwargs):
4627 super(Storage, self).__init__(
4628 node_type=NODE_TYPE.STORAGE, *args, **kwargs)
4629
4630 def clean_architecture(self, prev):
4631 # Storage aren't required to have a defined architecture
4632 pass
4633
4634 def clean_hostname_domain(self, prev):
4635 # Storage is never in a domain.
4636 if self.hostname.find('.') > -1:
4637 # They have specified an FQDN. Split up the pieces, and throw
4638 # away the rest.
4639 self.hostname, _ = self.hostname.split('.', 1)
4640 self.domain = None
4641
4642
4522class NodeGroupToRackController(CleanSave, Model):4643class NodeGroupToRackController(CleanSave, Model):
4523 """Store some of the old NodeGroup data so we can migrate it when a rack4644 """Store some of the old NodeGroup data so we can migrate it when a rack
4524 controller is registered.4645 controller is registered.
45254646
=== modified file 'src/maasserver/models/signals/nodes.py'
--- src/maasserver/models/signals/nodes.py 2016-08-16 09:31:16 +0000
+++ src/maasserver/models/signals/nodes.py 2016-12-07 15:50:52 +0000
@@ -12,8 +12,12 @@
12 post_save,12 post_save,
13 pre_save,13 pre_save,
14)14)
15from maasserver.enum import NODE_STATUS15from maasserver.enum import (
16 NODE_STATUS,
17 NODE_TYPE,
18)
16from maasserver.models import (19from maasserver.models import (
20 ChassisHints,
17 Controller,21 Controller,
18 Device,22 Device,
19 Machine,23 Machine,
@@ -105,5 +109,23 @@
105 sender=klass)109 sender=klass)
106110
107111
112def create_chassis_hints(sender, instance, created, **kwargs):
113 """Create `ChassisHints` when `Chassis` is created."""
114 try:
115 chassis_hints = instance.chassis_hints
116 except ChassisHints.DoesNotExist:
117 chassis_hints = None
118 if instance.node_type == NODE_TYPE.CHASSIS:
119 if chassis_hints is None:
120 ChassisHints.objects.create(chassis=instance)
121 elif chassis_hints is not None:
122 chassis_hints.delete()
123
124for klass in NODE_CLASSES:
125 signals.watch(
126 post_save, create_chassis_hints,
127 sender=klass)
128
129
108# Enable all signals by default.130# Enable all signals by default.
109signals.enable()131signals.enable()
110132
=== modified file 'src/maasserver/models/signals/tests/test_nodes.py'
--- src/maasserver/models/signals/tests/test_nodes.py 2016-09-07 14:23:05 +0000
+++ src/maasserver/models/signals/tests/test_nodes.py 2016-12-07 15:50:52 +0000
@@ -11,6 +11,7 @@
11 NODE_STATUS,11 NODE_STATUS,
12 NODE_TYPE,12 NODE_TYPE,
13)13)
14from maasserver.models import ChassisHints
14from maasserver.models.service import (15from maasserver.models.service import (
15 RACK_SERVICES,16 RACK_SERVICES,
16 REGION_SERVICES,17 REGION_SERVICES,
@@ -144,3 +145,27 @@
144 self.assertThat(145 self.assertThat(
145 {service.name for service in services},146 {service.name for service in services},
146 Equals(REGION_SERVICES))147 Equals(REGION_SERVICES))
148
149
150class TestCreateChassisHints(MAASServerTestCase):
151
152 def test_creates_hints_for_chassis(self):
153 chassis = factory.make_Node(node_type=NODE_TYPE.CHASSIS)
154 self.assertIsNotNone(chassis.chassis_hints)
155
156 def test_creates_hints_device_converted_to_chassis(self):
157 device = factory.make_Device()
158 device.node_type = NODE_TYPE.CHASSIS
159 device.save()
160 self.assertIsNotNone(device.chassis_hints)
161
162 def test_deletes_hints_when_chassis_converted_to_device(self):
163 chassis = factory.make_Node(node_type=NODE_TYPE.CHASSIS)
164 chassis.node_type = NODE_TYPE.DEVICE
165 chassis.save()
166 error = None
167 try:
168 reload_object(chassis).chassis_hints
169 except ChassisHints.DoesNotExist as exc:
170 error = exc
171 self.assertIsNotNone(error)
147172
=== modified file 'src/maasserver/models/staticipaddress.py'
--- src/maasserver/models/staticipaddress.py 2016-11-17 18:53:03 +0000
+++ src/maasserver/models/staticipaddress.py 2016-12-07 15:50:52 +0000
@@ -50,19 +50,10 @@
50from maasserver.models.domain import Domain50from maasserver.models.domain import Domain
51from maasserver.models.subnet import Subnet51from maasserver.models.subnet import Subnet
52from maasserver.models.timestampedmodel import TimestampedModel52from maasserver.models.timestampedmodel import TimestampedModel
53from maasserver.utils import orm
53from maasserver.utils.dns import get_ip_based_hostname54from maasserver.utils.dns import get_ip_based_hostname
54from maasserver.utils.orm import (
55 request_transaction_retry,
56 transactional,
57)
58from maasserver.utils.threads import deferToDatabase
59from netaddr import IPAddress55from netaddr import IPAddress
60from provisioningserver.logger import get_maas_logger
61from provisioningserver.utils.enum import map_enum_reverse56from provisioningserver.utils.enum import map_enum_reverse
62from provisioningserver.utils.twisted import asynchronous
63
64
65maaslog = get_maas_logger("node")
6657
6758
68class HostnameIPMapping:59class HostnameIPMapping:
@@ -144,15 +135,13 @@
144 :return: `StaticIPAddress` if successful.135 :return: `StaticIPAddress` if successful.
145 :raise StaticIPAddressUnavailable: if the address was already taken.136 :raise StaticIPAddressUnavailable: if the address was already taken.
146 """137 """
147 ipaddress = StaticIPAddress(138 ipaddress = StaticIPAddress(alloc_type=alloc_type, subnet=subnet)
148 ip=requested_address.format(), alloc_type=alloc_type,
149 subnet=subnet)
150 ipaddress.set_ip_address(requested_address.format())
151 try:139 try:
152 # Try to save this address to the database. Do this in a nested140 # Try to save this address to the database. Do this in a nested
153 # transaction so that we can continue using the outer transaction141 # transaction so that we can continue using the outer transaction
154 # even if this breaks.142 # even if this breaks.
155 with transaction.atomic():143 with transaction.atomic():
144 ipaddress.set_ip_address(requested_address.format())
156 ipaddress.save()145 ipaddress.save()
157 except IntegrityError:146 except IntegrityError:
158 # The address is already taken.147 # The address is already taken.
@@ -168,6 +157,55 @@
168 ipaddress.save()157 ipaddress.save()
169 return ipaddress158 return ipaddress
170159
160 def _attempt_allocation_of_free_address(
161 self, requested_address, alloc_type, user=None, subnet=None):
162 """Attempt to allocate `requested_address`, which is known to be free.
163
164 It is known to be free *in this transaction*, so this could still
165 fail. If it does fail because of a `UNIQUE_VIOLATION` it will request
166 a retry, except while holding an addition lock. This is not perfect:
167 other threads could jump in before acquiring the lock and steal an
168 apparently free address. However, in stampede situations this appears
169 to be effective enough. Experiment by increasing the `count` parameter
170 in `test_allocate_new_works_under_extreme_concurrency`.
171
172 This method shares a lot in common with `_attempt_allocation` so check
173 out its documentation for more details.
174
175 :param requested_address: The address to be allocated.
176 :typr requested_address: IPAddress
177 :param alloc_type: Allocation type.
178 :param user: Optional user.
179 :return: `StaticIPAddress` if successful.
180 :raise RetryTransaction: if the address was already taken.
181 """
182 ipaddress = StaticIPAddress(alloc_type=alloc_type, subnet=subnet)
183 try:
184 # Try to save this address to the database. Do this in a nested
185 # transaction so that we can continue using the outer transaction
186 # even if this breaks.
187 with orm.savepoint():
188 ipaddress.set_ip_address(requested_address.format())
189 ipaddress.save()
190 except IntegrityError as error:
191 if orm.is_unique_violation(error):
192 # The address is taken. We could allow the transaction retry
193 # machinery to take care of this, but instead we'll ask it to
194 # retry with the `address_allocation` lock. We can't take it
195 # here because we're already in a transaction; we need to exit
196 # the transaction, take the lock, and only then try again.
197 orm.request_transaction_retry(locks.address_allocation)
198 else:
199 raise
200 else:
201 # We deliberately do *not* save the user until now because it
202 # might result in an IntegrityError, and we rely on the latter
203 # in the code above to indicate an already allocated IP
204 # address and nothing else.
205 ipaddress.user = user
206 ipaddress.save()
207 return ipaddress
208
171 def allocate_new(209 def allocate_new(
172 self, subnet=None, alloc_type=IPADDRESS_TYPE.AUTO, user=None,210 self, subnet=None, alloc_type=IPADDRESS_TYPE.AUTO, user=None,
173 requested_address=None, exclude_addresses=[]):211 requested_address=None, exclude_addresses=[]):
@@ -185,9 +223,6 @@
185 :param exclude_addresses: A list of addresses which MUST NOT be used.223 :param exclude_addresses: A list of addresses which MUST NOT be used.
186224
187 All IP parameters can be strings or netaddr.IPAddress.225 All IP parameters can be strings or netaddr.IPAddress.
188
189 Note that this method has been designed to work even when the database
190 is running with READ COMMITTED isolation. Try to keep it that way.
191 """226 """
192 # This check for `alloc_type` is important for later on. We rely on227 # This check for `alloc_type` is important for later on. We rely on
193 # detecting IntegrityError as a sign than an IP address is already228 # detecting IntegrityError as a sign than an IP address is already
@@ -203,21 +238,15 @@
203 "Could not find an appropriate subnet.")238 "Could not find an appropriate subnet.")
204239
205 if requested_address is None:240 if requested_address is None:
206 with locks.staticip_acquire:241 requested_address = subnet.get_next_ip_for_allocation(
207 requested_address = self._async_find_free_ip(242 exclude_addresses=exclude_addresses)
208 subnet, exclude_addresses=exclude_addresses).wait(30)243 return self._attempt_allocation_of_free_address(
209 try:244 requested_address, alloc_type, user=user, subnet=subnet)
210 return self._attempt_allocation(
211 requested_address, alloc_type, user,
212 subnet=subnet)
213 except StaticIPAddressUnavailable:
214 # We lost the race: another transaction has taken this IP
215 # address. Retry this transaction from the top.
216 request_transaction_retry()
217 else:245 else:
218 requested_address = IPAddress(requested_address)246 requested_address = IPAddress(requested_address)
219 subnet.validate_static_ip(requested_address)247 subnet.validate_static_ip(requested_address)
220 return self._attempt_allocation(248 return self._attempt_allocation(
249<<<<<<< TREE
221 requested_address, alloc_type,250 requested_address, alloc_type,
222 user=user, subnet=subnet)251 user=user, subnet=subnet)
223252
@@ -375,6 +404,156 @@
375404
376 def _find_free_ip(self, subnet, exclude_addresses=None):405 def _find_free_ip(self, subnet, exclude_addresses=None):
377 return subnet.get_next_ip_for_allocation(exclude_addresses)406 return subnet.get_next_ip_for_allocation(exclude_addresses)
407=======
408 requested_address, alloc_type, user=user, subnet=subnet)
409
410 def _get_special_mappings(self, domain, raw_ttl=False):
411 """Get the special mappings, possibly limited to a single Domain.
412
413 This function is responsible for creating these mappings:
414 - any USER_RESERVED IP,
415 - any IP not associated with a Node,
416 - any IP associated with a DNSResource.
417 The caller is responsible for addresses otherwise derived from nodes.
418
419 Because of how the get hostname_ip_mapping code works, we actually need
420 to fetch ALL of the entries for subnets, but forward mappings need to
421 be domain-specific.
422
423 :param domain: limit return to just the given Domain. If anything
424 other than a Domain is passed in (e.g., a Subnet or None), we
425 return all of the reverse mappings.
426 :param raw_ttl: Boolean, if True then just return the address_ttl,
427 otherwise, coalesce the address_ttl to be the correct answer for
428 zone generation.
429 :return: a (default) dict of hostname: HostnameIPMapping entries.
430 """
431 default_ttl = "%d" % Config.objects.get_config('default_dns_ttl')
432 if isinstance(domain, Domain):
433 # Domains are special in that we only want to have entries for the
434 # domain that we were asked about. And they can possibly come from
435 # either the child or the parent for glue.
436 where_clause = """
437 AND (
438 dnsrr.dom2_id = %s OR
439 node.dom2_id = %s OR
440 dnsrr.domain_id = %s OR
441 node.domain_id = %s
442 """
443 query_parms = [domain.id, domain.id, domain.id, domain.id]
444 # And the default domain is extra special, since it needs to have
445 # A/AAAA RRs for any USER_RESERVED addresses that have no name
446 # otherwise attached to them.
447 if domain.is_default():
448 where_clause += """ OR (
449 dnsrr.fqdn IS NULL AND
450 node.fqdn IS NULL)
451 """
452 where_clause += ")"
453 else:
454 # There is nothing special about the query for subnets.
455 domain = None
456 where_clause = ""
457 query_parms = []
458 # raw_ttl says that we don't coalesce, but we need to pick one, so we
459 # go with DNSResource if it is involved.
460 if raw_ttl:
461 ttl_clause = """COALESCE(dnsrr.address_ttl, node.address_ttl)"""
462 else:
463 ttl_clause = """
464 COALESCE(
465 dnsrr.address_ttl,
466 dnsrr.ttl,
467 node.address_ttl,
468 node.ttl,
469 %s)""" % default_ttl
470 # And here is the SQL query of doom. Build up inner selects to get the
471 # view of a DNSResource (and Node) that we need, and finally use
472 # domain2 to handle the case where an FQDN is also the name of a domain
473 # that we know.
474 sql_query = """
475 SELECT
476 COALESCE(dnsrr.fqdn, node.fqdn) AS fqdn,
477 node.system_id,
478 node.node_type,
479 """ + ttl_clause + """ AS ttl,
480 staticip.ip
481 FROM
482 maasserver_staticipaddress AS staticip
483 LEFT JOIN (
484 /* Create a dnsrr that has what we need. */
485 SELECT
486 CASE WHEN dnsrr.name = '@' THEN
487 dom.name
488 ELSE
489 CONCAT(dnsrr.name, '.', dom.name)
490 END AS fqdn,
491 dom.name as dom_name,
492 dnsrr.domain_id,
493 dnsrr.address_ttl,
494 dom.ttl,
495 dia.staticipaddress_id AS dnsrr_sip_id,
496 dom2.id AS dom2_id
497 FROM maasserver_dnsresource_ip_addresses AS dia
498 JOIN maasserver_dnsresource AS dnsrr ON
499 dia.dnsresource_id = dnsrr.id
500 JOIN maasserver_domain AS dom ON
501 dnsrr.domain_id = dom.id
502 LEFT JOIN maasserver_domain AS dom2 ON
503 CONCAT(dnsrr.name, '.', dom.name) = dom2.name OR (
504 dnsrr.name = '@' AND
505 dom.name SIMILAR TO CONCAT('[-A-Za-z0-9]*.', dom2.name)
506 )
507 ) AS dnsrr ON
508 dnsrr_sip_id = staticip.id
509 LEFT JOIN (
510 /* Create a node that has what we need. */
511 SELECT
512 CONCAT(nd.hostname, '.', dom.name) AS fqdn,
513 dom.name as dom_name,
514 nd.system_id,
515 nd.node_type,
516 nd.domain_id,
517 nd.address_ttl,
518 dom.ttl,
519 iia.staticipaddress_id AS node_sip_id,
520 dom2.id AS dom2_id
521 FROM maasserver_interface_ip_addresses AS iia
522 JOIN maasserver_interface AS iface ON
523 iia.interface_id = iface.id
524 JOIN maasserver_node AS nd ON
525 iface.node_id = nd.id
526 JOIN maasserver_domain AS dom ON
527 nd.domain_id = dom.id
528 LEFT JOIN maasserver_domain AS dom2 ON
529 CONCAT(nd.hostname, '.', dom.name) = dom2.name
530 ) AS node ON
531 node_sip_id = staticip.id
532 WHERE
533 staticip.ip IS NOT NULL AND
534 host(staticip.ip) != '' AND
535 (
536 staticip.alloc_type = %s OR
537 node.fqdn IS NULL OR
538 dnsrr IS NOT NULL
539 )""" + where_clause + """
540 """
541 default_domain = Domain.objects.get_default_domain()
542 mapping = defaultdict(HostnameIPMapping)
543 cursor = connection.cursor()
544 query_parms = [IPADDRESS_TYPE.USER_RESERVED] + query_parms
545 cursor.execute(sql_query, query_parms)
546 for (fqdn, system_id, node_type, ttl,
547 ip) in cursor.fetchall():
548 if fqdn is None or fqdn == '':
549 fqdn = "%s.%s" % (
550 get_ip_based_hostname(ip), default_domain.name)
551 mapping[fqdn].node_type = node_type
552 mapping[fqdn].system_id = system_id
553 mapping[fqdn].ttl = ttl
554 mapping[fqdn].ips.add(ip)
555 return mapping
556>>>>>>> MERGE-SOURCE
378557
379 def get_hostname_ip_mapping(self, domain_or_subnet, raw_ttl=False):558 def get_hostname_ip_mapping(self, domain_or_subnet, raw_ttl=False):
380 """Return hostname mappings for `StaticIPAddress` entries.559 """Return hostname mappings for `StaticIPAddress` entries.
381560
=== modified file 'src/maasserver/models/subnet.py'
--- src/maasserver/models/subnet.py 2016-10-18 16:48:13 +0000
+++ src/maasserver/models/subnet.py 2016-12-07 15:50:52 +0000
@@ -59,6 +59,7 @@
59)59)
60from provisioningserver.logger import get_maas_logger60from provisioningserver.logger import get_maas_logger
61from provisioningserver.utils.network import (61from provisioningserver.utils.network import (
62 IPRANGE_TYPE as MAASIPRANGE_TYPE,
62 MAASIPSet,63 MAASIPSet,
63 make_ipaddress,64 make_ipaddress,
64 make_iprange,65 make_iprange,
@@ -386,6 +387,9 @@
386 active_discovery = BooleanField(387 active_discovery = BooleanField(
387 editable=True, blank=False, null=False, default=False)388 editable=True, blank=False, null=False, default=False)
388389
390 managed = BooleanField(
391 editable=True, blank=False, null=False, default=True)
392
389 @property393 @property
390 def label(self):394 def label(self):
391 """Returns a human-friendly label for this subnet."""395 """Returns a human-friendly label for this subnet."""
@@ -474,7 +478,8 @@
474478
475 def get_ipranges_in_use(479 def get_ipranges_in_use(
476 self, exclude_addresses: IPAddressExcludeList=None,480 self, exclude_addresses: IPAddressExcludeList=None,
477 ranges_only: bool=False,481 ranges_only: bool=False, include_reserved: bool=True,
482 with_neighbours: bool=False,
478 ignore_discovered_ips: bool=False) -> MAASIPSet:483 ignore_discovered_ips: bool=False) -> MAASIPSet:
479 """Returns a `MAASIPSet` of `MAASIPRange` objects which are currently484 """Returns a `MAASIPSet` of `MAASIPRange` objects which are currently
480 in use on this `Subnet`.485 in use on this `Subnet`.
@@ -483,6 +488,8 @@
483 :param ignore_discovered_ips: DISCOVERED addresses are not "in use".488 :param ignore_discovered_ips: DISCOVERED addresses are not "in use".
484 :param ranges_only: if True, filters out gateway IPs, static routes,489 :param ranges_only: if True, filters out gateway IPs, static routes,
485 DNS servers, and `exclude_addresses`.490 DNS servers, and `exclude_addresses`.
491 :param with_neighbours: If True, includes addresses learned from
492 neighbour observation.
486 """493 """
487 if exclude_addresses is None:494 if exclude_addresses is None:
488 exclude_addresses = []495 exclude_addresses = []
@@ -531,8 +538,11 @@
531 for address in exclude_addresses538 for address in exclude_addresses
532 if address in network539 if address in network
533 )540 )
534 ranges |= self.get_reserved_maasipset()541 if include_reserved:
542 ranges |= self.get_reserved_maasipset()
535 ranges |= self.get_dynamic_maasipset()543 ranges |= self.get_dynamic_maasipset()
544 if with_neighbours:
545 ranges |= self.get_maasipset_for_neighbours()
536 return MAASIPSet(ranges)546 return MAASIPSet(ranges)
537547
538 def get_ipranges_available_for_reserved_range(self):548 def get_ipranges_available_for_reserved_range(self):
@@ -549,19 +559,71 @@
549 """Returns a `MAASIPSet` of ranges which are currently free on this559 """Returns a `MAASIPSet` of ranges which are currently free on this
550 `Subnet`.560 `Subnet`.
551561
562 :param ranges_only: if True, filters out gateway IPs, static routes,
563 DNS servers, and `exclude_addresses`.
552 :param exclude_addresses: An iterable of addresses not to use.564 :param exclude_addresses: An iterable of addresses not to use.
553 :param ignore_discovered_ips: DISCOVERED addresses are not "in use".565 :param ignore_discovered_ips: DISCOVERED addresses are not "in use".
566 :param with_neighbours: If True, includes addresses learned from
567 neighbour observation.
554 """568 """
555 if exclude_addresses is None:569 if exclude_addresses is None:
556 exclude_addresses = []570 exclude_addresses = []
557 ranges = self.get_ipranges_in_use(571 in_use = self.get_ipranges_in_use(
558 exclude_addresses=exclude_addresses,572 exclude_addresses=exclude_addresses,
559 ranges_only=ranges_only,573 ranges_only=ranges_only,
574 with_neighbours=with_neighbours,
560 ignore_discovered_ips=ignore_discovered_ips)575 ignore_discovered_ips=ignore_discovered_ips)
561 if with_neighbours:576 if self.managed or ranges_only:
562 ranges |= self.get_maasipset_for_neighbours()577 not_in_use = in_use.get_unused_ranges(self.get_ipnetwork())
563 unused = ranges.get_unused_ranges(self.get_ipnetwork())578 else:
564 return unused579 # The end result we want is a list of unused IP addresses *within*
580 # reserved ranges. To get that result, we first need the full list
581 # of unused IP addresses on the subnet. This is better illustrated
582 # visually below.
583 #
584 # Legend:
585 # X: in-use IP addresses
586 # R: reserved range
587 # Rx: reserved range (with allocated, in-use IP address)
588 #
589 # +----+----+----+----+----+----+
590 # IP address: | 1 | 2 | 3 | 4 | 5 | 6 |
591 # +----+----+----+----+----+----+
592 # Usages: | X | | R | Rx | | X |
593 # +----+----+----+----+----+----+
594 #
595 # We need a set that just contains `3` in this case. To get there,
596 # first calculate the set of all unused addresses on the subnet,
597 # then intersect that set with set of in-use addresses *excluding*
598 # the reserved range, then calculate which addresses within *that*
599 # set are unused:
600 # +----+----+----+----+----+----+
601 # IP address: | 1 | 2 | 3 | 4 | 5 | 6 |
602 # +----+----+----+----+----+----+
603 # unused: | | U | | | U | |
604 # +----+----+----+----+----+----+
605 # unmanaged_in_use: | u | | | u | | u |
606 # +----+----+----+----+----+----+
607 # |= unmanaged: ===============================
608 # +----+----+----+----+----+----+
609 # unmanaged_in_use: | u | U | | u | U | u |
610 # +----+----+----+----+----+----+
611 # get_unused_ranges(): ===============================
612 # +----+----+----+----+----+----+
613 # not_in_use: | | | n | | | |
614 # +----+----+----+----+----+----+
615 unused = in_use.get_unused_ranges(
616 self.get_ipnetwork(), purpose=MAASIPRANGE_TYPE.UNMANAGED)
617 unmanaged_in_use = self.get_ipranges_in_use(
618 exclude_addresses=exclude_addresses,
619 ranges_only=ranges_only,
620 include_reserved=False,
621 with_neighbours=with_neighbours,
622 ignore_discovered_ips=ignore_discovered_ips)
623 unmanaged_in_use |= unused
624 not_in_use = unmanaged_in_use.get_unused_ranges(
625 self.get_ipnetwork(), purpose=MAASIPRANGE_TYPE.UNUSED)
626 return not_in_use
565627
566 def get_maasipset_for_neighbours(self) -> MAASIPSet:628 def get_maasipset_for_neighbours(self) -> MAASIPSet:
567 """Return the observed neighbours in this subnet.629 """Return the observed neighbours in this subnet.
568630
=== modified file 'src/maasserver/models/tests/test_discovery.py'
--- src/maasserver/models/tests/test_discovery.py 2016-11-02 20:18:07 +0000
+++ src/maasserver/models/tests/test_discovery.py 2016-12-07 15:50:52 +0000
@@ -16,6 +16,7 @@
16from maasserver.testing.testcase import MAASServerTestCase16from maasserver.testing.testcase import MAASServerTestCase
17from maastesting.matchers import (17from maastesting.matchers import (
18 DocTestMatches,18 DocTestMatches,
19 IsNonEmptyString,
19 Matches,20 Matches,
20 MockCalledOnceWith,21 MockCalledOnceWith,
21 MockNotCalled,22 MockNotCalled,
@@ -34,7 +35,7 @@
3435
35 def test_mac_organization(self):36 def test_mac_organization(self):
36 discovery = factory.make_Discovery(mac_address="48:51:b7:00:00:00")37 discovery = factory.make_Discovery(mac_address="48:51:b7:00:00:00")
37 self.assertThat(discovery.mac_organization, Equals("Intel Corporate"))38 self.assertThat(discovery.mac_organization, IsNonEmptyString)
3839
39 def test__ignores_duplicate_macs(self):40 def test__ignores_duplicate_macs(self):
40 rack1 = factory.make_RackController()41 rack1 = factory.make_RackController()
4142
=== modified file 'src/maasserver/models/tests/test_event.py'
--- src/maasserver/models/tests/test_event.py 2015-12-01 18:12:59 +0000
+++ src/maasserver/models/tests/test_event.py 2016-12-07 15:50:52 +0000
@@ -11,6 +11,7 @@
11from django.db import IntegrityError11from django.db import IntegrityError
12from maasserver.models import (12from maasserver.models import (
13 Event,13 Event,
14 event as event_module,
14 EventType,15 EventType,
15)16)
16from maasserver.testing.factory import factory17from maasserver.testing.factory import factory
@@ -84,6 +85,15 @@
84 self.assertIsNotNone(EventType.objects.get(name=event_type))85 self.assertIsNotNone(EventType.objects.get(name=event_type))
85 self.assertIsNotNone(Event.objects.get(node=node))86 self.assertIsNotNone(Event.objects.get(node=node))
8687
88 def test_create_region_event_creates_region_event(self):
89 region = factory.make_RegionRackController()
90 self.patch(event_module, 'get_maas_id').return_value = region.system_id
91 Event.objects.create_region_event(
92 event_type=EVENT_TYPES.REGION_IMPORT_ERROR)
93 self.assertIsNotNone(
94 EventType.objects.get(name=EVENT_TYPES.REGION_IMPORT_ERROR))
95 self.assertIsNotNone(Event.objects.get(node=region))
96
87 def test_register_event_and_event_type_handles_integrity_errors(self):97 def test_register_event_and_event_type_handles_integrity_errors(self):
88 # It's possible that two calls to98 # It's possible that two calls to
89 # register_event_and_event_type() could arrive at more-or-less99 # register_event_and_event_type() could arrive at more-or-less
90100
=== modified file 'src/maasserver/models/tests/test_neighbour.py'
--- src/maasserver/models/tests/test_neighbour.py 2016-08-19 11:40:52 +0000
+++ src/maasserver/models/tests/test_neighbour.py 2016-12-07 15:50:52 +0000
@@ -7,11 +7,11 @@
77
8from maasserver.testing.factory import factory8from maasserver.testing.factory import factory
9from maasserver.testing.testcase import MAASServerTestCase9from maasserver.testing.testcase import MAASServerTestCase
10from testtools.matchers import Equals10from maastesting.matchers import IsNonEmptyString
1111
1212
13class TestNeighbourModel(MAASServerTestCase):13class TestNeighbourModel(MAASServerTestCase):
1414
15 def test_mac_organization(self):15 def test_mac_organization(self):
16 neighbour = factory.make_Neighbour(mac_address="48:51:b7:00:00:00")16 neighbour = factory.make_Neighbour(mac_address="48:51:b7:00:00:00")
17 self.assertThat(neighbour.mac_organization, Equals("Intel Corporate"))17 self.assertThat(neighbour.mac_organization, IsNonEmptyString)
1818
=== modified file 'src/maasserver/models/tests/test_node.py'
--- src/maasserver/models/tests/test_node.py 2016-12-07 11:26:49 +0000
+++ src/maasserver/models/tests/test_node.py 2016-12-07 15:50:52 +0000
@@ -60,6 +60,7 @@
60 BondInterface,60 BondInterface,
61 BootResource,61 BootResource,
62 BridgeInterface,62 BridgeInterface,
63 Chassis,
63 Config,64 Config,
64 Controller,65 Controller,
65 Device,66 Device,
@@ -78,6 +79,7 @@
78 RegionRackRPCConnection,79 RegionRackRPCConnection,
79 Service,80 Service,
80 Space,81 Space,
82 Storage,
81 Subnet,83 Subnet,
82 UnknownInterface,84 UnknownInterface,
83 VLAN,85 VLAN,
@@ -95,8 +97,6 @@
95 GatewayDefinition,97 GatewayDefinition,
96 generate_node_system_id,98 generate_node_system_id,
97 PowerInfo,99 PowerInfo,
98 typecast_node,
99 typecast_to_node_type,
100)100)
101from maasserver.models.signals import power as node_query101from maasserver.models.signals import power as node_query
102from maasserver.models.timestampedmodel import now102from maasserver.models.timestampedmodel import now
@@ -137,6 +137,7 @@
137from maasserver.worker_user import get_worker_user137from maasserver.worker_user import get_worker_user
138from maastesting.matchers import (138from maastesting.matchers import (
139 DocTestMatches,139 DocTestMatches,
140 IsNonEmptyString,
140 MockCalledOnce,141 MockCalledOnce,
141 MockCalledOnceWith,142 MockCalledOnceWith,
142 MockCallsMatch,143 MockCallsMatch,
@@ -154,12 +155,11 @@
154 disk_erasing,155 disk_erasing,
155)156)
156from netaddr import IPAddress157from netaddr import IPAddress
158from provisioningserver.drivers.power import PowerDriverRegistry
157from provisioningserver.events import (159from provisioningserver.events import (
158 EVENT_DETAILS,160 EVENT_DETAILS,
159 EVENT_TYPES,161 EVENT_TYPES,
160)162)
161from provisioningserver.power import QUERY_POWER_TYPES
162from provisioningserver.power.schema import JSON_POWER_TYPE_PARAMETERS
163from provisioningserver.rpc.cluster import (163from provisioningserver.rpc.cluster import (
164 AddChassis,164 AddChassis,
165 DisableAndShutoffRackd,165 DisableAndShutoffRackd,
@@ -225,59 +225,86 @@
225 "... after 1000 iterations ... no unused node identifiers."))225 "... after 1000 iterations ... no unused node identifiers."))
226226
227227
228class TestTypeCastNode(MAASServerTestCase):228def HasType(type_):
229 def test_all_node_types_can_be_casted(self):229 return AfterPreprocessing(type, Is(type_), annotate=False)
230 node = factory.make_Node()230
231 cast_to = random.choice(231
232 [Device, Machine, Node, RackController, RegionController])232def SharesStorageWith(other):
233 typecast_node(node, cast_to)233 return AfterPreprocessing(
234 self.assertIsInstance(node, cast_to)234 (lambda thing: thing.__dict__), Is(other.__dict__),
235235 annotate=False)
236 def test_rejects_casting_to_non_node_type_objects(self):
237 node = factory.make_Node()
238 self.assertRaises(AssertionError, typecast_node, node, object)
239
240 def test_rejects_casting_non_node_type(self):
241 node = object()
242 cast_to = random.choice(
243 [Device, Machine, Node, RackController, RegionController])
244 self.assertRaises(AssertionError, typecast_node, node, cast_to)
245
246 def test_sets_hostname_if_blank(self):
247 node = factory.make_Node(hostname='')
248 self.assertNotEqual('', node.hostname)
249236
250237
251class TestTypeCastToNodeType(MAASServerTestCase):238class TestTypeCastToNodeType(MAASServerTestCase):
239
240 def test_cast_to_self(self):
241 node = factory.make_Node().as_node()
242 node_types = set(map_enum(NODE_TYPE).values())
243 casts = {
244 NODE_TYPE.DEVICE: Device,
245 NODE_TYPE.MACHINE: Machine,
246 NODE_TYPE.RACK_CONTROLLER: RackController,
247 NODE_TYPE.REGION_AND_RACK_CONTROLLER: RackController,
248 NODE_TYPE.REGION_CONTROLLER: RegionController,
249 NODE_TYPE.CHASSIS: Chassis,
250 NODE_TYPE.STORAGE: Storage,
251 }
252 self.assertThat(casts.keys(), Equals(node_types))
253 for node_type, cast_type in casts.items():
254 node.node_type = node_type
255 node_as_self = node.as_self()
256 self.assertThat(node, HasType(Node))
257 self.assertThat(node_as_self, HasType(cast_type))
258 self.assertThat(node_as_self, SharesStorageWith(node))
259
252 def test_cast_to_machine(self):260 def test_cast_to_machine(self):
253 node = factory.make_Node(node_type=NODE_TYPE.MACHINE)261 node = factory.make_Node().as_node()
254 machine = typecast_to_node_type(node)262 machine = node.as_machine()
255 self.assertIsInstance(machine, Machine)263 self.assertThat(node, HasType(Node))
264 self.assertThat(machine, HasType(Machine))
265 self.assertThat(machine, SharesStorageWith(node))
256266
257 def test_cast_to_rack_controller(self):267 def test_cast_to_rack_controller(self):
258 node = factory.make_Node(node_type=NODE_TYPE.RACK_CONTROLLER)268 node = factory.make_Node().as_node()
259 rack = typecast_to_node_type(node)269 rack = node.as_rack_controller()
260 self.assertIsInstance(rack, RackController)270 self.assertThat(node, HasType(Node))
261271 self.assertThat(rack, HasType(RackController))
262 def test_cast_to_region_and_rack_controller(self):272 self.assertThat(rack, SharesStorageWith(node))
263 node = factory.make_Node(
264 node_type=NODE_TYPE.REGION_AND_RACK_CONTROLLER)
265 rack = typecast_to_node_type(node)
266 self.assertIsInstance(rack, RackController)
267273
268 def test_cast_to_region_controller(self):274 def test_cast_to_region_controller(self):
269 node = factory.make_Node(node_type=NODE_TYPE.REGION_CONTROLLER)275 node = factory.make_Node().as_node()
270 region = typecast_to_node_type(node)276 region = node.as_region_controller()
271 self.assertIsInstance(region, RegionController)277 self.assertThat(node, HasType(Node))
278 self.assertThat(region, HasType(RegionController))
279 self.assertThat(region, SharesStorageWith(node))
272280
273 def test_cast_to_device(self):281 def test_cast_to_device(self):
274 node = factory.make_Node(node_type=NODE_TYPE.DEVICE)282 node = factory.make_Node().as_node()
275 device = typecast_to_node_type(node)283 device = node.as_device()
276 self.assertIsInstance(device, Device)284 self.assertThat(node, HasType(Node))
277285 self.assertThat(device, HasType(Device))
278 def test_throws_exception_on_unknown_type(self):286 self.assertThat(device, SharesStorageWith(node))
279 node = factory.make_Node(node_type=random.randint(10, 10000))287
280 self.assertRaises(NotImplementedError, typecast_to_node_type, node)288 def test_cast_to_node(self):
289 machine = factory.make_Machine()
290 node = machine.as_node()
291 self.assertThat(machine, HasType(Machine))
292 self.assertThat(node, HasType(Node))
293 self.assertThat(node, SharesStorageWith(machine))
294
295 def test_cast_to_chassis(self):
296 node = factory.make_Node().as_node()
297 chassis = node.as_chassis()
298 self.assertThat(node, HasType(Node))
299 self.assertThat(chassis, HasType(Chassis))
300 self.assertThat(chassis, SharesStorageWith(node))
301
302 def test_cast_to_storage(self):
303 node = factory.make_Node().as_node()
304 storage = node.as_storage()
305 self.assertThat(node, HasType(Node))
306 self.assertThat(storage, HasType(Storage))
307 self.assertThat(storage, SharesStorageWith(node))
281308
282309
283class TestNodeManager(MAASServerTestCase):310class TestNodeManager(MAASServerTestCase):
@@ -1229,12 +1256,11 @@
1229 sentinel.power_parameters), node.get_effective_power_info())1256 sentinel.power_parameters), node.get_effective_power_info())
12301257
1231 def test_get_effective_power_info_cant_be_queried(self):1258 def test_get_effective_power_info_cant_be_queried(self):
1232 all_power_types = {1259 uncontrolled_power_types = [
1233 power_type_details['name']1260 driver.name
1234 for power_type_details in JSON_POWER_TYPE_PARAMETERS1261 for _, driver in PowerDriverRegistry
1235 }1262 if not driver.queryable
1236 uncontrolled_power_types = all_power_types.difference(1263 ]
1237 QUERY_POWER_TYPES)
1238 for power_type in uncontrolled_power_types:1264 for power_type in uncontrolled_power_types:
1239 node = factory.make_Node(power_type=power_type)1265 node = factory.make_Node(power_type=power_type)
1240 gepp = self.patch(node, "get_effective_power_parameters")1266 gepp = self.patch(node, "get_effective_power_parameters")
@@ -1245,7 +1271,11 @@
1245 node.get_effective_power_info())1271 node.get_effective_power_info())
12461272
1247 def test_get_effective_power_info_can_be_queried(self):1273 def test_get_effective_power_info_can_be_queried(self):
1248 power_type = random.choice(QUERY_POWER_TYPES)1274 power_type = random.choice([
1275 driver.name
1276 for _, driver in PowerDriverRegistry
1277 if driver.queryable
1278 ])
1249 node = factory.make_Node(power_type=power_type)1279 node = factory.make_Node(power_type=power_type)
1250 gepp = self.patch(node, "get_effective_power_parameters")1280 gepp = self.patch(node, "get_effective_power_parameters")
1251 self.assertEqual(1281 self.assertEqual(
@@ -1499,7 +1529,8 @@
1499 node_start = self.patch(node, '_start')1529 node_start = self.patch(node, '_start')
1500 # Return a post-commit hook from Node.start().1530 # Return a post-commit hook from Node.start().
1501 node_start.side_effect = (1531 node_start.side_effect = (
1502 lambda user, user_data, old_status: post_commit())1532 lambda user, user_data, old_status, allow_power_cycle: (
1533 post_commit()))
1503 Config.objects.set_config('disk_erase_with_secure_erase', True)1534 Config.objects.set_config('disk_erase_with_secure_erase', True)
1504 Config.objects.set_config('disk_erase_with_quick_erase', True)1535 Config.objects.set_config('disk_erase_with_quick_erase', True)
1505 with post_commit_hooks:1536 with post_commit_hooks:
@@ -1521,7 +1552,8 @@
1521 node_start = self.patch(node, '_start')1552 node_start = self.patch(node, '_start')
1522 # Return a post-commit hook from Node.start().1553 # Return a post-commit hook from Node.start().
1523 node_start.side_effect = (1554 node_start.side_effect = (
1524 lambda user, user_data, old_status: post_commit())1555 lambda user, user_data, old_status, allow_power_cycle: (
1556 post_commit()))
1525 Config.objects.set_config('disk_erase_with_secure_erase', False)1557 Config.objects.set_config('disk_erase_with_secure_erase', False)
1526 Config.objects.set_config('disk_erase_with_quick_erase', False)1558 Config.objects.set_config('disk_erase_with_quick_erase', False)
1527 with post_commit_hooks:1559 with post_commit_hooks:
@@ -1543,14 +1575,17 @@
1543 node_start = self.patch(node, '_start')1575 node_start = self.patch(node, '_start')
1544 # Return a post-commit hook from Node.start().1576 # Return a post-commit hook from Node.start().
1545 node_start.side_effect = (1577 node_start.side_effect = (
1546 lambda user, user_data, old_status: post_commit())1578 lambda user, user_data, old_status, allow_power_cycle: (
1579 post_commit()))
1547 with post_commit_hooks:1580 with post_commit_hooks:
1548 node.start_disk_erasing(owner)1581 node.start_disk_erasing(owner)
1549 self.expectThat(node.owner, Equals(owner))1582 self.expectThat(node.owner, Equals(owner))
1550 self.expectThat(node.status, Equals(NODE_STATUS.DISK_ERASING))1583 self.expectThat(node.status, Equals(NODE_STATUS.DISK_ERASING))
1551 self.expectThat(node.agent_name, Equals(agent_name))1584 self.expectThat(node.agent_name, Equals(agent_name))
1552 self.assertThat(1585 self.assertThat(
1553 node_start, MockCalledOnceWith(owner, ANY, NODE_STATUS.ALLOCATED))1586 node_start,
1587 MockCalledOnceWith(
1588 owner, ANY, NODE_STATUS.ALLOCATED, allow_power_cycle=True))
15541589
1555 def test_start_disk_erasing_logs_user_request(self):1590 def test_start_disk_erasing_logs_user_request(self):
1556 owner = factory.make_User()1591 owner = factory.make_User()
@@ -1558,12 +1593,15 @@
1558 node_start = self.patch(node, '_start')1593 node_start = self.patch(node, '_start')
1559 # Return a post-commit hook from Node.start().1594 # Return a post-commit hook from Node.start().
1560 node_start.side_effect = (1595 node_start.side_effect = (
1561 lambda user, user_data, old_status: post_commit())1596 lambda user, user_data, old_status, allow_power_cycle: (
1597 post_commit()))
1562 register_event = self.patch(node, '_register_request_event')1598 register_event = self.patch(node, '_register_request_event')
1563 with post_commit_hooks:1599 with post_commit_hooks:
1564 node.start_disk_erasing(owner)1600 node.start_disk_erasing(owner)
1565 self.assertThat(1601 self.assertThat(
1566 node_start, MockCalledOnceWith(owner, ANY, NODE_STATUS.ALLOCATED))1602 node_start,
1603 MockCalledOnceWith(
1604 owner, ANY, NODE_STATUS.ALLOCATED, allow_power_cycle=True))
1567 self.assertThat(register_event, MockCalledOnceWith(1605 self.assertThat(register_event, MockCalledOnceWith(
1568 owner, EVENT_TYPES.REQUEST_NODE_ERASE_DISK,1606 owner, EVENT_TYPES.REQUEST_NODE_ERASE_DISK,
1569 action='start disk erasing', comment=None))1607 action='start disk erasing', comment=None))
@@ -1626,7 +1664,8 @@
16261664
1627 self.assertThat(1665 self.assertThat(
1628 node_start, MockCalledOnceWith(1666 node_start, MockCalledOnceWith(
1629 admin, generate_user_data.return_value, NODE_STATUS.ALLOCATED))1667 admin, generate_user_data.return_value, NODE_STATUS.ALLOCATED,
1668 allow_power_cycle=True))
1630 self.assertEqual(NODE_STATUS.FAILED_DISK_ERASING, node.status)1669 self.assertEqual(NODE_STATUS.FAILED_DISK_ERASING, node.status)
16311670
1632 def test_start_disk_erasing_sets_status_on_post_commit_error(self):1671 def test_start_disk_erasing_sets_status_on_post_commit_error(self):
@@ -1807,20 +1846,18 @@
1807 }1846 }
1808 # Use an "uncontrolled" power type (i.e. a power type for which we1847 # Use an "uncontrolled" power type (i.e. a power type for which we
1809 # cannot query the status of the node).1848 # cannot query the status of the node).
1810 all_power_types = {1849 power_type = random.choice([
1811 power_type_details['name']1850 driver.name
1812 for power_type_details in JSON_POWER_TYPE_PARAMETERS1851 for _, driver in PowerDriverRegistry
1813 }1852 if not driver.queryable
1814 uncontrolled_power_types = (1853 ])
1815 all_power_types.difference(QUERY_POWER_TYPES))
1816 power_type = random.choice(list(uncontrolled_power_types))
1817 rack = factory.make_RackController()1854 rack = factory.make_RackController()
1818 node = factory.make_Node_with_Interface_on_Subnet(1855 node = factory.make_Node_with_Interface_on_Subnet(
1819 status=NODE_STATUS.ALLOCATED, owner=owner, owner_data=owner_data,1856 status=NODE_STATUS.ALLOCATED, owner=owner, owner_data=owner_data,
1820 agent_name=agent_name, power_type=power_type, primary_rack=rack)1857 agent_name=agent_name, power_type=power_type, primary_rack=rack)
1821 self.patch(Node, '_set_status_expires')1858 self.patch(Node, '_set_status_expires')
1822 mock_stop = self.patch(node, "_stop")1859 mock_stop = self.patch(node, "_stop")
1823 mock_release_to_ready = self.patch(node, "_release_to_ready")1860 mock_finalize_release = self.patch(node, "_finalize_release")
1824 node.power_state = POWER_STATE.ON1861 node.power_state = POWER_STATE.ON
1825 node.release()1862 node.release()
1826 self.expectThat(Node._set_status_expires, MockNotCalled())1863 self.expectThat(Node._set_status_expires, MockNotCalled())
@@ -1833,7 +1870,7 @@
1833 self.expectThat(node.distro_series, Equals(''))1870 self.expectThat(node.distro_series, Equals(''))
1834 self.expectThat(node.license_key, Equals(''))1871 self.expectThat(node.license_key, Equals(''))
1835 self.expectThat(mock_stop, MockCalledOnceWith(node.owner))1872 self.expectThat(mock_stop, MockCalledOnceWith(node.owner))
1836 self.expectThat(mock_release_to_ready, MockCalledOnceWith())1873 self.expectThat(mock_finalize_release, MockCalledOnceWith())
18371874
1838 def test_release_node_that_has_power_off(self):1875 def test_release_node_that_has_power_off(self):
1839 agent_name = factory.make_name('agent-name')1876 agent_name = factory.make_name('agent-name')
@@ -1879,6 +1916,16 @@
1879 [], list(NodeResult.objects.filter(1916 [], list(NodeResult.objects.filter(
1880 node=node, result_type=RESULT_TYPE.INSTALLATION)))1917 node=node, result_type=RESULT_TYPE.INSTALLATION)))
18811918
1919 def test_release_deletes_dynamic_machine(self):
1920 agent_name = factory.make_name('agent-name')
1921 owner = factory.make_User()
1922 node = factory.make_Node(
1923 status=NODE_STATUS.ALLOCATED, owner=owner, agent_name=agent_name,
1924 dynamic=True, power_state=POWER_STATE.OFF)
1925 with post_commit_hooks:
1926 node.release()
1927 self.assertIsNone(reload_object(node))
1928
1882 def test_dynamic_ip_addresses_from_ip_address_table(self):1929 def test_dynamic_ip_addresses_from_ip_address_table(self):
1883 node = factory.make_Node()1930 node = factory.make_Node()
1884 interfaces = [1931 interfaces = [
@@ -2233,7 +2280,8 @@
2233 node_start = self.patch(node, '_start')2280 node_start = self.patch(node, '_start')
2234 # Return a post-commit hook from Node.start().2281 # Return a post-commit hook from Node.start().
2235 node_start.side_effect = (2282 node_start.side_effect = (
2236 lambda user, user_data, old_status: post_commit())2283 lambda user, user_data, old_status, allow_power_cycle: (
2284 post_commit()))
2237 admin = factory.make_admin()2285 admin = factory.make_admin()
2238 node.start_commissioning(admin)2286 node.start_commissioning(admin)
2239 post_commit_hooks.reset() # Ignore these for now.2287 post_commit_hooks.reset() # Ignore these for now.
@@ -2243,7 +2291,7 @@
2243 }2291 }
2244 self.assertAttributes(node, expected_attrs)2292 self.assertAttributes(node, expected_attrs)
2245 self.assertThat(node_start, MockCalledOnceWith(2293 self.assertThat(node_start, MockCalledOnceWith(
2246 admin, ANY, NODE_STATUS.NEW))2294 admin, ANY, NODE_STATUS.NEW, allow_power_cycle=True))
22472295
2248 def test_start_commissioning_sets_options(self):2296 def test_start_commissioning_sets_options(self):
2249 rack = factory.make_RackController()2297 rack = factory.make_RackController()
@@ -2253,7 +2301,8 @@
2253 node_start = self.patch(node, '_start')2301 node_start = self.patch(node, '_start')
2254 # Return a post-commit hook from Node.start().2302 # Return a post-commit hook from Node.start().
2255 node_start.side_effect = (2303 node_start.side_effect = (
2256 lambda user, user_data, old_status: post_commit())2304 lambda user, user_data, old_status, allow_power_cycle: (
2305 post_commit()))
2257 admin = factory.make_admin()2306 admin = factory.make_admin()
2258 enable_ssh = factory.pick_bool()2307 enable_ssh = factory.pick_bool()
2259 skip_networking = factory.pick_bool()2308 skip_networking = factory.pick_bool()
@@ -2274,7 +2323,8 @@
2274 node = factory.make_Node(status=NODE_STATUS.NEW)2323 node = factory.make_Node(status=NODE_STATUS.NEW)
2275 node_start = self.patch(node, '_start')2324 node_start = self.patch(node, '_start')
2276 node_start.side_effect = (2325 node_start.side_effect = (
2277 lambda user, user_data, old_status: post_commit())2326 lambda user, user_data, old_status, allow_power_cycle: (
2327 post_commit()))
2278 user_data = factory.make_string().encode('ascii')2328 user_data = factory.make_string().encode('ascii')
2279 generate_user_data = self.patch(2329 generate_user_data = self.patch(
2280 commissioning, 'generate_user_data')2330 commissioning, 'generate_user_data')
@@ -2283,13 +2333,14 @@
2283 node.start_commissioning(admin)2333 node.start_commissioning(admin)
2284 post_commit_hooks.reset() # Ignore these for now.2334 post_commit_hooks.reset() # Ignore these for now.
2285 self.assertThat(node_start, MockCalledOnceWith(2335 self.assertThat(node_start, MockCalledOnceWith(
2286 admin, user_data, NODE_STATUS.NEW))2336 admin, user_data, NODE_STATUS.NEW, allow_power_cycle=True))
22872337
2288 def test_start_commissioning_sets_min_hwe_kernel(self):2338 def test_start_commissioning_sets_min_hwe_kernel(self):
2289 node = factory.make_Node(status=NODE_STATUS.NEW)2339 node = factory.make_Node(status=NODE_STATUS.NEW)
2290 node_start = self.patch(node, '_start')2340 node_start = self.patch(node, '_start')
2291 node_start.side_effect = (2341 node_start.side_effect = (
2292 lambda user, user_data, old_status: post_commit())2342 lambda user, user_data, old_status, allow_power_cycle: (
2343 post_commit()))
2293 user_data = factory.make_string().encode('ascii')2344 user_data = factory.make_string().encode('ascii')
2294 generate_user_data = self.patch(2345 generate_user_data = self.patch(
2295 commissioning, 'generate_user_data')2346 commissioning, 'generate_user_data')
@@ -2300,11 +2351,34 @@
2300 post_commit_hooks.reset() # Ignore these for now.2351 post_commit_hooks.reset() # Ignore these for now.
2301 self.assertEqual('hwe-v', node.min_hwe_kernel)2352 self.assertEqual('hwe-v', node.min_hwe_kernel)
23022353
2354 def test_start_commissioning_starts_node_if_already_on(self):
2355 node = factory.make_Node(
2356 interface=True, status=NODE_STATUS.NEW, power_type='manual',
2357 power_state=POWER_STATE.ON)
2358 node_start = self.patch(node, '_start')
2359 # Return a post-commit hook from Node.start().
2360 node_start.side_effect = (
2361 lambda user, user_data, old_status, allow_power_cycle: (
2362 post_commit()))
2363 admin = factory.make_admin()
2364 node.start_commissioning(admin)
2365 post_commit_hooks.reset() # Ignore these for now.
2366 node = reload_object(node)
2367 expected_attrs = {
2368 'status': NODE_STATUS.COMMISSIONING,
2369 'owner': admin,
2370 }
2371 self.assertAttributes(node, expected_attrs)
2372 self.expectThat(node.owner, Equals(admin))
2373 self.assertThat(node_start, MockCalledOnceWith(
2374 admin, ANY, NODE_STATUS.NEW, allow_power_cycle=True))
2375
2303 def test_start_commissioning_clears_node_commissioning_results(self):2376 def test_start_commissioning_clears_node_commissioning_results(self):
2304 node = factory.make_Node(status=NODE_STATUS.NEW)2377 node = factory.make_Node(status=NODE_STATUS.NEW)
2305 node_start = self.patch(node, '_start')2378 node_start = self.patch(node, '_start')
2306 node_start.side_effect = (2379 node_start.side_effect = (
2307 lambda user, user_data, old_status: post_commit())2380 lambda user, user_data, old_status, allow_power_cycle: (
2381 post_commit()))
2308 NodeResult.objects.store_data(2382 NodeResult.objects.store_data(
2309 node, factory.make_string(),2383 node, factory.make_string(),
2310 random.randint(0, 10),2384 random.randint(0, 10),
@@ -2318,7 +2392,8 @@
2318 node = factory.make_Node(status=NODE_STATUS.NEW)2392 node = factory.make_Node(status=NODE_STATUS.NEW)
2319 node_start = self.patch(node, '_start')2393 node_start = self.patch(node, '_start')
2320 node_start.side_effect = (2394 node_start.side_effect = (
2321 lambda user, user_data, old_status: post_commit())2395 lambda user, user_data, old_status, allow_power_cycle: (
2396 post_commit()))
2322 clear_storage = self.patch_autospec(2397 clear_storage = self.patch_autospec(
2323 node, '_clear_full_storage_configuration')2398 node, '_clear_full_storage_configuration')
2324 admin = factory.make_admin()2399 admin = factory.make_admin()
@@ -2330,7 +2405,8 @@
2330 node = factory.make_Node(status=NODE_STATUS.NEW)2405 node = factory.make_Node(status=NODE_STATUS.NEW)
2331 node_start = self.patch(node, '_start')2406 node_start = self.patch(node, '_start')
2332 node_start.side_effect = (2407 node_start.side_effect = (
2333 lambda user, user_data, old_status: post_commit())2408 lambda user, user_data, old_status, allow_power_cycle: (
2409 post_commit()))
2334 clear_storage = self.patch_autospec(2410 clear_storage = self.patch_autospec(
2335 node, '_clear_full_storage_configuration')2411 node, '_clear_full_storage_configuration')
2336 admin = factory.make_admin()2412 admin = factory.make_admin()
@@ -2342,7 +2418,8 @@
2342 node = factory.make_Node(status=NODE_STATUS.NEW)2418 node = factory.make_Node(status=NODE_STATUS.NEW)
2343 node_start = self.patch(node, '_start')2419 node_start = self.patch(node, '_start')
2344 node_start.side_effect = (2420 node_start.side_effect = (
2345 lambda user, user_data, old_status: post_commit())2421 lambda user, user_data, old_status, allow_power_cycle: (
2422 post_commit()))
2346 clear_networking = self.patch_autospec(2423 clear_networking = self.patch_autospec(
2347 node, '_clear_networking_configuration')2424 node, '_clear_networking_configuration')
2348 admin = factory.make_admin()2425 admin = factory.make_admin()
@@ -2354,7 +2431,8 @@
2354 node = factory.make_Node(status=NODE_STATUS.NEW)2431 node = factory.make_Node(status=NODE_STATUS.NEW)
2355 node_start = self.patch(node, '_start')2432 node_start = self.patch(node, '_start')
2356 node_start.side_effect = (2433 node_start.side_effect = (
2357 lambda user, user_data, old_status: post_commit())2434 lambda user, user_data, old_status, allow_power_cycle: (
2435 post_commit()))
2358 clear_networking = self.patch_autospec(2436 clear_networking = self.patch_autospec(
2359 node, '_clear_networking_configuration')2437 node, '_clear_networking_configuration')
2360 admin = factory.make_admin()2438 admin = factory.make_admin()
@@ -2398,7 +2476,8 @@
2398 self.assertThat(2476 self.assertThat(
2399 node_start,2477 node_start,
2400 MockCalledOnceWith(2478 MockCalledOnceWith(
2401 admin, generate_user_data.return_value, NODE_STATUS.NEW))2479 admin, generate_user_data.return_value, NODE_STATUS.NEW,
2480 allow_power_cycle=True))
2402 self.assertEqual(NODE_STATUS.NEW, node.status)2481 self.assertEqual(NODE_STATUS.NEW, node.status)
24032482
2404 def test_start_commissioning_logs_and_raises_errors_in_starting(self):2483 def test_start_commissioning_logs_and_raises_errors_in_starting(self):
@@ -2422,7 +2501,8 @@
2422 node_start = self.patch(node, '_start')2501 node_start = self.patch(node, '_start')
2423 # Return a post-commit hook from Node.start().2502 # Return a post-commit hook from Node.start().
2424 node_start.side_effect = (2503 node_start.side_effect = (
2425 lambda user, user_data, old_status: post_commit())2504 lambda user, user_data, old_status, allow_power_cycle: (
2505 post_commit()))
2426 admin = factory.make_admin()2506 admin = factory.make_admin()
2427 node.start_commissioning(admin)2507 node.start_commissioning(admin)
2428 post_commit_hooks.reset() # Ignore these for now.2508 post_commit_hooks.reset() # Ignore these for now.
@@ -2640,11 +2720,10 @@
26402720
2641 def test_full_clean_checks_architecture_for_installable_nodes(self):2721 def test_full_clean_checks_architecture_for_installable_nodes(self):
2642 device = factory.make_Device(architecture='')2722 device = factory.make_Device(architecture='')
2643 # Set type here so we don't cause exception while creating object2723 device.node_type = factory.pick_enum(
2644 node = typecast_node(device, Node)
2645 node.node_type = factory.pick_enum(
2646 NODE_TYPE, but_not=[NODE_TYPE.DEVICE])2724 NODE_TYPE, but_not=[NODE_TYPE.DEVICE])
2647 exception = self.assertRaises(ValidationError, node.full_clean)2725 exception = self.assertRaises(
2726 ValidationError, device.as_node().full_clean)
2648 self.assertEqual(2727 self.assertEqual(
2649 exception.message_dict,2728 exception.message_dict,
2650 {'architecture':2729 {'architecture':
@@ -2985,9 +3064,7 @@
2985 INTERFACE_TYPE.PHYSICAL, mac_address='ec:a8:6b:fd:ae:3f',3064 INTERFACE_TYPE.PHYSICAL, mac_address='ec:a8:6b:fd:ae:3f',
2986 node=node)3065 node=node)
2987 node.save()3066 node.save()
2988 self.assertEqual(3067 self.assertThat(node.get_pxe_mac_vendor(), IsNonEmptyString)
2989 "ELITEGROUP COMPUTER SYSTEMS CO., LTD.",
2990 node.get_pxe_mac_vendor())
29913068
2992 def test_get_extra_macs_returns_all_but_boot_interface_mac(self):3069 def test_get_extra_macs_returns_all_but_boot_interface_mac(self):
2993 node = factory.make_Node()3070 node = factory.make_Node()
@@ -4914,10 +4991,12 @@
4914 register_view("maasserver_discovery")4991 register_view("maasserver_discovery")
49154992
4916 def make_acquired_node_with_interface(4993 def make_acquired_node_with_interface(
4917 self, user, bmc_connected_to=None, power_type="virsh"):4994 self, user, bmc_connected_to=None, power_type="virsh",
4995 power_state=POWER_STATE.OFF):
4918 node = factory.make_Node_with_Interface_on_Subnet(4996 node = factory.make_Node_with_Interface_on_Subnet(
4919 status=NODE_STATUS.READY, with_boot_disk=True,4997 status=NODE_STATUS.READY, with_boot_disk=True,
4920 bmc_connected_to=bmc_connected_to, power_type=power_type)4998 bmc_connected_to=bmc_connected_to, power_type=power_type,
4999 power_state=power_state)
4921 node.acquire(user)5000 node.acquire(user)
4922 return node5001 return node
49235002
@@ -5035,6 +5114,24 @@
5035 node.system_id, status=old_status),5114 node.system_id, status=old_status),
5036 call(callOutToDatabase, node.release_interface_config)))5115 call(callOutToDatabase, node.release_interface_config)))
50375116
5117 def test__calls_power_cycle_when_cycling_allowed(self):
5118 user = factory.make_User()
5119 node = self.make_acquired_node_with_interface(
5120 user, power_state=POWER_STATE.ON)
5121
5122 post_commit_defer = self.patch(node_module, "post_commit")
5123 mock_power_control = self.patch(Node, "_power_control_node")
5124 mock_power_control.return_value = post_commit_defer
5125
5126 # Power cycling is allowed when starting deployment. This node is
5127 # allocated and the power_state is ON. Power cycle should be called
5128 # instead of power_on.
5129 node.start(user)
5130
5131 # Calls _power_control_node when power_cycle.
5132 self.assertThat(
5133 mock_power_control, MockCalledOnceWith(ANY, power_cycle, ANY))
5134
5038 def test_storage_layout_issues_returns_invalid_no_boot_arm64_non_efi(self):5135 def test_storage_layout_issues_returns_invalid_no_boot_arm64_non_efi(self):
5039 node = factory.make_Node(5136 node = factory.make_Node(
5040 architecture="arm64/generic", bios_boot_method="pxe")5137 architecture="arm64/generic", bios_boot_method="pxe")
@@ -5640,8 +5737,7 @@
5640 )5737 )
56415738
5642 def create_empty_controller(self):5739 def create_empty_controller(self):
5643 node = factory.make_Node(node_type=self.node_type)5740 return factory.make_Node(node_type=self.node_type).as_self()
5644 return typecast_to_node_type(node)
56455741
5646 def test__order_of_calls_to_update_interface_is_always_the_same(self):5742 def test__order_of_calls_to_update_interface_is_always_the_same(self):
5647 controller = self.create_empty_controller()5743 controller = self.create_empty_controller()
@@ -8369,7 +8465,7 @@
8369 region_and_rack = factory.make_Node(8465 region_and_rack = factory.make_Node(
8370 node_type=NODE_TYPE.REGION_AND_RACK_CONTROLLER)8466 node_type=NODE_TYPE.REGION_AND_RACK_CONTROLLER)
8371 system_id = region_and_rack.system_id8467 system_id = region_and_rack.system_id
8372 typecast_node(region_and_rack, RackController).delete()8468 region_and_rack.as_rack_controller().delete()
8373 self.assertEquals(8469 self.assertEquals(
8374 NODE_TYPE.REGION_CONTROLLER,8470 NODE_TYPE.REGION_CONTROLLER,
8375 Node.objects.get(system_id=system_id).node_type)8471 Node.objects.get(system_id=system_id).node_type)
@@ -8576,7 +8672,7 @@
8576 def test_delete_converts_region_and_rack_to_rack(self):8672 def test_delete_converts_region_and_rack_to_rack(self):
8577 region_and_rack = factory.make_Node(8673 region_and_rack = factory.make_Node(
8578 node_type=NODE_TYPE.REGION_AND_RACK_CONTROLLER)8674 node_type=NODE_TYPE.REGION_AND_RACK_CONTROLLER)
8579 typecast_node(region_and_rack, RegionController).delete()8675 region_and_rack.as_region_controller().delete()
8580 self.assertEquals(8676 self.assertEquals(
8581 NODE_TYPE.RACK_CONTROLLER,8677 NODE_TYPE.RACK_CONTROLLER,
8582 Node.objects.get(system_id=region_and_rack.system_id).node_type)8678 Node.objects.get(system_id=region_and_rack.system_id).node_type)
@@ -8776,3 +8872,25 @@
8776 self.assertThat(monitoring_state, Contains('eth2'))8872 self.assertThat(monitoring_state, Contains('eth2'))
8777 self.assertThat(8873 self.assertThat(
8778 monitoring_state['eth1'], Equals(eth1.get_discovery_state()))8874 monitoring_state['eth1'], Equals(eth1.get_discovery_state()))
8875
8876
8877class TestChassis(MAASServerTestCase):
8878
8879 def test__domain_is_always_empty(self):
8880 hostname = factory.make_hostname()
8881 domain = factory.make_name("domain")
8882 chassis = factory.make_Chassis(
8883 hostname="%s.%s" % (hostname, domain))
8884 self.assertEquals(hostname, chassis.hostname)
8885 self.assertIsNone(chassis.domain)
8886
8887
8888class TestStorage(MAASServerTestCase):
8889
8890 def test__domain_is_always_empty(self):
8891 hostname = factory.make_hostname()
8892 domain = factory.make_name("domain")
8893 storage = factory.make_Storage(
8894 hostname="%s.%s" % (hostname, domain))
8895 self.assertEquals(hostname, storage.hostname)
8896 self.assertIsNone(storage.domain)
87798897
=== modified file 'src/maasserver/models/tests/test_staticipaddress.py'
--- src/maasserver/models/tests/test_staticipaddress.py 2016-12-07 15:03:00 +0000
+++ src/maasserver/models/tests/test_staticipaddress.py 2016-12-07 15:50:52 +0000
@@ -9,11 +9,12 @@
9 randint,9 randint,
10 shuffle,10 shuffle,
11)11)
12import threading
12from unittest import skip13from unittest import skip
13from unittest.mock import sentinel14from unittest.mock import sentinel
1415
15from django.core.exceptions import ValidationError16from django.core.exceptions import ValidationError
16from django.db import transaction17from django.db import IntegrityError
17from maasserver import locks18from maasserver import locks
18from maasserver.dbviews import register_view19from maasserver.dbviews import register_view
19from maasserver.enum import (20from maasserver.enum import (
@@ -41,29 +42,39 @@
41 MAASServerTestCase,42 MAASServerTestCase,
42 MAASTransactionServerTestCase,43 MAASTransactionServerTestCase,
43)44)
44from maasserver.utils.dns import get_ip_based_hostname45<<<<<<< TREE
46from maasserver.utils.dns import get_ip_based_hostname
47=======
48from maasserver.utils import orm
49from maasserver.utils.dns import get_ip_based_hostname
50>>>>>>> MERGE-SOURCE
45from maasserver.utils.orm import (51from maasserver.utils.orm import (
46 reload_object,52 reload_object,
47 RetryTransaction,
48 transactional,53 transactional,
49)54)
50from maasserver.websockets.base import dehydrate_datetime55from maasserver.websockets.base import dehydrate_datetime
51from maastesting.matchers import (
52 MockCalledOnceWith,
53 MockNotCalled,
54)
55from netaddr import IPAddress56from netaddr import IPAddress
57from psycopg2.errorcodes import FOREIGN_KEY_VIOLATION
56from testtools import ExpectedException58from testtools import ExpectedException
57from testtools.matchers import (59from testtools.matchers import (
60 AfterPreprocessing,
61 AllMatch,
58 Contains,62 Contains,
59 Equals,63 Equals,
60 HasLength,64 HasLength,
65 Is,
66 IsInstance,
61 Not,67 Not,
62)68)
69from twisted.python.failure import Failure
6370
6471
65class TestStaticIPAddressManager(MAASServerTestCase):72class TestStaticIPAddressManager(MAASServerTestCase):
6673
74 def setUp(self):
75 super(TestStaticIPAddressManager, self).setUp()
76 register_view("maasserver_discovery")
77
67 def test_filter_by_ip_family_ipv4(self):78 def test_filter_by_ip_family_ipv4(self):
68 network_v4 = factory.make_ipv4_network()79 network_v4 = factory.make_ipv4_network()
69 subnet_v4 = factory.make_Subnet(cidr=str(network_v4.cidr))80 subnet_v4 = factory.make_Subnet(cidr=str(network_v4.cidr))
@@ -134,36 +145,21 @@
134 StaticIPAddress.objects.filter_by_subnet_cidr_family(145 StaticIPAddress.objects.filter_by_subnet_cidr_family(
135 IPADDRESS_FAMILY.IPv6))146 IPADDRESS_FAMILY.IPv6))
136147
137
138class TestStaticIPAddressManagerTransactional(MAASTransactionServerTestCase):
139 """The following TestStaticIPAddressManager tests require
140 MAASTransactionServerTestCase, and thus have been separated from the
141 TestStaticIPAddressManager above.
142 """
143
144 def setUp(self):
145 register_view("maasserver_discovery")
146 return super().setUp()
147
148 def test_allocate_new_returns_ip_in_correct_range(self):148 def test_allocate_new_returns_ip_in_correct_range(self):
149 with transaction.atomic():149 subnet = factory.make_managed_Subnet()
150 subnet = factory.make_managed_Subnet()150 ipaddress = StaticIPAddress.objects.allocate_new(subnet)
151 with transaction.atomic():
152 ipaddress = StaticIPAddress.objects.allocate_new(subnet)
153 self.assertIsInstance(ipaddress, StaticIPAddress)151 self.assertIsInstance(ipaddress, StaticIPAddress)
154 self.assertTrue(152 self.assertTrue(
155 subnet.is_valid_static_ip(ipaddress.ip),153 subnet.is_valid_static_ip(ipaddress.ip),
156 "%s: not valid for subnet with reserved IPs: %r" % (154 "%s: not valid for subnet with reserved IPs: %r" % (
157 ipaddress.ip, subnet.get_ipranges_in_use()))155 ipaddress.ip, subnet.get_ipranges_in_use()))
158156
159 @transactional
160 def test_allocate_new_allocates_IPv6_address(self):157 def test_allocate_new_allocates_IPv6_address(self):
161 subnet = factory.make_managed_ipv6_Subnet()158 subnet = factory.make_managed_Subnet(ipv6=True)
162 ipaddress = StaticIPAddress.objects.allocate_new(subnet)159 ipaddress = StaticIPAddress.objects.allocate_new(subnet)
163 self.assertIsInstance(ipaddress, StaticIPAddress)160 self.assertIsInstance(ipaddress, StaticIPAddress)
164 self.assertTrue(subnet.is_valid_static_ip(ipaddress.ip))161 self.assertTrue(subnet.is_valid_static_ip(ipaddress.ip))
165162
166 @transactional
167 def test_allocate_new_sets_user(self):163 def test_allocate_new_sets_user(self):
168 subnet = factory.make_managed_Subnet()164 subnet = factory.make_managed_Subnet()
169 user = factory.make_User()165 user = factory.make_User()
@@ -171,7 +167,6 @@
171 subnet=subnet, alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=user)167 subnet=subnet, alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=user)
172 self.assertEqual(user, ipaddress.user)168 self.assertEqual(user, ipaddress.user)
173169
174 @transactional
175 def test_allocate_new_with_user_disallows_wrong_alloc_types(self):170 def test_allocate_new_with_user_disallows_wrong_alloc_types(self):
176 subnet = factory.make_managed_Subnet()171 subnet = factory.make_managed_Subnet()
177 user = factory.make_User()172 user = factory.make_User()
@@ -185,7 +180,6 @@
185 StaticIPAddress.objects.allocate_new(180 StaticIPAddress.objects.allocate_new(
186 subnet, user=user, alloc_type=alloc_type)181 subnet, user=user, alloc_type=alloc_type)
187182
188 @transactional
189 def test_allocate_new_with_reserved_type_requires_a_user(self):183 def test_allocate_new_with_reserved_type_requires_a_user(self):
190 subnet = factory.make_managed_Subnet()184 subnet = factory.make_managed_Subnet()
191 with ExpectedException(AssertionError):185 with ExpectedException(AssertionError):
@@ -196,18 +190,15 @@
196 # Django has a bug that casts IP addresses with HOST(), which190 # Django has a bug that casts IP addresses with HOST(), which
197 # results in alphabetical comparisons of strings instead of IP191 # results in alphabetical comparisons of strings instead of IP
198 # addresses. See https://bugs.launchpad.net/maas/+bug/1338452192 # addresses. See https://bugs.launchpad.net/maas/+bug/1338452
199 with transaction.atomic():193 subnet = factory.make_Subnet(
200 subnet = factory.make_Subnet(194 cidr='10.0.0.0/24', gateway_ip='10.0.0.1')
201 cidr='10.0.0.0/24', gateway_ip='10.0.0.1')195 factory.make_IPRange(subnet, '10.0.0.2', '10.0.0.97')
202 factory.make_IPRange(subnet, '10.0.0.2', '10.0.0.97')196 factory.make_IPRange(subnet, '10.0.0.101', '10.0.0.254')
203 factory.make_IPRange(subnet, '10.0.0.101', '10.0.0.254')197 factory.make_StaticIPAddress("10.0.0.99", subnet=subnet)
204 factory.make_StaticIPAddress("10.0.0.99", subnet=subnet)198 subnet = reload_object(subnet)
205 subnet = reload_object(subnet)199 ipaddress = StaticIPAddress.objects.allocate_new(subnet)
206 with transaction.atomic():200 self.assertEqual(ipaddress.ip, "10.0.0.98")
207 ipaddress = StaticIPAddress.objects.allocate_new(subnet)
208 self.assertEqual(ipaddress.ip, "10.0.0.98")
209201
210 @transactional
211 def test_allocate_new_returns_requested_IP_if_available(self):202 def test_allocate_new_returns_requested_IP_if_available(self):
212 subnet = factory.make_Subnet(cidr='10.0.0.0/24')203 subnet = factory.make_Subnet(cidr='10.0.0.0/24')
213 ipaddress = StaticIPAddress.objects.allocate_new(204 ipaddress = StaticIPAddress.objects.allocate_new(
@@ -220,7 +211,6 @@
220 requested_address='10.0.0.1')211 requested_address='10.0.0.1')
221 self.assertEqual('10.0.0.1', ipaddress.ip)212 self.assertEqual('10.0.0.1', ipaddress.ip)
222213
223 @transactional
224 def test_allocate_new_raises_when_requested_IP_unavailable(self):214 def test_allocate_new_raises_when_requested_IP_unavailable(self):
225 subnet = factory.make_ipv4_Subnet_with_IPRanges()215 subnet = factory.make_ipv4_Subnet_with_IPRanges()
226 requested_address = StaticIPAddress.objects.allocate_new(216 requested_address = StaticIPAddress.objects.allocate_new(
@@ -235,28 +225,6 @@
235 StaticIPAddress.objects.allocate_new(225 StaticIPAddress.objects.allocate_new(
236 subnet, requested_address=requested_address)226 subnet, requested_address=requested_address)
237227
238 @transactional
239 def test_allocate_new_requests_transaction_retry_if_ip_taken(self):
240 subnet = factory.make_ipv4_Subnet_with_IPRanges()
241 # Simulate a "IP already taken" error.
242 mock_attempt_allocation = self.patch(
243 StaticIPAddress.objects, '_attempt_allocation')
244 mock_attempt_allocation.side_effect = StaticIPAddressUnavailable()
245 self.assertRaises(
246 RetryTransaction, StaticIPAddress.objects.allocate_new, subnet)
247
248 @transactional
249 def test_allocate_new_does_not_use_lock_for_requested_ip(self):
250 # When requesting a specific IP address, there's no need to
251 # acquire the lock.
252 lock = self.patch(locks, 'staticip_acquire')
253 subnet = factory.make_Subnet(cidr='10.0.0.0/24')
254 ipaddress = StaticIPAddress.objects.allocate_new(
255 subnet, requested_address='10.0.0.1')
256 self.assertIsInstance(ipaddress, StaticIPAddress)
257 self.assertThat(lock.__enter__, MockNotCalled())
258
259 @transactional
260 def test_allocate_new_raises_when_requested_IP_out_of_network(self):228 def test_allocate_new_raises_when_requested_IP_out_of_network(self):
261 subnet = factory.make_Subnet(cidr='10.0.0.0/24')229 subnet = factory.make_Subnet(cidr='10.0.0.0/24')
262 requested_address = '10.0.1.1'230 requested_address = '10.0.1.1'
@@ -275,31 +243,28 @@
275 str(e))243 str(e))
276244
277 def test_allocate_new_raises_when_requested_IP_in_dynamic_range(self):245 def test_allocate_new_raises_when_requested_IP_in_dynamic_range(self):
278 with transaction.atomic():246 subnet = factory.make_ipv4_Subnet_with_IPRanges()
279 subnet = factory.make_ipv4_Subnet_with_IPRanges()247 dynamic_range = subnet.get_dynamic_ranges().first()
280 dynamic_range = subnet.get_dynamic_ranges().first()248 requested_address = str(IPAddress(
281 requested_address = str(IPAddress(249 dynamic_range.netaddr_iprange.first))
282 dynamic_range.netaddr_iprange.first))250 dynamic_range_end = str(IPAddress(
283 dynamic_range_end = str(IPAddress(251 dynamic_range.netaddr_iprange.last))
284 dynamic_range.netaddr_iprange.last))252 subnet = reload_object(subnet)
285 subnet = reload_object(subnet)253 e = self.assertRaises(
286 with transaction.atomic():254 StaticIPAddressUnavailable,
287 e = self.assertRaises(255 StaticIPAddress.objects.allocate_new,
288 StaticIPAddressUnavailable,256 subnet, factory.pick_enum(
289 StaticIPAddress.objects.allocate_new,257 IPADDRESS_TYPE, but_not=[
290 subnet, factory.pick_enum(258 IPADDRESS_TYPE.DHCP,
291 IPADDRESS_TYPE, but_not=[259 IPADDRESS_TYPE.DISCOVERED,
292 IPADDRESS_TYPE.DHCP,260 IPADDRESS_TYPE.USER_RESERVED,
293 IPADDRESS_TYPE.DISCOVERED,261 ]),
294 IPADDRESS_TYPE.USER_RESERVED,262 requested_address=requested_address)
295 ]),263 self.assertEqual(
296 requested_address=requested_address)264 "%s is within the dynamic range from %s to %s" % (
297 self.assertEqual(265 requested_address, requested_address, dynamic_range_end),
298 "%s is within the dynamic range from %s to %s" % (266 str(e))
299 requested_address, requested_address, dynamic_range_end),
300 str(e))
301267
302 @transactional
303 def test_allocate_new_raises_when_alloc_type_is_None(self):268 def test_allocate_new_raises_when_alloc_type_is_None(self):
304 error = self.assertRaises(269 error = self.assertRaises(
305 ValueError, StaticIPAddress.objects.allocate_new,270 ValueError, StaticIPAddress.objects.allocate_new,
@@ -308,7 +273,6 @@
308 "IP address type None is not allowed to use allocate_new.",273 "IP address type None is not allowed to use allocate_new.",
309 str(error))274 str(error))
310275
311 @transactional
312 def test_allocate_new_raises_when_alloc_type_is_not_allowed(self):276 def test_allocate_new_raises_when_alloc_type_is_not_allowed(self):
313 error = self.assertRaises(277 error = self.assertRaises(
314 ValueError, StaticIPAddress.objects.allocate_new,278 ValueError, StaticIPAddress.objects.allocate_new,
@@ -317,32 +281,98 @@
317 "IP address type 5 is not allowed to use allocate_new.",281 "IP address type 5 is not allowed to use allocate_new.",
318 str(error))282 str(error))
319283
320 @transactional
321 def test_allocate_new_uses_staticip_acquire_lock(self):
322 lock = self.patch(locks, 'staticip_acquire')
323 subnet = factory.make_ipv4_Subnet_with_IPRanges()
324 ipaddress = StaticIPAddress.objects.allocate_new(subnet)
325 self.assertIsInstance(ipaddress, StaticIPAddress)
326 self.assertThat(lock.__enter__, MockCalledOnceWith())
327 self.assertThat(
328 lock.__exit__, MockCalledOnceWith(None, None, None))
329
330 def test_allocate_new_raises_when_addresses_exhausted(self):284 def test_allocate_new_raises_when_addresses_exhausted(self):
331 network = "192.168.230.0/24"285 network = "192.168.230.0/24"
332 with transaction.atomic():286 subnet = factory.make_Subnet(cidr=network)
333 subnet = factory.make_Subnet(cidr=network)287 factory.make_IPRange(
334 factory.make_IPRange(288 subnet, '192.168.230.1', '192.168.230.254',
335 subnet, '192.168.230.1', '192.168.230.254',289 type=IPRANGE_TYPE.RESERVED)
336 type=IPRANGE_TYPE.RESERVED)290 e = self.assertRaises(
337 with transaction.atomic():291 StaticIPAddressExhaustion,
338 e = self.assertRaises(292 StaticIPAddress.objects.allocate_new,
339 StaticIPAddressExhaustion,293 subnet)
340 StaticIPAddress.objects.allocate_new,
341 subnet)
342 self.assertEqual(294 self.assertEqual(
343 "No more IPs available in subnet: %s." % subnet.cidr,295 "No more IPs available in subnet: %s." % subnet.cidr,
344 str(e))296 str(e))
345297
298 def test_allocate_new_requests_retry_when_free_address_taken(self):
299 set_ip_address = self.patch(StaticIPAddress, "set_ip_address")
300 set_ip_address.side_effect = orm.make_unique_violation()
301 with orm.retry_context:
302 # A retry has been requested.
303 self.assertRaises(
304 orm.RetryTransaction, StaticIPAddress.objects.allocate_new,
305 subnet=factory.make_managed_Subnet())
306 # Aquisition of `address_allocation` is pending.
307 self.assertThat(
308 list(orm.retry_context.stack._cm_pending),
309 Equals([locks.address_allocation]))
310
311 def test_allocate_new_propagates_other_integrity_errors(self):
312 set_ip_address = self.patch(StaticIPAddress, "set_ip_address")
313 set_ip_address.side_effect = orm.make_unique_violation()
314 set_ip_address.side_effect.__cause__.pgcode = FOREIGN_KEY_VIOLATION
315 with orm.retry_context:
316 # An integrity error that's not `UNIQUE_VIOLATION` is propagated.
317 self.assertRaises(
318 IntegrityError, StaticIPAddress.objects.allocate_new,
319 subnet=factory.make_managed_Subnet())
320 # There is no pending retry context.
321 self.assertThat(
322 orm.retry_context.stack._cm_pending,
323 HasLength(0))
324
325
326class TestStaticIPAddressManagerTransactional(MAASTransactionServerTestCase):
327 """Transactional tests for `StaticIPAddressManager."""
328
329 scenarios = (
330 ("IPv4", dict(ip_version=4)),
331 ("IPv6", dict(ip_version=6)),
332 )
333
334 def test_allocate_new_works_under_extreme_concurrency(self):
335 register_view("maasserver_discovery")
336
337 ipv6 = (self.ip_version == 6)
338 subnet = factory.make_managed_Subnet(ipv6=ipv6)
339 count = 20 # Allocate this number of IP addresses.
340 concurrency = threading.Semaphore(16)
341 mutex = threading.Lock()
342 results = []
343
344 @transactional
345 def allocate():
346 return StaticIPAddress.objects.allocate_new(subnet)
347
348 def allocate_one():
349 try:
350 with concurrency:
351 sip = allocate()
352 except:
353 failure = Failure()
354 with mutex:
355 results.append(failure)
356 else:
357 with mutex:
358 results.append(sip)
359
360 threads = [
361 threading.Thread(target=allocate_one)
362 for _ in range(count)
363 ]
364
365 for thread in threads:
366 thread.start()
367 for thread in threads:
368 thread.join()
369
370 self.assertThat(results, AllMatch(IsInstance(StaticIPAddress)))
371 ips = {sip.ip for sip in results}
372 self.assertThat(ips, HasLength(count))
373 self.assertThat(ips, AllMatch(
374 AfterPreprocessing(subnet.is_valid_static_ip, Is(True))))
375
346376
347class TestStaticIPAddressManagerMapping(MAASServerTestCase):377class TestStaticIPAddressManagerMapping(MAASServerTestCase):
348 """Tests for get_hostname_ip_mapping()."""378 """Tests for get_hostname_ip_mapping()."""
349379
=== modified file 'src/maasserver/models/tests/test_subnet.py'
--- src/maasserver/models/tests/test_subnet.py 2016-10-18 00:19:51 +0000
+++ src/maasserver/models/tests/test_subnet.py 2016-12-07 15:50:52 +0000
@@ -35,6 +35,7 @@
35from maasserver.testing.factory import factory35from maasserver.testing.factory import factory
36from maasserver.testing.orm import rollback36from maasserver.testing.orm import rollback
37from maasserver.testing.testcase import MAASServerTestCase37from maasserver.testing.testcase import MAASServerTestCase
38from maasserver.utils.orm import reload_object
38from maastesting.matchers import DocTestMatches39from maastesting.matchers import DocTestMatches
39from netaddr import (40from netaddr import (
40 AddrFormatError,41 AddrFormatError,
@@ -615,7 +616,7 @@
615 parent, subnet.get_smallest_enclosing_sane_subnet())616 parent, subnet.get_smallest_enclosing_sane_subnet())
616617
617 def test_cannot_delete_with_dhcp_enabled(self):618 def test_cannot_delete_with_dhcp_enabled(self):
618 subnet = factory.make_managed_Subnet(ipv6=False)619 subnet = factory.make_ipv4_Subnet_with_IPRanges()
619 with ExpectedException(ValidationError, ".*servicing a dynamic.*"):620 with ExpectedException(ValidationError, ".*servicing a dynamic.*"):
620 subnet.delete()621 subnet.delete()
621622
@@ -898,13 +899,37 @@
898899
899class TestSubnetGetNextIPForAllocation(MAASServerTestCase):900class TestSubnetGetNextIPForAllocation(MAASServerTestCase):
900901
902 scenarios = (
903 ("managed", {'managed': True}),
904 ("unmanaged", {'managed': False}),
905 )
906
907 def make_Subnet(self, *args, **kwargs):
908 """Helper to create a subnet for this test suite.
909
910 Eclipses the entire subnet with an IPRange of type RESERVED, so that
911 unmanaged and managed test scenarios are expected to behave the same.
912 """
913 cidr = kwargs.get('cidr')
914 network = IPNetwork(cidr)
915 # Note: these tests assume IPv4.
916 first = str(IPAddress(network.first + 1))
917 last = str(IPAddress(network.last - 1))
918 subnet = factory.make_Subnet(*args, managed=self.managed, **kwargs)
919 if not self.managed:
920 factory.make_IPRange(
921 subnet, start_ip=first, end_ip=last,
922 type=IPRANGE_TYPE.RESERVED)
923 subnet = reload_object(subnet)
924 return subnet
925
901 def setUp(self):926 def setUp(self):
902 register_view("maasserver_discovery")927 register_view("maasserver_discovery")
903 return super().setUp()928 return super().setUp()
904929
905 def test__raises_if_no_free_addresses(self):930 def test__raises_if_no_free_addresses(self):
906 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.931 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
907 subnet = factory.make_Subnet(932 subnet = self.make_Subnet(
908 cidr="10.0.0.0/30", gateway_ip="10.0.0.1",933 cidr="10.0.0.0/30", gateway_ip="10.0.0.1",
909 dns_servers=["10.0.0.2"])934 dns_servers=["10.0.0.2"])
910 with ExpectedException(935 with ExpectedException(
@@ -914,35 +939,39 @@
914939
915 def test__allocates_next_free_address(self):940 def test__allocates_next_free_address(self):
916 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.941 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
917 subnet = factory.make_Subnet(942 subnet = self.make_Subnet(
918 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)943 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None,
944 )
919 ip = subnet.get_next_ip_for_allocation()945 ip = subnet.get_next_ip_for_allocation()
920 self.assertThat(ip, Equals("10.0.0.1"))946 self.assertThat(ip, Equals("10.0.0.1"))
921947
922 def test__avoids_gateway_ip(self):948 def test__avoids_gateway_ip(self):
923 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.949 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
924 subnet = factory.make_Subnet(950 subnet = self.make_Subnet(
925 cidr="10.0.0.0/30", gateway_ip="10.0.0.1", dns_servers=None)951 cidr="10.0.0.0/30", gateway_ip="10.0.0.1", dns_servers=None,
952 )
926 ip = subnet.get_next_ip_for_allocation()953 ip = subnet.get_next_ip_for_allocation()
927 self.assertThat(ip, Equals("10.0.0.2"))954 self.assertThat(ip, Equals("10.0.0.2"))
928955
929 def test__avoids_excluded_addresses(self):956 def test__avoids_excluded_addresses(self):
930 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.957 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
931 subnet = factory.make_Subnet(958 subnet = factory.make_Subnet(
932 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)959 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None,
960 )
933 ip = subnet.get_next_ip_for_allocation(exclude_addresses=["10.0.0.1"])961 ip = subnet.get_next_ip_for_allocation(exclude_addresses=["10.0.0.1"])
934 self.assertThat(ip, Equals("10.0.0.2"))962 self.assertThat(ip, Equals("10.0.0.2"))
935963
936 def test__avoids_dns_servers(self):964 def test__avoids_dns_servers(self):
937 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.965 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
938 subnet = factory.make_Subnet(966 subnet = factory.make_Subnet(
939 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=["10.0.0.1"])967 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=["10.0.0.1"],
968 )
940 ip = subnet.get_next_ip_for_allocation()969 ip = subnet.get_next_ip_for_allocation()
941 self.assertThat(ip, Equals("10.0.0.2"))970 self.assertThat(ip, Equals("10.0.0.2"))
942971
943 def test__avoids_observed_neighbours(self):972 def test__avoids_observed_neighbours(self):
944 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.973 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
945 subnet = factory.make_Subnet(974 subnet = self.make_Subnet(
946 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)975 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)
947 rackif = factory.make_Interface(vlan=subnet.vlan)976 rackif = factory.make_Interface(vlan=subnet.vlan)
948 factory.make_Discovery(ip="10.0.0.1", interface=rackif)977 factory.make_Discovery(ip="10.0.0.1", interface=rackif)
@@ -951,7 +980,7 @@
951980
952 def test__logs_if_suggests_previously_observed_neighbour(self):981 def test__logs_if_suggests_previously_observed_neighbour(self):
953 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.982 # Note: 10.0.0.0/30 --> 10.0.0.1 and 10.0.0.0.2 are usable.
954 subnet = factory.make_Subnet(983 subnet = self.make_Subnet(
955 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)984 cidr="10.0.0.0/30", gateway_ip=None, dns_servers=None)
956 rackif = factory.make_Interface(vlan=subnet.vlan)985 rackif = factory.make_Interface(vlan=subnet.vlan)
957 now = datetime.now()986 now = datetime.now()
@@ -969,7 +998,7 @@
969998
970 def test__uses_smallest_free_range_when_not_considering_neighbours(self):999 def test__uses_smallest_free_range_when_not_considering_neighbours(self):
971 # Note: 10.0.0.0/29 --> 10.0.0.1 through 10.0.0.0.6 are usable.1000 # Note: 10.0.0.0/29 --> 10.0.0.1 through 10.0.0.0.6 are usable.
972 subnet = factory.make_Subnet(1001 subnet = self.make_Subnet(
973 cidr="10.0.0.0/29", gateway_ip=None, dns_servers=None)1002 cidr="10.0.0.0/29", gateway_ip=None, dns_servers=None)
974 # With .4 in use, the free ranges are {1, 2, 3}, {5, 6}. So MAAS should1003 # With .4 in use, the free ranges are {1, 2, 3}, {5, 6}. So MAAS should
975 # select 10.0.0.5, since that is the first address in the smallest1004 # select 10.0.0.5, since that is the first address in the smallest
@@ -977,3 +1006,48 @@
977 factory.make_StaticIPAddress(ip="10.0.0.4", cidr="10.0.0.0/29")1006 factory.make_StaticIPAddress(ip="10.0.0.4", cidr="10.0.0.0/29")
978 ip = subnet.get_next_ip_for_allocation()1007 ip = subnet.get_next_ip_for_allocation()
979 self.assertThat(ip, Equals("10.0.0.5"))1008 self.assertThat(ip, Equals("10.0.0.5"))
1009
1010
1011class TestUnmanagedSubnets(MAASServerTestCase):
1012 def setUp(self):
1013 register_view("maasserver_discovery")
1014 return super().setUp()
1015
1016 def test__allocation_uses_reserved_range(self):
1017 # Note: 10.0.0.0/29 --> 10.0.0.1 through 10.0.0.0.6 are usable.
1018 subnet = factory.make_Subnet(
1019 cidr="10.0.0.0/29", gateway_ip=None, dns_servers=None,
1020 managed=False)
1021 range1 = factory.make_IPRange(
1022 subnet, start_ip='10.0.0.1', end_ip='10.0.0.1',
1023 type=IPRANGE_TYPE.RESERVED)
1024 subnet = reload_object(subnet)
1025 ip = subnet.get_next_ip_for_allocation()
1026 self.assertThat(ip, Equals("10.0.0.1"))
1027 range1.delete()
1028 factory.make_IPRange(
1029 subnet, start_ip='10.0.0.6', end_ip='10.0.0.6',
1030 type=IPRANGE_TYPE.RESERVED)
1031 subnet = reload_object(subnet)
1032 ip = subnet.get_next_ip_for_allocation()
1033 self.assertThat(ip, Equals("10.0.0.6"))
1034
1035 def test__allocation_uses_multiple_reserved_ranges(self):
1036 # Note: 10.0.0.0/29 --> 10.0.0.1 through 10.0.0.0.6 are usable.
1037 subnet = factory.make_Subnet(
1038 cidr="10.0.0.0/29", gateway_ip=None, dns_servers=None,
1039 managed=False)
1040 factory.make_IPRange(
1041 subnet, start_ip='10.0.0.3', end_ip='10.0.0.4',
1042 type=IPRANGE_TYPE.RESERVED)
1043 subnet = reload_object(subnet)
1044 ip = subnet.get_next_ip_for_allocation()
1045 self.assertThat(ip, Equals("10.0.0.3"))
1046 factory.make_StaticIPAddress(ip)
1047 ip = subnet.get_next_ip_for_allocation()
1048 self.assertThat(ip, Equals("10.0.0.4"))
1049 factory.make_StaticIPAddress(ip)
1050 with ExpectedException(
1051 StaticIPAddressExhaustion,
1052 "No more IPs available in subnet: 10.0.0.0/29."):
1053 subnet.get_next_ip_for_allocation()
9801054
=== modified file 'src/maasserver/models/tests/test_vlan.py'
--- src/maasserver/models/tests/test_vlan.py 2016-10-19 18:06:01 +0000
+++ src/maasserver/models/tests/test_vlan.py 2016-12-07 15:50:52 +0000
@@ -88,6 +88,14 @@
8888
89class TestVLAN(MAASServerTestCase):89class TestVLAN(MAASServerTestCase):
9090
91 def test_delete_relay_vlan_doesnt_delete_vlan(self):
92 relay_vlan = factory.make_VLAN()
93 vlan = factory.make_VLAN(relay_vlan=relay_vlan)
94 relay_vlan.delete()
95 vlan = reload_object(vlan)
96 self.assertIsNotNone(vlan)
97 self.assertIsNone(vlan.relay_vlan)
98
91 def test_get_name_for_default_vlan_is_untagged(self):99 def test_get_name_for_default_vlan_is_untagged(self):
92 fabric = factory.make_Fabric()100 fabric = factory.make_Fabric()
93 self.assertEqual("untagged", fabric.get_default_vlan().get_name())101 self.assertEqual("untagged", fabric.get_default_vlan().get_name())
94102
=== modified file 'src/maasserver/models/vlan.py'
--- src/maasserver/models/vlan.py 2016-10-20 19:39:48 +0000
+++ src/maasserver/models/vlan.py 2016-12-07 15:50:52 +0000
@@ -14,6 +14,7 @@
14from django.db.models import (14from django.db.models import (
15 BooleanField,15 BooleanField,
16 CharField,16 CharField,
17 deletion,
17 ForeignKey,18 ForeignKey,
18 IntegerField,19 IntegerField,
19 Manager,20 Manager,
@@ -169,6 +170,10 @@
169 'RackController', null=True, blank=True, editable=True,170 'RackController', null=True, blank=True, editable=True,
170 related_name='+')171 related_name='+')
171172
173 relay_vlan = ForeignKey(
174 'self', null=True, blank=True, editable=True,
175 related_name='relay_vlans', on_delete=deletion.SET_NULL)
176
172 def __str__(self):177 def __str__(self):
173 return "%s.%s" % (self.fabric.get_name(), self.get_name())178 return "%s.%s" % (self.fabric.get_name(), self.get_name())
174179
175180
=== modified file 'src/maasserver/node_action.py'
--- src/maasserver/node_action.py 2016-08-16 09:31:16 +0000
+++ src/maasserver/node_action.py 2016-12-07 15:50:52 +0000
@@ -230,10 +230,6 @@
230 self, enable_ssh=False, skip_networking=False,230 self, enable_ssh=False, skip_networking=False,
231 skip_storage=False):231 skip_storage=False):
232 """See `NodeAction.execute`."""232 """See `NodeAction.execute`."""
233 if self.node.power_state == POWER_STATE.ON:
234 raise NodeActionError(
235 "Unable to be commissioned because the power is currently on.")
236
237 try:233 try:
238 self.node.start_commissioning(234 self.node.start_commissioning(
239 self.user,235 self.user,
240236
=== modified file 'src/maasserver/rpc/nodes.py'
--- src/maasserver/rpc/nodes.py 2016-10-20 08:41:30 +0000
+++ src/maasserver/rpc/nodes.py 2016-12-07 15:50:52 +0000
@@ -30,6 +30,7 @@
30)30)
31from maasserver.models.timestampedmodel import now31from maasserver.models.timestampedmodel import now
32from maasserver.utils.orm import transactional32from maasserver.utils.orm import transactional
33from provisioningserver.drivers.power import PowerDriverRegistry
33from provisioningserver.rpc.exceptions import (34from provisioningserver.rpc.exceptions import (
34 CommissionNodeFailed,35 CommissionNodeFailed,
35 NodeAlreadyExists,36 NodeAlreadyExists,
@@ -66,16 +67,16 @@
66 :return: A generator yielding `dict`s.67 :return: A generator yielding `dict`s.
67 """68 """
68 five_minutes_ago = now() - timedelta(minutes=5)69 five_minutes_ago = now() - timedelta(minutes=5)
6970 queryable_power_types = [
70 # This is meant to be temporary until all the power types support querying71 driver.name
71 # the power state of a node. See the definition of QUERY_POWER_TYPES for72 for _, driver in PowerDriverRegistry
72 # more information.73 if driver.queryable
73 from provisioningserver.power import QUERY_POWER_TYPES74 ]
7475
75 nodes_unchecked = (76 nodes_unchecked = (
76 nodes77 nodes
77 .filter(power_state_queried=None)78 .filter(power_state_queried=None)
78 .filter(bmc__power_type__in=QUERY_POWER_TYPES)79 .filter(bmc__power_type__in=queryable_power_types)
79 .exclude(status=NODE_STATUS.BROKEN)80 .exclude(status=NODE_STATUS.BROKEN)
80 .distinct()81 .distinct()
81 )82 )
@@ -83,7 +84,7 @@
83 nodes84 nodes
84 .exclude(power_state_queried=None)85 .exclude(power_state_queried=None)
85 .exclude(power_state_queried__gt=five_minutes_ago)86 .exclude(power_state_queried__gt=five_minutes_ago)
86 .filter(bmc__power_type__in=QUERY_POWER_TYPES)87 .filter(bmc__power_type__in=queryable_power_types)
87 .exclude(status=NODE_STATUS.BROKEN)88 .exclude(status=NODE_STATUS.BROKEN)
88 .order_by("power_state_queried", "system_id")89 .order_by("power_state_queried", "system_id")
89 .distinct()90 .distinct()
9091
=== modified file 'src/maasserver/rpc/rackcontrollers.py'
--- src/maasserver/rpc/rackcontrollers.py 2016-10-17 06:42:10 +0000
+++ src/maasserver/rpc/rackcontrollers.py 2016-12-07 15:50:52 +0000
@@ -26,7 +26,6 @@
26 RegionController,26 RegionController,
27 StaticIPAddress,27 StaticIPAddress,
28)28)
29from maasserver.models.node import typecast_node
30from maasserver.models.timestampedmodel import now29from maasserver.models.timestampedmodel import now
31from maasserver.utils import synchronised30from maasserver.utils import synchronised
32from maasserver.utils.orm import (31from maasserver.utils.orm import (
@@ -120,7 +119,7 @@
120 node.node_type = NODE_TYPE.RACK_CONTROLLER119 node.node_type = NODE_TYPE.RACK_CONTROLLER
121 node.save()120 node.save()
122121
123 rackcontroller = typecast_node(node, RackController)122 rackcontroller = node.as_rack_controller()
124123
125 # Update `rackcontroller.url` from the given URL, if it has changed.124 # Update `rackcontroller.url` from the given URL, if it has changed.
126 update_fields = []125 update_fields = []
127126
=== modified file 'src/maasserver/rpc/regionservice.py'
--- src/maasserver/rpc/regionservice.py 2016-10-28 15:58:32 +0000
+++ src/maasserver/rpc/regionservice.py 2016-12-07 15:50:52 +0000
@@ -642,19 +642,17 @@
642 # and into the database.642 # and into the database.
643 self.ident = rack_controller.system_id643 self.ident = rack_controller.system_id
644 self.factory.service._addConnectionFor(self.ident, self)644 self.factory.service._addConnectionFor(self.ident, self)
645
646 # A local rack is treated differently to one that's remote.
645 self.host = self.transport.getHost()647 self.host = self.transport.getHost()
646 self.hostIsRemote = isinstance(648 self.hostIsRemote = isinstance(
647 self.host, (IPv4Address, IPv6Address))649 self.host, (IPv4Address, IPv6Address))
648650
649 # Get the region ID if we're dealing with a non-local rack; we651 # Only register the connection into the database when it's a valid
650 # won't need to bother for local racks.
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: