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