Merge lp:~andreserl/maas/lp1591093_1.9 into lp:~maas-committers/maas/trunk

Proposed by Andres Rodriguez
Status: Superseded
Proposed branch: lp:~andreserl/maas/lp1591093_1.9
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 18336 lines (+14712/-206) (has conflicts)
118 files modified
Makefile (+2/-2)
contrib/maas-http.conf (+1/-1)
contrib/preseeds_v2/enlist_userdata (+39/-4)
docs/changelog.rst (+395/-0)
etc/maas/drivers.yaml (+2/-2)
src/maasserver/api/devices.py (+133/-0)
src/maasserver/api/interfaces.py (+96/-0)
src/maasserver/api/support.py (+24/-0)
src/maasserver/api/tests/test_devices.py (+421/-0)
src/maasserver/api/tests/test_filestorage.py (+33/-0)
src/maasserver/api/tests/test_interfaces.py (+207/-0)
src/maasserver/api/tests/test_pxeconfig.py.OTHER (+693/-0)
src/maasserver/api/tests/test_support.py (+75/-0)
src/maasserver/api/tests/test_version.py (+32/-1)
src/maasserver/api/tests/test_vlans.py (+7/-0)
src/maasserver/api/tests/test_volume_groups.py (+15/-0)
src/maasserver/api/vlans.py (+10/-0)
src/maasserver/forms.py (+14/-1)
src/maasserver/forms_interface.py (+73/-0)
src/maasserver/forms_subnet.py (+26/-0)
src/maasserver/forms_vlan.py (+6/-0)
src/maasserver/models/__init__.py (+23/-0)
src/maasserver/models/fabric.py (+41/-10)
src/maasserver/models/interface.py (+272/-0)
src/maasserver/models/migrations/create_default_storage_layout.py (+201/-97)
src/maasserver/models/migrations/tests/test_create_default_storage_layout.py (+150/-0)
src/maasserver/models/node.py (+73/-0)
src/maasserver/models/nodegroupinterface.py.OTHER (+764/-0)
src/maasserver/models/signals/interfaces.py (+78/-0)
src/maasserver/models/signals/tests/test_interfaces.py (+49/-0)
src/maasserver/models/space.py (+32/-0)
src/maasserver/models/staticipaddress.py (+100/-0)
src/maasserver/models/subnet.py (+17/-0)
src/maasserver/models/tests/test_fabric.py (+30/-0)
src/maasserver/models/tests/test_filesystem.py (+4/-0)
src/maasserver/models/tests/test_filesystemgroup.py (+41/-0)
src/maasserver/models/tests/test_interface.py (+550/-0)
src/maasserver/models/tests/test_node.py (+120/-0)
src/maasserver/models/tests/test_nodegroupinterface.py.OTHER (+939/-0)
src/maasserver/models/tests/test_partition.py (+28/-0)
src/maasserver/models/tests/test_partitiontable.py (+67/-19)
src/maasserver/models/tests/test_space.py (+4/-0)
src/maasserver/models/tests/test_staticipaddress.py (+133/-0)
src/maasserver/models/tests/test_virtualblockdevice.py (+34/-0)
src/maasserver/start_up.py (+41/-0)
src/maasserver/static/css/maas-styles.css (+1/-0)
src/maasserver/static/js/angular/controllers/add_hardware.js (+11/-0)
src/maasserver/static/js/angular/controllers/node_details_storage.js (+27/-0)
src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js (+28/-0)
src/maasserver/static/js/angular/maas.js (+14/-0)
src/maasserver/static/partials/node-details.html (+173/-0)
src/maasserver/static/scss/maas/components/_tables.scss (+45/-0)
src/maasserver/testing/factory.py (+145/-0)
src/maasserver/tests/test_auth.py (+31/-0)
src/maasserver/tests/test_forms_blockdevice.py (+5/-0)
src/maasserver/tests/test_forms_interface.py (+45/-0)
src/maasserver/tests/test_forms_partition.py (+5/-0)
src/maasserver/tests/test_forms_vlan.py (+12/-0)
src/maasserver/tests/test_middleware.py (+26/-0)
src/maasserver/tests/test_node_constraint_filter_forms.py (+61/-0)
src/maasserver/tests/test_preseed_storage.py (+222/-0)
src/maasserver/tests/test_storage_layouts.py (+146/-0)
src/maasserver/triggers/tests/test_websocket_listener.py (+76/-0)
src/maasserver/triggers/websocket.py (+29/-0)
src/maasserver/urls_api.py (+54/-8)
src/maasserver/utils/converters.py (+5/-0)
src/maasserver/utils/orm.py (+25/-0)
src/maasserver/utils/tests/test_converters.py (+43/-20)
src/maasserver/websockets/handlers/node.py (+4/-0)
src/maasserver/websockets/handlers/tests/test_node.py.OTHER (+2292/-0)
src/metadataserver/models/commissioningscript.py (+235/-0)
src/metadataserver/models/tests/test_commissioningscript.py (+550/-0)
src/provisioningserver/config.py (+85/-0)
src/provisioningserver/dhcp/leases_parser.py.OTHER (+245/-0)
src/provisioningserver/dhcp/tests/test_leases_parser.py.OTHER (+680/-0)
src/provisioningserver/dhcp/tests/test_omshell.py (+5/-0)
src/provisioningserver/drivers/hardware/apc.py.OTHER (+107/-0)
src/provisioningserver/drivers/hardware/hmc.py.OTHER (+112/-0)
src/provisioningserver/drivers/hardware/mscm.py.OTHER (+230/-0)
src/provisioningserver/drivers/hardware/tests/test_apc.py.OTHER (+182/-0)
src/provisioningserver/drivers/hardware/tests/test_mscm.py.OTHER (+389/-0)
src/provisioningserver/drivers/osystem/tests/test_ubuntu.py (+15/-1)
src/provisioningserver/drivers/osystem/ubuntu.py (+9/-1)
src/provisioningserver/drivers/power/amt.py (+11/-0)
src/provisioningserver/drivers/power/apc.py (+33/-0)
src/provisioningserver/drivers/power/dli.py (+27/-1)
src/provisioningserver/drivers/power/ether_wake.py.OTHER (+34/-0)
src/provisioningserver/drivers/power/fence_cdu.py (+11/-0)
src/provisioningserver/drivers/power/hmc.py (+36/-0)
src/provisioningserver/drivers/power/ipmi.py (+55/-1)
src/provisioningserver/drivers/power/moonshot.py (+24/-5)
src/provisioningserver/drivers/power/mscm.py (+69/-0)
src/provisioningserver/drivers/power/msftocs.py (+41/-0)
src/provisioningserver/drivers/power/tests/test_amt.py (+17/-0)
src/provisioningserver/drivers/power/tests/test_apc.py (+58/-0)
src/provisioningserver/drivers/power/tests/test_base.py (+21/-0)
src/provisioningserver/drivers/power/tests/test_dli.py (+23/-0)
src/provisioningserver/drivers/power/tests/test_ether_wake.py.OTHER (+51/-0)
src/provisioningserver/drivers/power/tests/test_fence_cdu.py (+15/-0)
src/provisioningserver/drivers/power/tests/test_hmc.py (+64/-0)
src/provisioningserver/drivers/power/tests/test_ipmi.py (+195/-4)
src/provisioningserver/drivers/power/tests/test_moonshot.py (+68/-28)
src/provisioningserver/drivers/power/tests/test_mscm.py (+92/-0)
src/provisioningserver/drivers/power/tests/test_msftocs.py (+67/-0)
src/provisioningserver/drivers/power/tests/test_vmware.py (+5/-0)
src/provisioningserver/network.py.OTHER (+355/-0)
src/provisioningserver/pserv_services/tests/test_tftp.py (+26/-0)
src/provisioningserver/rpc/dhcp.py (+151/-0)
src/provisioningserver/rpc/tests/test_dhcp.py (+255/-0)
src/provisioningserver/templates/dhcp/dhcpd.conf.template (+30/-0)
src/provisioningserver/templates/dhcp/dhcpd6.conf.template (+25/-0)
src/provisioningserver/testing/config.py (+5/-0)
src/provisioningserver/tests/test_cluster_config_command.py (+10/-0)
src/provisioningserver/tests/test_config.py (+87/-0)
src/provisioningserver/utils/tests/test_ipaddr.py (+106/-0)
src/provisioningserver/utils/tests/test_twisted.py (+7/-0)
src/provisioningserver/utils/twisted.py (+101/-0)
utilities/remote-reinstall (+73/-0)
Text conflict in contrib/preseeds_v2/enlist_userdata
Text conflict in docs/changelog.rst
Text conflict in src/maasserver/api/devices.py
Text conflict in src/maasserver/api/interfaces.py
Text conflict in src/maasserver/api/support.py
Text conflict in src/maasserver/api/tests/test_devices.py
Text conflict in src/maasserver/api/tests/test_filestorage.py
Text conflict in src/maasserver/api/tests/test_interfaces.py
Contents conflict in src/maasserver/api/tests/test_pxeconfig.py
Text conflict in src/maasserver/api/tests/test_support.py
Text conflict in src/maasserver/api/tests/test_version.py
Text conflict in src/maasserver/api/tests/test_vlans.py
Text conflict in src/maasserver/api/tests/test_volume_groups.py
Text conflict in src/maasserver/api/vlans.py
Text conflict in src/maasserver/forms.py
Text conflict in src/maasserver/forms_interface.py
Text conflict in src/maasserver/forms_subnet.py
Text conflict in src/maasserver/forms_vlan.py
Text conflict in src/maasserver/models/__init__.py
Text conflict in src/maasserver/models/fabric.py
Text conflict in src/maasserver/models/interface.py
Text conflict in src/maasserver/models/migrations/create_default_storage_layout.py
Text conflict in src/maasserver/models/migrations/tests/test_create_default_storage_layout.py
Text conflict in src/maasserver/models/node.py
Contents conflict in src/maasserver/models/nodegroupinterface.py
Text conflict in src/maasserver/models/signals/interfaces.py
Text conflict in src/maasserver/models/signals/tests/test_interfaces.py
Text conflict in src/maasserver/models/space.py
Text conflict in src/maasserver/models/staticipaddress.py
Text conflict in src/maasserver/models/subnet.py
Text conflict in src/maasserver/models/tests/test_fabric.py
Text conflict in src/maasserver/models/tests/test_filesystem.py
Text conflict in src/maasserver/models/tests/test_filesystemgroup.py
Text conflict in src/maasserver/models/tests/test_interface.py
Text conflict in src/maasserver/models/tests/test_node.py
Contents conflict in src/maasserver/models/tests/test_nodegroupinterface.py
Text conflict in src/maasserver/models/tests/test_partition.py
Text conflict in src/maasserver/models/tests/test_partitiontable.py
Text conflict in src/maasserver/models/tests/test_space.py
Text conflict in src/maasserver/models/tests/test_staticipaddress.py
Text conflict in src/maasserver/models/tests/test_virtualblockdevice.py
Text conflict in src/maasserver/start_up.py
Conflict adding file src/maasserver/static/css/maas-styles.css.  Moved existing file to src/maasserver/static/css/maas-styles.css.moved.
Text conflict in src/maasserver/static/js/angular/controllers/add_hardware.js
Text conflict in src/maasserver/static/js/angular/controllers/node_details_storage.js
Text conflict in src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js
Text conflict in src/maasserver/static/js/angular/maas.js
Text conflict in src/maasserver/static/partials/node-details.html
Text conflict in src/maasserver/static/scss/maas/components/_tables.scss
Text conflict in src/maasserver/testing/factory.py
Text conflict in src/maasserver/tests/test_auth.py
Text conflict in src/maasserver/tests/test_forms_blockdevice.py
Text conflict in src/maasserver/tests/test_forms_interface.py
Text conflict in src/maasserver/tests/test_forms_partition.py
Text conflict in src/maasserver/tests/test_forms_vlan.py
Text conflict in src/maasserver/tests/test_middleware.py
Text conflict in src/maasserver/tests/test_node_constraint_filter_forms.py
Text conflict in src/maasserver/tests/test_preseed_storage.py
Text conflict in src/maasserver/tests/test_storage_layouts.py
Text conflict in src/maasserver/triggers/tests/test_websocket_listener.py
Text conflict in src/maasserver/triggers/websocket.py
Text conflict in src/maasserver/urls_api.py
Text conflict in src/maasserver/utils/converters.py
Text conflict in src/maasserver/utils/orm.py
Text conflict in src/maasserver/utils/tests/test_converters.py
Text conflict in src/maasserver/websockets/handlers/node.py
Contents conflict in src/maasserver/websockets/handlers/tests/test_node.py
Text conflict in src/metadataserver/models/commissioningscript.py
Text conflict in src/metadataserver/models/tests/test_commissioningscript.py
Text conflict in src/provisioningserver/config.py
Contents conflict in src/provisioningserver/dhcp/leases_parser.py
Contents conflict in src/provisioningserver/dhcp/tests/test_leases_parser.py
Text conflict in src/provisioningserver/dhcp/tests/test_omshell.py
Contents conflict in src/provisioningserver/drivers/hardware/apc.py
Contents conflict in src/provisioningserver/drivers/hardware/hmc.py
Contents conflict in src/provisioningserver/drivers/hardware/mscm.py
Contents conflict in src/provisioningserver/drivers/hardware/tests/test_apc.py
Contents conflict in src/provisioningserver/drivers/hardware/tests/test_mscm.py
Text conflict in src/provisioningserver/drivers/osystem/tests/test_ubuntu.py
Text conflict in src/provisioningserver/drivers/osystem/ubuntu.py
Text conflict in src/provisioningserver/drivers/power/amt.py
Text conflict in src/provisioningserver/drivers/power/apc.py
Text conflict in src/provisioningserver/drivers/power/dli.py
Contents conflict in src/provisioningserver/drivers/power/ether_wake.py
Text conflict in src/provisioningserver/drivers/power/fence_cdu.py
Text conflict in src/provisioningserver/drivers/power/hmc.py
Text conflict in src/provisioningserver/drivers/power/ipmi.py
Text conflict in src/provisioningserver/drivers/power/moonshot.py
Text conflict in src/provisioningserver/drivers/power/mscm.py
Text conflict in src/provisioningserver/drivers/power/msftocs.py
Text conflict in src/provisioningserver/drivers/power/tests/test_amt.py
Text conflict in src/provisioningserver/drivers/power/tests/test_apc.py
Text conflict in src/provisioningserver/drivers/power/tests/test_base.py
Text conflict in src/provisioningserver/drivers/power/tests/test_dli.py
Contents conflict in src/provisioningserver/drivers/power/tests/test_ether_wake.py
Text conflict in src/provisioningserver/drivers/power/tests/test_fence_cdu.py
Text conflict in src/provisioningserver/drivers/power/tests/test_hmc.py
Text conflict in src/provisioningserver/drivers/power/tests/test_ipmi.py
Text conflict in src/provisioningserver/drivers/power/tests/test_moonshot.py
Text conflict in src/provisioningserver/drivers/power/tests/test_mscm.py
Text conflict in src/provisioningserver/drivers/power/tests/test_msftocs.py
Text conflict in src/provisioningserver/drivers/power/tests/test_vmware.py
Contents conflict in src/provisioningserver/network.py
Text conflict in src/provisioningserver/pserv_services/tests/test_tftp.py
Text conflict in src/provisioningserver/rpc/dhcp.py
Text conflict in src/provisioningserver/rpc/tests/test_dhcp.py
Text conflict in src/provisioningserver/templates/dhcp/dhcpd.conf.template
Text conflict in src/provisioningserver/templates/dhcp/dhcpd6.conf.template
Text conflict in src/provisioningserver/testing/config.py
Text conflict in src/provisioningserver/tests/test_cluster_config_command.py
Text conflict in src/provisioningserver/tests/test_config.py
Text conflict in src/provisioningserver/utils/tests/test_ipaddr.py
Text conflict in src/provisioningserver/utils/tests/test_twisted.py
Text conflict in src/provisioningserver/utils/twisted.py
Text conflict in utilities/remote-reinstall
To merge this branch: bzr merge lp:~andreserl/maas/lp1591093_1.9
Reviewer Review Type Date Requested Status
Andres Rodriguez (community) Approve
Review via email: mp+297090@code.launchpad.net

This proposal has been superseded by a proposal from 2016-06-10.

Commit message

Use the correct repository URL for hpdsa in drivers.yaml.

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2016-05-25 16:48:25 +0000
3+++ Makefile 2016-06-10 16:34:39 +0000
4@@ -527,8 +527,8 @@
5 # this.
6
7 # Old names.
8-PACKAGING := $(abspath ../packaging.trunk)
9-PACKAGING_BRANCH := lp:~maas-maintainers/maas/packaging
10+PACKAGING := $(abspath ../packaging-1.9)
11+PACKAGING_BRANCH := lp:~maas-maintainers/maas/packaging-1.9
12
13 packaging-tree = $(PACKAGING)
14 packaging-branch = $(PACKAGING_BRANCH)
15
16=== modified file 'contrib/maas-http.conf'
17--- contrib/maas-http.conf 2016-03-28 13:54:47 +0000
18+++ contrib/maas-http.conf 2016-06-10 16:34:39 +0000
19@@ -26,7 +26,7 @@
20
21 <IfModule proxy_module>
22 ProxyPreserveHost on
23- ProxyPass /MAAS/ws "ws://localhost:5240/MAAS/ws"
24+ ProxyPass /MAAS/ws "ws://localhost:5240/MAAS/ws" disablereuse=on
25 ProxyPass /MAAS/static/ !
26 ProxyPass /MAAS/ http://localhost:5240/MAAS/
27 ProxyPass /MAAS http://localhost:5240/MAAS/
28
29=== modified file 'contrib/preseeds_v2/enlist_userdata'
30--- contrib/preseeds_v2/enlist_userdata 2016-04-22 17:44:18 +0000
31+++ contrib/preseeds_v2/enlist_userdata 2016-06-10 16:34:39 +0000
32@@ -159,10 +159,45 @@
33 sleep 60
34 [ -e $bfile ] && exit 0
35 fi
36-
37-
38-
39-packages: [ freeipmi-tools, openipmi, ipmitool, archdetect-deb ]
40+<<<<<<< TREE
41+
42+
43+
44+packages: [ freeipmi-tools, openipmi, ipmitool, archdetect-deb ]
45+=======
46+ - &write_poweroff_job |
47+ cat >/etc/init/maas-poweroff.conf <<EOF
48+ description "poweroff when maas task is done"
49+ start on stopped cloud-final
50+ console output
51+ task
52+ script
53+ [ ! -e /tmp/block-poweroff ] || exit 0
54+ poweroff
55+ end script
56+ EOF
57+ # reload required due to lack of inotify in overlayfs (LP: #882147)
58+ initctl reload-configuration
59+ - &write_systemd_poweroff_job |
60+ cat >/lib/systemd/system/maas-poweroff.service <<EOF
61+ [Unit]
62+ Description=Poweroff when maas task is done
63+ Wants=cloud-final.service
64+ After=cloud-final.service
65+ ConditionPathExists=!/tmp/block-poweroff
66+
67+ [Service]
68+ ExecStart=/sbin/poweroff
69+ EOF
70+ # reload required due to lack of inotify in overlayfs (LP: #882147)
71+ if [ -d /run/systemd/system ]; then
72+ systemctl daemon-reload
73+ fi
74+
75+
76+
77+packages: [ freeipmi-tools, openipmi, ipmitool, archdetect-deb ]
78+>>>>>>> MERGE-SOURCE
79 output: {all: '| tee -a /var/log/cloud-init-output.log'}
80 runcmd:
81 - [ sh, -c, *maas_enlist ]
82
83=== modified file 'docs/changelog.rst'
84--- docs/changelog.rst 2016-06-04 14:37:29 +0000
85+++ docs/changelog.rst 2016-06-10 16:34:39 +0000
86@@ -2,6 +2,7 @@
87 Changelog
88 =========
89
90+<<<<<<< TREE
91 2.0.0 (beta6)
92 =============
93
94@@ -1155,6 +1156,400 @@
95
96 LP: #1510457 [UI] No error message if there is no boot and/or root disk configured for a node.
97
98+=======
99+1.9.4
100+=====
101+
102+LP: #1584850 [1.9] DNS record added for non-boot interface IP when no address of that family exists on the boot interface
103+
104+LP: #1583715 [1.9] Ensure that restricted resources also perform meaningful authentication of clients.
105+
106+LP: #1584211 [1.9] Exclude RAM, floppy, and loopback devices from lsblk during commissioning.
107+
108+LP: #1585400 [1.9] Change detect_missing_packages in moonshot power driver to look for `ipmitool` instead of `ipmipower`
109+
110+LP: #1581318 [1.9] Append version to templateUrl in maas.js angular code.
111+
112+LP: #1591093 [2.0,1.9] 3rd party HP drivers (archive hostname renamed) - deployment fails
113+
114+
115+1.9.3
116+=====
117+
118+See https://launchpad.net/maas/+milestone/1.9.3 for full details.
119+
120+Bug Fix Update
121+--------------
122+
123+LP: #1521618 [1.9] wrong subnet in DHCP answer when multiple networks are present
124+
125+LP: #1536604 [1.9] IntegrityError while uploading leases - when there are reserved IP's on the dynamic range
126+
127+LP: #1580712 [1.9] dhcp update error: str object has no attribute mac
128+
129+LP: #1575567 [1.9] Re-commissioning doesn't detect storage changes
130+
131+LP: #1576194 [1.9] Enlistment via DHCP fails because DNS has bogus PTR record
132+
133+
134+1.9.2
135+=====
136+
137+See https://launchpad.net/maas/+milestone/1.9.2 for full details.
138+
139+Bug Fix Update
140+--------------
141+
142+LP: #1573219 Deleting user in UI leads to 500
143+
144+LP: #1508741 IPMI driver does not handle timeouts correctly
145+
146+LP: #1572070 MAAS 2.0 cannot link physical device interfaces to tagged vlans, breaking juju 2.0 multi-NIC containers
147+
148+LP: #1573046 14.04 images not available for commissioning as distrio-info --lts now reports xenial
149+
150+LP: #1571563 Can't override built in partitioning
151+
152+LP: #1552923 API allows attaching physical, bond interface to VLAN with a known tag (Inconsistent with UI)
153+
154+LP: #1566336 MAAS keeps IPs assigned to eth0, even after eth0 is enslaved into a bond
155+
156+LP: #1543195 unable to set mtu on default VLAN
157+
158+LP: #1560693 Migration 0188 dist-upgrade update failure
159+
160+LP: #1554747 CPU Utilization of postgresql thread reaches 100% for deleting a node from MaaS
161+
162+LP: #1499934 Power state could not be queried (vmware)
163+
164+LP: #1543707 MAAS 1.9+ should not allow whitespace characters in space names
165+
166+LP: #1543968 MAAS 1.9.0 allows non-unique space names
167+
168+LP: #1567213 devices results missing interface_set
169+
170+LP: #1568051 ThreadPool context entry failure causes thread pool to break
171+
172+LP: #1212205 get_file_by_name does not check owner
173+
174+LP: #1298772 MAAS API vulnerable to CSRF attack
175+
176+LP: #1379826 uuid.uuid1() is not suitable as an unguessable identifier/token
177+
178+LP: #1573264 Enlistment fails: archdetect not found.
179+
180+LP: #1556219 Discover correct IPMI driver in Power8.
181+
182+
183+1.9.1
184+=====
185+
186+See https://launchpad.net/maas/+milestone/1.9.1 for full details.
187+
188+Bug Fix Update
189+--------------
190+
191+LP: #1523779 Fix grub-install error on deploying power8 machines.
192+
193+LP: #1526542 Skip block devices with duplicate serial numbers to fix multipath issue.
194+
195+LP: #1532262 Fix failure to power query requests for SM15K servers.
196+
197+LP: #1484696 Fix bug in apache2 maas config where it will reuse websocket connections to work around a bug in apache2 itself.
198+
199+
200+1.9.0
201+=====
202+
203+Important announcements
204+-----------------------
205+
206+**New Networking Concepts and API's: Fabrics, Spaces and Subnets**
207+ With the introduction of new MAAS networking concepts, new API's are also
208+ been introduced. These are:
209+
210+ * fabrics
211+ * spaces
212+ * subnets
213+ * vlans
214+ * fan-networks
215+
216+ MAAS 1.9.0 will continue to provide backwards compatibility with the old
217+ network API for reading purposes, but moving forward, users are required to
218+ use the new API to manipulate fabrics, spaces and subnets.
219+
220+**Advanced Network and Storage Configuration only available for Ubuntu deployments**
221+ Users can now perform advanced network and storage configurations for nodes
222+ before deployment. The advanced configuration is only available for Ubuntu
223+ deployments. All other deployments using third party OS', including CentOS,
224+ RHEL, Windows and Custom Images, won't result in such configuration.
225+
226+**Re-commissioning required for upgraded MAAS**
227+ Now that storage partitioning and advanced configuration is supported natively,
228+ VM nodes in MAAS need to be re-commissioned.
229+
230+ * If upgrading from MAAS 1.8, only VM nodes with VirtIO storage devices need
231+ to be re-commissioned.
232+
233+ * If upgrading from MAAS 1.7, all nodes will need to be re-commissioned in
234+ order for MAAS to correctly capture the storage and networking devices.
235+
236+ This does not affect nodes that are currently deployed.
237+
238+**Default Storage Partitioning Layout - Flat**
239+ With the introduction of custom storage, MAAS has also introduced the concept
240+ of partitioning layouts. Partitioning layouts allow the user to quickly
241+ auto-configure the disk partitioning scheme after first commissioning or
242+ re-commissioning (if selected to do so). The partitioning layouts are set
243+ globally on the `Settings` page.
244+
245+ The current default Partitioning layout is 'Flat', maintaining backwards
246+ compatibility with previous MAAS releases. This means MAAS will take the
247+ first disk it finds in the system and use it as the root and boot disk.
248+
249+**Deployment with configured /etc/network/interfaces**
250+ Starting with MAAS 1.9, all node deployments will result in writing
251+ `/etc/network/interfaces` statically, by default. This increases MAAS'
252+ robustness and reliability as users no longer have to depend on DHCP for
253+ IP address allocation solely.
254+
255+ MAAS will continue to provide IP addresses via DHCP, even though interfaces
256+ in `/etc/network/interfaces` may have been configured statically.
257+
258+Major new features
259+------------------
260+
261+**Storage Partitioning and Advanced Configuration**
262+ MAAS now supports Storage Partitioning and Advanced Configuration natively.
263+ This allows MAAS to deploy machines with different Storage Layouts, as
264+ well as different complex partitioning configurations. Storage support
265+ includes:
266+
267+ * LVM
268+ * Bcache
269+ * Software RAID levels 0, 1, 5, 6, 10.
270+ * Advanced partitioning
271+
272+ Storace configuration is available both via the WebUI and API. For more
273+ information refer to :ref:`storage`.
274+
275+**Advanced Networking (Fabrics, Spaces, Subnetworks) and Node Network Configuration**
276+ MAAS now supports Advanced Network configuration, allowing users to not
277+ only perform advanced node network configuration, but also allowing users
278+ to declare and map their infrastructure in the form of Fabrics, VLANs,
279+ Spaces and Subnets.
280+
281+ **Fabrics, Spaces, Subnets and Fan networks**
282+ MAAS now supports the concept of Fabrics, Spaces, Subnets and FANS,
283+ which introduce a whole new way of declaring and mapping your network
284+ and infrastructure in MAAS.
285+
286+ The MAAS WebUI allows users to view all the declared Fabrics, Spaces,
287+ VLANs inside fabrics and Subnets inside Spaces. The WebUI does not yet
288+ support the ability to create new of these, but the API does.
289+
290+ These new concepts replace the old `Network` concepts from MAAS'
291+ earlier versions. For more information, see :ref:`networking`.
292+
293+ For more information about the API, see :ref:`api`.
294+
295+ **Advanced Node Networking Configuration**
296+ MAAS can now perform the Node's networking configuration. Doing so,
297+ results in `/etc/network/interfaces` being written. Advanced
298+ configuration includes:
299+
300+ * Assign subnets, fabrics, and IP to interfaces.
301+ * Create VLAN interfaces.
302+ * Create bond interfaces.
303+ * Change interface names.
304+
305+ MAAS also allows configuration of node interfaces in different modes:
306+
307+ * Auto Assign - Node interface will be configured statically
308+ and MAAS will auto assign an IP address.
309+ * DHCP - The node interface will be configured to DHCP.
310+ * Static - The user will be able to specify what IP address the
311+ interface will obtain, while MAAS will configure it statically.
312+ * Unconfigured - MAAS will leave the interface with LINK UP.
313+
314+**Curtin & cloud-init status updates**
315+ Starting from MAAS 1.9.0, curtin and cloud-init will now send messages
316+ to MAAS providing information regarding various of the actions being
317+ taken. This information will be displayed in MAAS in the `Node Event Log`.
318+
319+ Note that this information is only available when using MAAS 1.9.0 and
320+ the latest version fo curtin. For cloud-init messages this information
321+ is only available when deploying Wily+.
322+
323+**Fabric and subnet creation**
324+ MAAS now auto-creates multiple fabrics per physical interface connected
325+ to the Cluster Controller, and will correctly create subnetworks under
326+ each fabric, as well as VLAN's, if any of the Cluster Controller
327+ interface is a VLAN interface.
328+
329+**HWE Kernels**
330+ MAAS now has a different approach to deploying Hardware Enablement
331+ Kernels. Start from MAAS 1.9, the HWE kernels are no longer coupled
332+ to subarchitectures of a machine. For each Ubuntu release, users
333+ will be able to select any of the available HWE kernels for such
334+ release, as well as set the minimum kernel the machine will be
335+ deployed with by default.
336+
337+ For more information, see :ref:`hardware-enablement-kernels`.
338+
339+**CentOS images can be imported automatically**
340+ CentOS Image (CentOS 6 and 7) can now be imported automatically from the
341+ MAAS Images page. These images are currently part of the daily streams.
342+
343+ In order to test this images, you need to use the daily image stream.
344+ This can be changed in the `Settings` page under `Boot Images` to
345+ `http://maas.ubuntu.com/images/ephemeral-v2/daily/`. Once changed, images
346+ can be imported from the MAAS Images page. The CentOS image will be
347+ published in the Releases stream shortly.
348+
349+
350+Minor notable changes
351+---------------------
352+
353+**Minimal Config Files for Daemons**
354+ Starting from MAAS 1.9, minimal configuration files have been introduced
355+ for both, the MAAS Region Controller and the MAAS Cluster Controller daemons.
356+
357+ * The Region Controller (`maas-regiond`) has now dropped the usage of
358+ `/etc/maas/maas_local_settings.py` in favor of `/etc/maas/regiond.conf`.
359+ Available configuration options are now `database_host`, `database_name`,
360+ `database_user`, `database_pass`, `maas_url`. MAAS will attempt to migrate
361+ any configuration on upgrade, otherwise it will use sane defaults.
362+
363+ * The Cluster Controller (`maas-clusterd`) has now dropped the usage of
364+ `/etc/maas/pserv.yaml` and `/etc/maas/maas_cluster.conf` in favor of
365+ `/etc/maas/clusterd.conf`. Available configuration options are now `maas_url`
366+ and `cluster_uuid` only. MAAS will attempt to migrate any configuration
367+ on upgrade, otherwise it will use sane defaults.
368+
369+**Commissioning Actions**
370+ MAAS now supports commissioning actions. These allow the user to specify
371+ how commissioning should behave in certain escenarios. The commissioning
372+ actions available are:
373+
374+ * Enable SSH during commissioning & Keep machine ON after commissioning
375+ * Keep network configuration after commissioning
376+ * Keep storage configuration after commissioning
377+
378+**Warn users about missing power control tools**
379+ MAAS now warns users about the missing power control tools. Each MAAS
380+ power driver use a set of power tools that may or may not be installed
381+ by default. If these power tools are missing from the system, MAAS will
382+ warn users.
383+
384+**Python Power Drivers**
385+ Starting from MAAS 1.9, MAAS is moving away from using shell scripts
386+ templates for Power Drivers. These are being migrated to MAAS'
387+ internal control as power drivers. Currently supported are APC, MSCM,
388+ MSFT OCS, SM15k, UCSM, Virsh, VMWare and IPMI.
389+
390+ Remaining Power Drivers include AMT, Fence CDU's, Moonshot.
391+
392+Known Problems & Workarounds
393+----------------------------
394+
395+**Garbage in the UI after upgrade**
396+ When upgrading from any earlier release (1.5, 1.7, 1.8), the user may see
397+ garbage in the UI. This is because the local cache is dirty and won't be
398+ refreshed automatically. MAAS 1.9.0 introduced a mechanism to refresh the
399+ cache automatically, but this will only take into effect upgrading from
400+ 1.9.0 to any later release.
401+
402+ To work around this issue, the only thing required is to refresh the
403+ browsers cache, by hitting F5.
404+
405+ See bug `1515380`_ for more information.
406+
407+.. _1515380:
408+ https://launchpad.net/bugs/1515380
409+
410+
411+Major bugs fixed in this release
412+--------------------------------
413+
414+See https://launchpad.net/maas/+milestone/1.9.0 for details.
415+
416+
417+1.9.0 (RC4)
418+============
419+
420+Major bugs fixed in this release
421+--------------------------------
422+
423+LP: #1523674 Virsh is reporting ppc64le, not ppc64el.
424+
425+LP: #1524091 Don't require DHCP to be on if it should be off.
426+
427+LP: #1523988 No required packages for HMC as it uses pure python paramiko ssh client.
428+
429+LP: #1524007 Don't hold the cluster configuration lock while reloading boot images.
430+
431+LP: #1524924 Fix commissioning to correctly identify secondary subnets, VLAN's and fabrics.
432+
433+
434+1.9.0 (RC3)
435+=============
436+
437+Major bugs fixed in this release
438+--------------------------------
439+
440+LP: #1522898 "node-interface" API should just be "interface" - to allow devices to use it
441+
442+LP: #1519527 Juju 1.25.1 proposed: lxc units all have the same IP address after upgrade from 1.7/1.8.
443+
444+LP: #1522294 MAAS fails to parse some DHCP leases.
445+
446+LP: #1519090 DHCP interface automatically obtains an IP even when the subnet is unmanaged.
447+
448+LP: #1519077 MAAS assigns IP addresses on unmanaged subnets without consideration for some addresses known to be in use.
449+
450+LP: #1519396 MTU field is not exposed over the API for VLAN.
451+
452+LP: #1521833 Updating subnet name removes dns_server.
453+
454+LP: #1519919 CC looks for NICs with kernel module loaded and fall back doesn't check persistent device names.
455+
456+LP: #1522225 Migration 0181 can fail on upgrade if disks across nodes have duplicate serial numbers.
457+
458+LP: #1519247 Migration 0146 can fail on upgrade when migrating unmanaged subnets.
459+
460+LP: #1519397 [UI] Once a cache_set is created the UI fails with ERROR.
461+
462+LP: #1519918 [UI] "failed to detect a valid IP address" when trying to view node details.
463+
464+
465+1.9.0 (RC2)
466+=============
467+
468+Major bugs fixed in this release
469+--------------------------------
470+
471+LP: #1513085 Partitioning should align for performance.
472+
473+LP: #1516815 MAAS creates DNS record against Alias (eth0:1) if alias belongs to the PXE Interface.
474+
475+LP: #1515769 Failed to power on SM15k.
476+
477+LP: #1516722 Fix migration that might affect upgrade from 1.7.
478+
479+LP: #1516065 Failed to power control IPMI BMC that does not support setting the boot order.
480+
481+LP: #1517097 Constraints for acquiring interfaces argument should 'AND' key-value pairs for the same label.
482+
483+LP: #1517687 [UI] Cannot create a partition using the whole disk.
484+
485+LP: #1513258 [UI] CSS Broken for Bond Network Device.
486+
487+LP: #1516173 [UI] Prevent being able to unmount/remove filesystems while node is on.
488+
489+LP: #1510457 [UI] No error message if there is no boot and/or root disk configured for a node.
490+
491+>>>>>>> MERGE-SOURCE
492
493 1.9.0 (RC1)
494 =============
495
496=== modified file 'etc/maas/drivers.yaml'
497--- etc/maas/drivers.yaml 2015-09-24 16:22:12 +0000
498+++ etc/maas/drivers.yaml 2016-06-10 16:34:39 +0000
499@@ -45,7 +45,7 @@
500 - 'pci:v00001137d00000046sv*sd*bc*sc*i*'
501 module: snic
502 package: snic-dkms
503- repository: http://ppa.launchpad.net/cisco-ucs-team/snic-stable/ubuntu
504+ repository: http://ppa.launchpad.net/cisco-ucs-team/snic-stable/ubuntu
505 - blacklist: ahci
506 comment: HPDSA driver
507 key_binary: !!binary |
508@@ -75,4 +75,4 @@
509 - 'pci:v0000103Cd0000193Fsv0000103Csd00003381bc*sc*i*'
510 module: hpdsa
511 package: hpdsa-dkms
512- repository: http://downloads.linux.hp.com/SDR/repo/ubuntu-hpdsa
513+ repository: http://downloads.linux.hpe.com/SDR/repo/ubuntu-hpdsa
514
515=== modified file 'etc/maas/templates/commissioning-user-data/snippets/maas_ipmi_autodetect.py'
516=== modified file 'etc/maas/templates/commissioning-user-data/snippets/tests/test_maas_ipmi_autodetect.py'
517=== modified file 'src/maas/settings.py'
518=== modified file 'src/maasserver/api/devices.py'
519--- src/maasserver/api/devices.py 2016-05-12 19:07:37 +0000
520+++ src/maasserver/api/devices.py 2016-06-10 16:34:39 +0000
521@@ -7,6 +7,7 @@
522 ]
523
524 from maasserver.api.logger import maaslog
525+<<<<<<< TREE
526 from maasserver.api.nodes import (
527 NodeHandler,
528 NodesHandler,
529@@ -15,6 +16,24 @@
530 from maasserver.api.support import operation
531 from maasserver.enum import NODE_PERMISSION
532 from maasserver.exceptions import MAASAPIValidationError
533+=======
534+from maasserver.api.support import (
535+ operation,
536+ OperationsHandler,
537+)
538+from maasserver.api.utils import get_optional_list
539+from maasserver.enum import (
540+ INTERFACE_LINK_TYPE,
541+ IPADDRESS_TYPE,
542+ NODE_PERMISSION,
543+)
544+from maasserver.exceptions import (
545+ MAASAPIBadRequest,
546+ MAASAPIValidationError,
547+ StaticIPAddressExhaustion,
548+)
549+from maasserver.fields import MAC_RE
550+>>>>>>> MERGE-SOURCE
551 from maasserver.forms import (
552 DeviceForm,
553 DeviceWithMACsForm,
554@@ -156,8 +175,122 @@
555 device_system_id = device.system_id
556 return ('device_handler', (device_system_id,))
557
558+<<<<<<< TREE
559
560 class DevicesHandler(NodesHandler):
561+=======
562+ @operation(idempotent=False)
563+ def claim_sticky_ip_address(self, request, system_id):
564+ """Assign a "sticky" IP address to a device's MAC.
565+
566+ :param mac_address: Optional MAC address on the device on which to
567+ assign the sticky IP address. If not passed, defaults to the
568+ primary MAC for the device.
569+ :param requested_address: Optional IP address to claim. If this
570+ isn't passed, this method will draw an IP address from the static
571+ range of the cluster interface this MAC is related to.
572+ If passed, this method lets you associate any IP address
573+ with a MAC address if the MAC isn't related to a cluster interface.
574+
575+ Returns 404 if the device is not found.
576+ Returns 400 if the mac_address is not found on the device.
577+ Returns 503 if there are not enough IPs left on the cluster interface
578+ to which the mac_address is linked.
579+ Returns 503 if the interface does not have an associated subnet.
580+ Returns 503 if the requested_address falls in a dynamic range.
581+ Returns 503 if the requested_address is already allocated.
582+ """
583+ device = Device.objects.get_node_or_404(
584+ system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
585+ form = ClaimIPForMACForm(request.POST)
586+
587+ if not form.is_valid():
588+ raise MAASAPIValidationError(form.errors)
589+ else:
590+ raw_mac = request.POST.get('mac_address', None)
591+ if raw_mac is None:
592+ interface = device.get_boot_interface()
593+ else:
594+ try:
595+ interface = Interface.objects.get(
596+ mac_address=raw_mac, node=device)
597+ except Interface.DoesNotExist:
598+ raise MAASAPIBadRequest(
599+ "mac_address %s not found on the device" % raw_mac)
600+ requested_address = request.POST.get('requested_address', None)
601+ if requested_address is None:
602+ sticky_ips = interface.claim_static_ips()
603+ else:
604+ subnet = Subnet.objects.get_best_subnet_for_ip(
605+ requested_address)
606+ sticky_ips = [
607+ interface.link_subnet(
608+ INTERFACE_LINK_TYPE.STATIC, subnet,
609+ ip_address=requested_address),
610+ ]
611+
612+ if len(sticky_ips) == 0:
613+ raise StaticIPAddressExhaustion(
614+ "%s: An IP address could not be claimed at this time. "
615+ "Check your subnet ranges and utilization and try again." %
616+ device.hostname)
617+ else:
618+ maaslog.info(
619+ "%s: Sticky IP address(es) allocated: %s", device.hostname,
620+ ', '.join(allocation.ip for allocation in sticky_ips))
621+
622+ return device
623+
624+ @operation(idempotent=False)
625+ def release_sticky_ip_address(self, request, system_id):
626+ """Release a "sticky" IP address from a device's MAC.
627+
628+ :param address: Optional IP address to release. If left unspecified,
629+ will release every "sticky" IP address associated with the device.
630+
631+ Returns 400 if the specified addresses could not be deallocated
632+ Returns 404 if the device is not found.
633+ """
634+ device = Device.objects.get_node_or_404(
635+ system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
636+ form = ReleaseIPForm(request.POST)
637+
638+ if not form.is_valid():
639+ raise MAASAPIValidationError(form.errors)
640+
641+ address = request.POST.get('address', None)
642+ if address is not None and address.strip() == '':
643+ raise MAASAPIBadRequest(
644+ {'address': ["Cannot be empty if supplied."]})
645+
646+ deallocated_ips = []
647+ if address:
648+ sip = StaticIPAddress.objects.filter(
649+ alloc_type=IPADDRESS_TYPE.STICKY, ip=address,
650+ interface__node=device).first()
651+ if sip is None:
652+ raise MAASAPIBadRequest(
653+ "%s is not a sticky IP address on device: %s",
654+ address, device.hostname)
655+ for interface in sip.interface_set.all():
656+ interface.unlink_ip_address(sip)
657+ deallocated_ips.append(address)
658+ else:
659+ for interface in device.interface_set.all():
660+ for ip_address in interface.ip_addresses.filter(
661+ alloc_type=IPADDRESS_TYPE.STICKY, ip__isnull=False):
662+ if ip_address.ip:
663+ interface.unlink_ip_address(ip_address)
664+ deallocated_ips.append(ip_address.ip)
665+
666+ maaslog.info(
667+ "%s: Sticky IP address(es) deallocated: %s", device.hostname,
668+ ', '.join(unicode(ip) for ip in deallocated_ips))
669+ return device
670+
671+
672+class DevicesHandler(OperationsHandler):
673+>>>>>>> MERGE-SOURCE
674 """Manage the collection of all the devices in the MAAS."""
675 api_doc_section_name = "Devices"
676 update = delete = None
677
678=== modified file 'src/maasserver/api/files.py'
679=== modified file 'src/maasserver/api/interfaces.py'
680--- src/maasserver/api/interfaces.py 2016-05-12 19:07:37 +0000
681+++ src/maasserver/api/interfaces.py 2016-06-10 16:34:39 +0000
682@@ -75,6 +75,7 @@
683 "Cannot %s interface because the node is not Ready." % operation)
684
685
686+<<<<<<< TREE
687 def raise_error_if_controller(node, operation):
688 if node.is_controller:
689 raise MAASAPIForbidden(
690@@ -84,6 +85,11 @@
691 class InterfacesHandler(OperationsHandler):
692 """Manage interfaces on a node."""
693 api_doc_section_name = "Interfaces"
694+=======
695+class InterfacesHandler(OperationsHandler):
696+ """Manage interfaces on a node or device."""
697+ api_doc_section_name = "Interfaces"
698+>>>>>>> MERGE-SOURCE
699 create = update = delete = None
700 fields = DISPLAYED_INTERFACE_FIELDS
701
702@@ -93,8 +99,12 @@
703 return ('interfaces_handler', ["system_id"])
704
705 def read(self, request, system_id):
706+<<<<<<< TREE
707 """List all interfaces belonging to a machine, device, or
708 rack controller.
709+=======
710+ """List all interfaces belonging to a node or device.
711+>>>>>>> MERGE-SOURCE
712
713 Returns 404 if the node is not found.
714 """
715@@ -104,7 +114,11 @@
716
717 @operation(idempotent=False)
718 def create_physical(self, request, system_id):
719+<<<<<<< TREE
720 """Create a physical interface on a machine and device.
721+=======
722+ """Create a physical interface on a node or device.
723+>>>>>>> MERGE-SOURCE
724
725 :param name: Name of the interface.
726 :param mac_address: MAC address of the interface.
727@@ -119,6 +133,7 @@
728
729 Returns 404 if the node is not found.
730 """
731+<<<<<<< TREE
732 node = Node.objects.get_node_or_404(
733 system_id, request.user, NODE_PERMISSION.EDIT)
734 raise_error_if_controller(node, "create")
735@@ -126,6 +141,14 @@
736 if node.node_type == NODE_TYPE.MACHINE:
737 raise_error_for_invalid_state_on_allocated_operations(
738 node, request.user, "create")
739+=======
740+ node = Node.objects.get_node_or_404(
741+ system_id, request.user, NODE_PERMISSION.EDIT)
742+ # Installable nodes require the node needs to be in the correct state.
743+ if node.installable:
744+ raise_error_for_invalid_state_on_allocated_operations(
745+ node, request.user, "create")
746+>>>>>>> MERGE-SOURCE
747 form = PhysicalInterfaceForm(node=node, data=request.data)
748 if form.is_valid():
749 return form.save()
750@@ -386,6 +409,7 @@
751 Returns 404 if the node or interface is not found.
752 """
753 interface = Interface.objects.get_interface_or_404(
754+<<<<<<< TREE
755 system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
756 node = interface.get_node()
757 if node.node_type == NODE_TYPE.MACHINE:
758@@ -400,6 +424,15 @@
759 interface_form = ControllerInterfaceForm
760 else:
761 interface_form = InterfaceForm.get_interface_form(interface.type)
762+=======
763+ system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
764+ if interface.get_node().installable:
765+ # This node needs to be in the correct state to modify
766+ # the interface.
767+ raise_error_for_invalid_state_on_allocated_operations(
768+ interface.node, request.user, "update interface")
769+ interface_form = InterfaceForm.get_interface_form(interface.type)
770+>>>>>>> MERGE-SOURCE
771 # For VLAN interface we cast parents to parent. As a VLAN can only
772 # have one parent.
773 if interface.type == INTERFACE_TYPE.VLAN:
774@@ -423,6 +456,7 @@
775 Returns 404 if the node or interface is not found.
776 """
777 interface = Interface.objects.get_interface_or_404(
778+<<<<<<< TREE
779 system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
780 node = interface.get_node()
781 raise_error_if_controller(node, "delete interface")
782@@ -431,6 +465,14 @@
783 # the interface.
784 raise_error_for_invalid_state_on_allocated_operations(
785 interface.node, request.user, "delete interface")
786+=======
787+ system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
788+ if interface.get_node().installable:
789+ # This node needs to be in the correct state to modify
790+ # the interface.
791+ raise_error_for_invalid_state_on_allocated_operations(
792+ interface.node, request.user, "delete interface")
793+>>>>>>> MERGE-SOURCE
794 interface.delete()
795 return rc.DELETED
796
797@@ -466,6 +508,7 @@
798 Returns 404 if the node or interface is not found.
799 """
800 interface = Interface.objects.get_interface_or_404(
801+<<<<<<< TREE
802 system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
803 node = interface.get_node()
804 raise_error_if_controller(node, "link subnet")
805@@ -485,6 +528,26 @@
806 allowed_modes = [INTERFACE_LINK_TYPE.STATIC]
807 form = InterfaceLinkForm(
808 instance=interface, data=request.data, allowed_modes=allowed_modes)
809+=======
810+ system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
811+ node = interface.get_node()
812+ if node.installable:
813+ # This node needs to be in the correct state to modify
814+ # the interface.
815+ raise_error_for_invalid_state_on_allocated_operations(
816+ interface.node, request.user, "link subnet")
817+ allowed_modes = [
818+ INTERFACE_LINK_TYPE.AUTO,
819+ INTERFACE_LINK_TYPE.DHCP,
820+ INTERFACE_LINK_TYPE.STATIC,
821+ INTERFACE_LINK_TYPE.LINK_UP,
822+ ]
823+ else:
824+ # Devices can only be set in static IP mode.
825+ allowed_modes = [INTERFACE_LINK_TYPE.STATIC]
826+ form = InterfaceLinkForm(
827+ instance=interface, data=request.data, allowed_modes=allowed_modes)
828+>>>>>>> MERGE-SOURCE
829 if form.is_valid():
830 return form.save()
831 else:
832@@ -499,6 +562,7 @@
833 Returns 404 if the node or interface is not found.
834 """
835 interface = Interface.objects.get_interface_or_404(
836+<<<<<<< TREE
837 system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
838 node = interface.get_node()
839 raise_error_if_controller(node, "link subnet")
840@@ -507,6 +571,14 @@
841 # the interface.
842 raise_error_for_invalid_state_on_allocated_operations(
843 node, request.user, "unlink subnet")
844+=======
845+ system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
846+ if interface.get_node().installable:
847+ # This node needs to be in the correct state to modify
848+ # the interface.
849+ raise_error_for_invalid_state_on_allocated_operations(
850+ interface.node, request.user, "unlink subnet")
851+>>>>>>> MERGE-SOURCE
852 form = InterfaceUnlinkForm(instance=interface, data=request.data)
853 if form.is_valid():
854 return form.save()
855@@ -528,6 +600,7 @@
856 Returns 404 if the node or interface is not found.
857 """
858 interface = Interface.objects.get_interface_or_404(
859+<<<<<<< TREE
860 system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
861 node = interface.get_node()
862 raise_error_if_controller(node, "link subnet")
863@@ -536,6 +609,14 @@
864 # the interface.
865 raise_error_for_invalid_state_on_allocated_operations(
866 node, request.user, "set default gateway")
867+=======
868+ system_id, interface_id, request.user, NODE_PERMISSION.EDIT)
869+ if interface.get_node().installable:
870+ # This node needs to be in the correct state to modify
871+ # the interface.
872+ raise_error_for_invalid_state_on_allocated_operations(
873+ interface.node, request.user, "set default gateway")
874+>>>>>>> MERGE-SOURCE
875 form = InterfaceSetDefaultGatwayForm(
876 instance=interface, data=request.data)
877 if form.is_valid():
878@@ -543,6 +624,7 @@
879 else:
880 raise MAASAPIValidationError(form.errors)
881
882+<<<<<<< TREE
883 @operation(idempotent=False)
884 def add_tag(self, request, system_id, interface_id):
885 """Add a tag to interface on a node.
886@@ -575,6 +657,20 @@
887
888
889 class PhysicaInterfaceHandler(InterfaceHandler):
890+=======
891+
892+class NodeInterfacesHandler(InterfacesHandler):
893+ """Manage interfaces on a node. (Deprecated)"""
894+ api_doc_section_name = "Node Interfaces"
895+
896+
897+class NodeInterfaceHandler(InterfaceHandler):
898+ """Manage a node's interface. (Deprecated)"""
899+ api_doc_section_name = "Node Interface"
900+
901+
902+class PhysicaInterfaceHandler(InterfaceHandler):
903+>>>>>>> MERGE-SOURCE
904 """
905 This handler only exists because piston requires a unique handler per
906 class type. Without this class the resource_uri will not be added to any
907
908=== modified file 'src/maasserver/api/support.py'
909--- src/maasserver/api/support.py 2016-05-20 14:31:34 +0000
910+++ src/maasserver/api/support.py 2016-06-10 16:34:39 +0000
911@@ -16,9 +16,15 @@
912 from django.http import Http404
913 from maasserver.api.doc import get_api_description_hash
914 from maasserver.exceptions import MAASAPIBadRequest
915+<<<<<<< TREE
916 from piston3.authentication import NoAuthentication
917 from piston3.emitters import Emitter
918 from piston3.handler import (
919+=======
920+from piston.authentication import NoAuthentication
921+from piston.emitters import Emitter
922+from piston.handler import (
923+>>>>>>> MERGE-SOURCE
924 AnonymousBaseHandler,
925 BaseHandler,
926 HandlerMetaClass,
927@@ -82,6 +88,7 @@
928 class RestrictedResource(OperationsResource):
929 """A resource that's restricted to active users."""
930
931+<<<<<<< TREE
932 def __init__(self, handler, *, authentication):
933 """A value for `authentication` MUST be provided AND be meaningful.
934
935@@ -97,6 +104,23 @@
936 if not self.is_authentication_attempted:
937 raise AssertionError("Authentication must be attempted.")
938
939+=======
940+ def __init__(self, handler, authentication):
941+ """A value for `authentication` MUST be provided AND be meaningful.
942+
943+ This prevents the situation where none of the following are restricted
944+ at all::
945+
946+ handler = RestrictedResource(HandlerClass)
947+ handler = RestrictedResource(HandlerClass, authentication=None)
948+ handler = RestrictedResource(HandlerClass, authentication=[])
949+
950+ """
951+ super(RestrictedResource, self).__init__(handler, authentication)
952+ if not self.is_authentication_attempted:
953+ raise AssertionError("Authentication must be attempted.")
954+
955+>>>>>>> MERGE-SOURCE
956 def authenticate(self, request, rm):
957 actor, anonymous = super(
958 RestrictedResource, self).authenticate(request, rm)
959
960=== modified file 'src/maasserver/api/tests/test_devices.py'
961--- src/maasserver/api/tests/test_devices.py 2016-05-24 22:05:45 +0000
962+++ src/maasserver/api/tests/test_devices.py 2016-06-10 16:34:39 +0000
963@@ -11,15 +11,35 @@
964 from django.core.urlresolvers import reverse
965 from maasserver.enum import (
966 NODE_STATUS,
967+<<<<<<< TREE
968 NODE_TYPE,
969+=======
970+ NODEGROUP_STATUS,
971+ NODEGROUPINTERFACE_MANAGEMENT,
972+>>>>>>> MERGE-SOURCE
973 )
974 from maasserver.models import (
975+<<<<<<< TREE
976 Device,
977 Domain,
978 node as node_module,
979 )
980 from maasserver.testing.api import APITestCase
981+=======
982+ Device,
983+ Interface,
984+ interface as interface_module,
985+ Node,
986+ NodeGroup,
987+ StaticIPAddress,
988+)
989+from maasserver.testing.api import (
990+ APITestCase,
991+ APITransactionTestCase,
992+)
993+>>>>>>> MERGE-SOURCE
994 from maasserver.testing.factory import factory
995+<<<<<<< TREE
996 from maasserver.utils.converters import json_load_bytes
997 from maasserver.utils.orm import reload_object
998 from maastesting.matchers import MockCalledOnce
999@@ -42,6 +62,17 @@
1000
1001
1002 class TestDevicesAPI(APITestCase.ForUser):
1003+=======
1004+from maasserver.testing.orm import reload_object
1005+from mock import patch
1006+from testtools.matchers import (
1007+ HasLength,
1008+ Not,
1009+)
1010+
1011+
1012+class TestDevicesAPI(APITestCase):
1013+>>>>>>> MERGE-SOURCE
1014
1015 def test_handler_path(self):
1016 self.assertEqual(
1017@@ -348,6 +379,7 @@
1018 device = factory.make_Node(
1019 node_type=NODE_TYPE.DEVICE, owner=factory.make_User())
1020 response = self.client.delete(get_device_uri(device))
1021+<<<<<<< TREE
1022 self.assertEqual(http.client.FORBIDDEN, response.status_code)
1023 self.assertEqual(device, reload_object(device))
1024
1025@@ -392,3 +424,392 @@
1026 {'op': 'restore_default_configuration'})
1027 self.assertEqual(
1028 http.client.FORBIDDEN, response.status_code, response.content)
1029+=======
1030+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
1031+ self.assertEquals(device, reload_object(device))
1032+
1033+
1034+class TestClaimStickyIpAddressAPI(APITestCase):
1035+ """Tests for /api/1.0/devices/?op=claim_sticky_ip_address."""
1036+
1037+ def test__claims_ip_address_from_cluster_interface_static_range(self):
1038+ ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
1039+ ngi = factory.make_NodeGroupInterface(
1040+ ng, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
1041+ parent = factory.make_Node_with_Interface_on_Subnet(
1042+ nodegroup=ng, subnet=ngi.subnet)
1043+ device = factory.make_Node(
1044+ installable=False, parent=parent, interface=True,
1045+ disable_ipv4=False, owner=self.logged_in_user)
1046+ # Silence 'update_host_maps'.
1047+ self.patch_autospec(interface_module, "update_host_maps")
1048+ response = self.client.post(
1049+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
1050+ self.assertEqual(httplib.OK, response.status_code, response.content)
1051+ parsed_device = json.loads(response.content)
1052+ [returned_ip] = parsed_device["ip_addresses"]
1053+ static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first()
1054+ self.assertIsNotNone(static_ip)
1055+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
1056+
1057+ def test__claims_ip_address_from_unmanaged_cluster_interface(self):
1058+ ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
1059+ ngi = factory.make_NodeGroupInterface(
1060+ ng, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
1061+ parent = factory.make_Node_with_Interface_on_Subnet(
1062+ nodegroup=ng, subnet=ngi.subnet)
1063+ device = factory.make_Node(
1064+ installable=False, parent=parent, interface=True,
1065+ disable_ipv4=False, owner=self.logged_in_user)
1066+ # Silence 'update_host_maps'.
1067+ self.patch_autospec(interface_module, "update_host_maps")
1068+ response = self.client.post(
1069+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
1070+ self.assertEqual(httplib.OK, response.status_code, response.content)
1071+ parsed_device = json.loads(response.content)
1072+ [returned_ip] = parsed_device["ip_addresses"]
1073+ static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first()
1074+ self.assertIsNotNone(static_ip)
1075+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
1076+
1077+ def test__claims_ip_address_from_detached_cluster_interface(self):
1078+ ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
1079+ ngi = factory.make_NodeGroupInterface(
1080+ ng, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
1081+ subnet = ngi.subnet
1082+ ngi.subnet = None
1083+ ngi.save()
1084+ parent = factory.make_Node_with_Interface_on_Subnet(
1085+ nodegroup=ng, subnet=subnet, unmanaged=True)
1086+ device = factory.make_Node(
1087+ installable=False, parent=parent, interface=True,
1088+ disable_ipv4=False, owner=self.logged_in_user)
1089+ # Silence 'update_host_maps'.
1090+ self.patch_autospec(interface_module, "update_host_maps")
1091+ response = self.client.post(
1092+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
1093+ self.assertEqual(httplib.OK, response.status_code, response.content)
1094+ parsed_device = json.loads(response.content)
1095+ [returned_ip] = parsed_device["ip_addresses"]
1096+ static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first()
1097+ self.assertIsNotNone(static_ip)
1098+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
1099+
1100+ def test__claims_ip_address_after_devices_new(self):
1101+ ng = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
1102+ ngi = factory.make_NodeGroupInterface(
1103+ ng, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS)
1104+ parent = factory.make_Node_with_Interface_on_Subnet(
1105+ nodegroup=ng, subnet=ngi.subnet)
1106+ # Run 'devices new', as a sanity check to ensure the object is created
1107+ # the same way as it is when juju does it.
1108+ self.client.post(
1109+ reverse('devices_handler'),
1110+ {
1111+ 'op': 'new',
1112+ 'hostname': "lxc-1",
1113+ 'mac_addresses': "01:02:03:04:05:06",
1114+ 'parent': parent.system_id,
1115+ })
1116+ # Silence 'update_host_maps'.
1117+ device = Device.objects.first()
1118+ self.patch_autospec(interface_module, "update_host_maps")
1119+ response = self.client.post(
1120+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
1121+ self.assertEqual(httplib.OK, response.status_code, response.content)
1122+ parsed_device = json.loads(response.content)
1123+ # import pdb; pdb.set_trace()
1124+ [returned_ip] = parsed_device["ip_addresses"]
1125+ static_ip = StaticIPAddress.objects.filter(ip=returned_ip).first()
1126+ self.assertIsNotNone(static_ip)
1127+ self.assertEquals(IPADDRESS_TYPE.STICKY, static_ip.alloc_type)
1128+
1129+ def test__rejected_if_not_permitted(self):
1130+ parent = factory.make_Node_with_Interface_on_Subnet()
1131+ device = factory.make_Node(
1132+ installable=False, parent=parent, interface=True,
1133+ disable_ipv4=False, owner=factory.make_User())
1134+ self.patch_autospec(interface_module, "update_host_maps")
1135+ response = self.client.post(
1136+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
1137+ self.assertEqual(httplib.FORBIDDEN, response.status_code)
1138+
1139+ def test_creates_ip_with_random_ip(self):
1140+ requested_address = factory.make_ip_address()
1141+ device = factory.make_Node(
1142+ installable=False, interface=True, disable_ipv4=False,
1143+ owner=self.logged_in_user)
1144+ # Silence 'update_host_maps'.
1145+ self.patch_autospec(interface_module, "update_host_maps")
1146+ response = self.client.post(
1147+ get_device_uri(device),
1148+ {
1149+ 'op': 'claim_sticky_ip_address',
1150+ 'requested_address': requested_address,
1151+ })
1152+ self.assertEqual(httplib.OK, response.status_code, response.content)
1153+ parsed_device = json.loads(response.content)
1154+ [returned_ip] = parsed_device["ip_addresses"]
1155+ [given_ip] = StaticIPAddress.objects.all()
1156+ self.assertEqual(
1157+ (given_ip.ip, requested_address, IPADDRESS_TYPE.STICKY),
1158+ (returned_ip, returned_ip, given_ip.alloc_type)
1159+ )
1160+
1161+ def test_503_if_no_subnet_found(self):
1162+ device = factory.make_Node(
1163+ installable=False, interface=True, disable_ipv4=False,
1164+ owner=self.logged_in_user)
1165+ # Silence 'update_host_maps'.
1166+ self.patch_autospec(interface_module, "update_host_maps")
1167+ response = self.client.post(
1168+ get_device_uri(device),
1169+ {
1170+ 'op': 'claim_sticky_ip_address',
1171+ })
1172+ self.assertEqual(
1173+ httplib.SERVICE_UNAVAILABLE, response.status_code,
1174+ response.content)
1175+
1176+ @patch.object(Interface, 'claim_static_ips')
1177+ def test_503_if_no_ip_found(self, claim_static_ips):
1178+ claim_static_ips.side_effect = [list()]
1179+
1180+ device = factory.make_Node(
1181+ installable=False, interface=True, disable_ipv4=False,
1182+ owner=self.logged_in_user)
1183+ # Silence 'update_host_maps'.
1184+ self.patch_autospec(interface_module, "update_host_maps")
1185+ response = self.client.post(
1186+ get_device_uri(device),
1187+ {
1188+ 'op': 'claim_sticky_ip_address',
1189+ })
1190+ self.assertEqual(
1191+ httplib.SERVICE_UNAVAILABLE, response.status_code,
1192+ response.content)
1193+
1194+ def test_creates_ip_for_specific_mac(self):
1195+ requested_address = factory.make_ip_address()
1196+ device = factory.make_Node(
1197+ installable=False, interface=True, disable_ipv4=False,
1198+ owner=self.logged_in_user)
1199+ second_nic = factory.make_Interface(
1200+ INTERFACE_TYPE.PHYSICAL, node=device)
1201+ # Silence 'update_host_maps'.
1202+ self.patch_autospec(interface_module, "update_host_maps")
1203+ response = self.client.post(
1204+ get_device_uri(device),
1205+ {
1206+ 'op': 'claim_sticky_ip_address',
1207+ 'requested_address': requested_address,
1208+ 'mac_address': unicode(second_nic.mac_address),
1209+ })
1210+ self.assertEqual(httplib.OK, response.status_code, response.content)
1211+ parsed_device = json.loads(response.content)
1212+ [returned_ip] = parsed_device["ip_addresses"]
1213+ [given_ip] = StaticIPAddress.objects.all()
1214+ self.assertEqual(
1215+ (given_ip.ip, requested_address, IPADDRESS_TYPE.STICKY),
1216+ (returned_ip, returned_ip, given_ip.alloc_type)
1217+ )
1218+
1219+ def test_rejects_invalid_ip(self):
1220+ requested_address = factory.make_name('bogus')
1221+ device = factory.make_Node(
1222+ installable=False, interface=True, disable_ipv4=False,
1223+ owner=self.logged_in_user)
1224+ interface = device.interface_set.all()[0]
1225+ response = self.client.post(
1226+ get_device_uri(device),
1227+ {
1228+ 'op': 'claim_sticky_ip_address',
1229+ 'requested_address': requested_address,
1230+ 'mac_address': interface.mac_address
1231+ })
1232+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
1233+ self.assertEqual(
1234+ dict(requested_address=["Enter a valid IPv4 or IPv6 address."]),
1235+ json.loads(response.content))
1236+
1237+ def test_rejects_invalid_mac(self):
1238+ mac_address = factory.make_name('bogus')
1239+ requested_address = factory.make_ip_address()
1240+ device = factory.make_Node(
1241+ installable=False, interface=True, disable_ipv4=False,
1242+ owner=self.logged_in_user)
1243+ response = self.client.post(
1244+ get_device_uri(device),
1245+ {
1246+ 'op': 'claim_sticky_ip_address',
1247+ 'requested_address': requested_address,
1248+ 'mac_address': mac_address
1249+ })
1250+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
1251+ self.assertEqual(
1252+ dict(
1253+ mac_address=[
1254+ "'%s' is not a valid MAC address." % mac_address]),
1255+ json.loads(response.content))
1256+
1257+ def test_rejects_unrelated_mac(self):
1258+ # Create an other device.
1259+ other_device = factory.make_Node(
1260+ installable=False, interface=True, disable_ipv4=False,
1261+ owner=factory.make_User())
1262+ other_nic = other_device.interface_set.all()[0]
1263+
1264+ requested_address = factory.make_ip_address()
1265+ device = factory.make_Node(
1266+ installable=False, interface=True, disable_ipv4=False,
1267+ owner=self.logged_in_user)
1268+ # Silence 'update_host_maps'.
1269+ self.patch_autospec(interface_module, "update_host_maps")
1270+ response = self.client.post(
1271+ get_device_uri(device),
1272+ {
1273+ 'op': 'claim_sticky_ip_address',
1274+ 'requested_address': requested_address,
1275+ 'mac_address': other_nic.mac_address
1276+ })
1277+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
1278+ self.assertItemsEqual([], StaticIPAddress.objects.all())
1279+
1280+
1281+class TestDeviceReleaseStickyIpAddressAPI(APITestCase):
1282+ """Tests for /api/1.0/devices/?op=release_sticky_ip_address."""
1283+
1284+ def test__releases_ip_address(self):
1285+ parent = factory.make_Node_with_Interface_on_Subnet()
1286+ device = factory.make_Node(
1287+ installable=False, parent=parent, interface=True,
1288+ disable_ipv4=False, owner=self.logged_in_user)
1289+ # Silence 'update_host_maps' and 'remove_host_maps'
1290+ self.patch_autospec(interface_module, "update_host_maps")
1291+ self.patch_autospec(interface_module, "remove_host_maps")
1292+ response = self.client.post(
1293+ get_device_uri(device), {'op': 'claim_sticky_ip_address'})
1294+ self.assertEqual(httplib.OK, response.status_code, response.content)
1295+ parsed_device = json.loads(response.content)
1296+ self.expectThat(parsed_device["ip_addresses"], Not(HasLength(0)))
1297+
1298+ response = self.client.post(
1299+ get_device_uri(device), {'op': 'release_sticky_ip_address'})
1300+ self.assertEqual(httplib.OK, response.status_code, response.content)
1301+ parsed_device = json.loads(response.content)
1302+ self.expectThat(parsed_device["ip_addresses"], HasLength(0))
1303+
1304+ def test__rejects_invalid_ip(self):
1305+ device = factory.make_Node(
1306+ installable=False, interface=True, disable_ipv4=False,
1307+ owner=self.logged_in_user)
1308+ response = self.client.post(
1309+ get_device_uri(device),
1310+ {
1311+ 'op': 'release_sticky_ip_address',
1312+ 'address': factory.make_name('bogus'),
1313+ })
1314+ self.assertEqual(
1315+ httplib.BAD_REQUEST, response.status_code, response.content)
1316+ self.assertEqual(
1317+ dict(address=["Enter a valid IPv4 or IPv6 address."]),
1318+ json.loads(response.content))
1319+
1320+ def test__rejects_empty_ip(self):
1321+ device = factory.make_Node(
1322+ installable=False, interface=True, disable_ipv4=False,
1323+ owner=self.logged_in_user)
1324+ response = self.client.post(
1325+ get_device_uri(device),
1326+ {
1327+ 'op': 'release_sticky_ip_address',
1328+ 'address': '',
1329+ })
1330+ self.assertEqual(
1331+ httplib.BAD_REQUEST, response.status_code, response.content)
1332+
1333+
1334+class TestDeviceReleaseStickyIpAddressAPITransactional(APITransactionTestCase):
1335+ '''The following TestDeviceReleaseStickyIpAddressAPI tests require
1336+ APITransactionTestCase, and thus, have been separated
1337+ from the TestDeviceReleaseStickyIpAddressAPI above.
1338+ '''
1339+ def test__releases_all_ip_addresses(self):
1340+ network = factory._make_random_network(slash=24)
1341+ subnet = factory.make_Subnet(cidr=unicode(network.cidr))
1342+ device = factory.make_Node_with_Interface_on_Subnet(
1343+ installable=False, subnet=subnet,
1344+ disable_ipv4=False, owner=self.logged_in_user)
1345+ for _ in range(4):
1346+ extra_nic = factory.make_Interface(
1347+ INTERFACE_TYPE.PHYSICAL, node=device)
1348+ factory.make_StaticIPAddress(
1349+ alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="",
1350+ interface=extra_nic, subnet=subnet)
1351+ # Silence 'update_host_maps' and 'remove_host_maps'
1352+ self.patch_autospec(interface_module, "update_host_maps")
1353+ self.patch_autospec(interface_module, "remove_host_maps")
1354+ self.assertThat(device.interface_set.all(), HasLength(5))
1355+ for interface in device.interface_set.all():
1356+ with transaction.atomic():
1357+ allocated = interface.claim_static_ips()
1358+ self.expectThat(allocated, HasLength(1))
1359+ response = self.client.post(
1360+ get_device_uri(device), {'op': 'release_sticky_ip_address'})
1361+ self.assertEqual(httplib.OK, response.status_code, response.content)
1362+ parsed_device = json.loads(response.content)
1363+ self.expectThat(parsed_device["ip_addresses"], HasLength(0))
1364+
1365+ def test__releases_specific_address(self):
1366+ network = factory._make_random_network(slash=24)
1367+ subnet = factory.make_Subnet(cidr=unicode(network.cidr))
1368+ device = factory.make_Node_with_Interface_on_Subnet(
1369+ installable=False, subnet=subnet,
1370+ disable_ipv4=False, owner=self.logged_in_user)
1371+ extra_nic = factory.make_Interface(
1372+ INTERFACE_TYPE.PHYSICAL, node=device)
1373+ factory.make_StaticIPAddress(
1374+ alloc_type=IPADDRESS_TYPE.DISCOVERED, ip="",
1375+ interface=extra_nic, subnet=subnet)
1376+ # Silence 'update_host_maps' and 'remove_host_maps'
1377+ self.patch_autospec(interface_module, "update_host_maps")
1378+ self.patch_autospec(interface_module, "remove_host_maps")
1379+ self.assertThat(device.interface_set.all(), HasLength(2))
1380+ ips = []
1381+ for interface in device.interface_set.all():
1382+ with transaction.atomic():
1383+ allocated = interface.claim_static_ips()
1384+ self.expectThat(allocated, HasLength(1))
1385+ # Note: 'allocated' is a list of (ip,mac) tuples
1386+ ips.append(allocated[0])
1387+ response = self.client.post(
1388+ get_device_uri(device),
1389+ {
1390+ 'op': 'release_sticky_ip_address',
1391+ 'address': unicode(ips[0].ip)
1392+ })
1393+ self.assertEqual(httplib.OK, response.status_code, response.content)
1394+ parsed_device = json.loads(response.content)
1395+ self.expectThat(parsed_device["ip_addresses"], HasLength(1))
1396+
1397+ def test__rejected_if_not_permitted(self):
1398+ parent = factory.make_Node_with_Interface_on_Subnet()
1399+ device = factory.make_Node(
1400+ installable=False, parent=parent, interface=True,
1401+ disable_ipv4=False, owner=factory.make_User())
1402+ # Silence 'update_host_maps' and 'remove_host_maps'
1403+ self.patch_autospec(interface_module, "update_host_maps")
1404+ self.patch_autospec(interface_module, "remove_host_maps")
1405+ with transaction.atomic():
1406+ device.get_boot_interface().claim_static_ips()
1407+ self.assertThat(
1408+ StaticIPAddress.objects.filter(alloc_type=IPADDRESS_TYPE.STICKY),
1409+ HasLength(1))
1410+ response = self.client.post(
1411+ get_device_uri(device), {'op': 'release_sticky_ip_address'})
1412+ self.assertEqual(
1413+ httplib.FORBIDDEN, response.status_code, response.content)
1414+ self.assertThat(
1415+ StaticIPAddress.objects.filter(alloc_type=IPADDRESS_TYPE.STICKY),
1416+ HasLength(1))
1417+>>>>>>> MERGE-SOURCE
1418
1419=== modified file 'src/maasserver/api/tests/test_filestorage.py'
1420--- src/maasserver/api/tests/test_filestorage.py 2016-05-24 22:05:45 +0000
1421+++ src/maasserver/api/tests/test_filestorage.py 2016-06-10 16:34:39 +0000
1422@@ -75,7 +75,11 @@
1423 def test_get_does_not_work_anonymously(self):
1424 storage = factory.make_FileStorage()
1425 response = self.make_API_GET_request("get", storage.filename)
1426+<<<<<<< TREE
1427 self.assertEqual(http.client.BAD_REQUEST, response.status_code)
1428+=======
1429+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
1430+>>>>>>> MERGE-SOURCE
1431
1432 def test_get_by_key_works_anonymously(self):
1433 storage = factory.make_FileStorage()
1434@@ -196,13 +200,20 @@
1435 def test_get_file_succeeds(self):
1436 filename = factory.make_name('file')
1437 factory.make_FileStorage(
1438+<<<<<<< TREE
1439 filename=filename, content=b"give me rope",
1440 owner=self.user)
1441 response = self.make_API_GET_request("get", filename)
1442+=======
1443+ filename=filename, content=b"give me rope",
1444+ owner=self.logged_in_user)
1445+ response = self.make_API_GET_request("get", filename)
1446+>>>>>>> MERGE-SOURCE
1447
1448 self.assertEqual(http.client.OK, response.status_code)
1449 self.assertEqual(b"give me rope", response.content)
1450
1451+<<<<<<< TREE
1452 def test_get_file_checks_owner(self):
1453 filename = factory.make_name('file')
1454 factory.make_FileStorage(
1455@@ -223,6 +234,28 @@
1456 self.assertEqual(http.client.OK, response.status_code)
1457 self.assertEqual(storage.content, response.content)
1458
1459+=======
1460+ def test_get_file_checks_owner(self):
1461+ filename = factory.make_name('file')
1462+ factory.make_FileStorage(
1463+ filename=filename, content=b"give me rope",
1464+ owner=factory.make_User())
1465+ response = self.make_API_GET_request("get", filename)
1466+
1467+ self.assertEqual(httplib.NOT_FOUND, response.status_code)
1468+
1469+ def test_get_fetches_the_most_recent_file(self):
1470+ filename = factory.make_name('file')
1471+ factory.make_FileStorage(
1472+ filename=filename, owner=self.logged_in_user)
1473+ storage = factory.make_FileStorage(
1474+ filename=filename, owner=self.logged_in_user)
1475+ response = self.make_API_GET_request("get", filename)
1476+
1477+ self.assertEqual(httplib.OK, response.status_code)
1478+ self.assertEqual(storage.content, response.content)
1479+
1480+>>>>>>> MERGE-SOURCE
1481 def test_get_file_fails_with_no_filename(self):
1482 response = self.make_API_GET_request("get")
1483
1484
1485=== modified file 'src/maasserver/api/tests/test_interfaces.py'
1486--- src/maasserver/api/tests/test_interfaces.py 2016-05-26 13:28:46 +0000
1487+++ src/maasserver/api/tests/test_interfaces.py 2016-06-10 16:34:39 +0000
1488@@ -67,13 +67,22 @@
1489 return bond_interface, parents, [vlan_nic_10, vlan_nic_11]
1490
1491
1492+<<<<<<< TREE
1493 class TestInterfacesAPI(APITestCase.ForUser):
1494+=======
1495+class TestInterfacesAPI(APITestCase):
1496+>>>>>>> MERGE-SOURCE
1497
1498 def test_handler_path(self):
1499 node = factory.make_Node()
1500 self.assertEqual(
1501+<<<<<<< TREE
1502 '/api/2.0/nodes/%s/interfaces/' % (node.system_id),
1503 get_interfaces_uri(node))
1504+=======
1505+ '/api/1.0/nodes/%s/interfaces/' % (node.system_id),
1506+ get_interfaces_uri(node))
1507+>>>>>>> MERGE-SOURCE
1508
1509 def test_read(self):
1510 node = factory.make_Node()
1511@@ -93,6 +102,7 @@
1512 ]
1513 self.assertItemsEqual(expected_ids, result_ids)
1514
1515+<<<<<<< TREE
1516 def test_read_on_device(self):
1517 parent = factory.make_Node()
1518 device = factory.make_Device(
1519@@ -107,6 +117,20 @@
1520 self.assertEqual(
1521 interface.id, json_load_bytes(response.content)[0]['id'])
1522
1523+=======
1524+ def test_read_on_device(self):
1525+ parent = factory.make_Node()
1526+ device = factory.make_Node(
1527+ owner=self.logged_in_user, installable=False, parent=parent)
1528+ interface = factory.make_Interface(
1529+ INTERFACE_TYPE.PHYSICAL, node=device)
1530+ uri = get_interfaces_uri(device)
1531+ response = self.client.get(uri)
1532+
1533+ self.assertEqual(httplib.OK, response.status_code, response.content)
1534+ self.assertEqual(interface.id, json.loads(response.content)[0]['id'])
1535+
1536+>>>>>>> MERGE-SOURCE
1537 def test_create_physical(self):
1538 self.become_admin()
1539 for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
1540@@ -141,6 +165,7 @@
1541 "enabled": Equals(True),
1542 }))
1543
1544+<<<<<<< TREE
1545 def test_create_physical_on_device(self):
1546 parent = factory.make_Node()
1547 device = factory.make_Device(
1548@@ -175,6 +200,42 @@
1549 "enabled": Equals(True),
1550 }))
1551
1552+=======
1553+ def test_create_physical_on_device(self):
1554+ parent = factory.make_Node()
1555+ device = factory.make_Node(
1556+ owner=self.logged_in_user, installable=False, parent=parent)
1557+ mac = factory.make_mac_address()
1558+ name = factory.make_name("eth")
1559+ fabric = factory.make_Fabric()
1560+ vlan = fabric.get_default_vlan()
1561+ tags = [
1562+ factory.make_name("tag")
1563+ for _ in range(3)
1564+ ]
1565+ uri = get_interfaces_uri(device)
1566+ response = self.client.post(uri, {
1567+ "op": "create_physical",
1568+ "mac_address": mac,
1569+ "name": name,
1570+ "vlan": vlan.id,
1571+ "tags": ",".join(tags),
1572+ })
1573+
1574+ self.assertEqual(
1575+ httplib.OK, response.status_code, response.content)
1576+ self.assertThat(json.loads(response.content), ContainsDict({
1577+ "mac_address": Equals(mac),
1578+ "name": Equals(name),
1579+ "vlan": ContainsDict({
1580+ "id": Equals(vlan.id),
1581+ }),
1582+ "type": Equals("physical"),
1583+ "tags": Equals(tags),
1584+ "enabled": Equals(True),
1585+ }))
1586+
1587+>>>>>>> MERGE-SOURCE
1588 def test_create_physical_disabled(self):
1589 self.become_admin()
1590 for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
1591@@ -337,6 +398,7 @@
1592 parent_2_iface.name,
1593 ], parsed_interface['parents'])
1594
1595+<<<<<<< TREE
1596 def test_create_bond_404_on_device(self):
1597 parent = factory.make_Node()
1598 device = factory.make_Node(
1599@@ -349,6 +411,19 @@
1600 self.assertEqual(
1601 http.client.NOT_FOUND, response.status_code, response.content)
1602
1603+=======
1604+ def test_create_bond_404_on_device(self):
1605+ parent = factory.make_Node()
1606+ device = factory.make_Node(
1607+ owner=self.logged_in_user, installable=False, parent=parent)
1608+ uri = get_interfaces_uri(device)
1609+ response = self.client.post(uri, {
1610+ "op": "create_bond",
1611+ })
1612+ self.assertEqual(
1613+ httplib.NOT_FOUND, response.status_code, response.content)
1614+
1615+>>>>>>> MERGE-SOURCE
1616 def test_create_bond_requires_admin(self):
1617 node = factory.make_Node()
1618 vlan = factory.make_VLAN()
1619@@ -417,8 +492,13 @@
1620 self.assertEqual({
1621 "mac_address": ["This field cannot be blank."],
1622 "name": ["This field is required."],
1623+<<<<<<< TREE
1624 "parents": ["A bond interface must have one or more parents."],
1625 }, json_load_bytes(response.content))
1626+=======
1627+ "parents": ["A Bond interface must have one or more parents."],
1628+ }, json.loads(response.content))
1629+>>>>>>> MERGE-SOURCE
1630
1631 def test_create_vlan(self):
1632 self.become_admin()
1633@@ -452,6 +532,7 @@
1634 "tags": Equals(tags),
1635 }))
1636
1637+<<<<<<< TREE
1638 def test_create_vlan_404_on_device(self):
1639 parent = factory.make_Node()
1640 device = factory.make_Node(
1641@@ -464,6 +545,19 @@
1642 self.assertEqual(
1643 http.client.NOT_FOUND, response.status_code, response.content)
1644
1645+=======
1646+ def test_create_vlan_404_on_device(self):
1647+ parent = factory.make_Node()
1648+ device = factory.make_Node(
1649+ owner=self.logged_in_user, installable=False, parent=parent)
1650+ uri = get_interfaces_uri(device)
1651+ response = self.client.post(uri, {
1652+ "op": "create_vlan",
1653+ })
1654+ self.assertEqual(
1655+ httplib.NOT_FOUND, response.status_code, response.content)
1656+
1657+>>>>>>> MERGE-SOURCE
1658 def test_create_vlan_requires_admin(self):
1659 node = factory.make_Node()
1660 untagged_vlan = factory.make_VLAN()
1661@@ -719,6 +813,7 @@
1662 parsed_interface = json_load_bytes(response.content)
1663 self.assertEqual(bond0.id, parsed_interface['id'])
1664
1665+<<<<<<< TREE
1666 def test_read_device_interface(self):
1667 parent = factory.make_Node()
1668 device = factory.make_Device(parent=parent)
1669@@ -731,6 +826,19 @@
1670 parsed_interface = json_load_bytes(response.content)
1671 self.assertEqual(interface.id, parsed_interface['id'])
1672
1673+=======
1674+ def test_read_device_interface(self):
1675+ parent = factory.make_Node()
1676+ device = factory.make_Node(installable=False, parent=parent)
1677+ interface = factory.make_Interface(
1678+ INTERFACE_TYPE.PHYSICAL, node=device)
1679+ uri = get_interface_uri(interface)
1680+ response = self.client.get(uri)
1681+ self.assertEqual(httplib.OK, response.status_code, response.content)
1682+ parsed_interface = json.loads(response.content)
1683+ self.assertEqual(interface.id, parsed_interface['id'])
1684+
1685+>>>>>>> MERGE-SOURCE
1686 def test_read_404_when_invalid_id(self):
1687 node = factory.make_Node()
1688 uri = reverse(
1689@@ -780,6 +888,26 @@
1690 self.assertEquals(new_name, parsed_interface["name"])
1691 self.assertEquals(new_vlan.vid, parsed_interface["vlan"]["vid"])
1692
1693+ def test_update_device_physical_interface(self):
1694+ node = factory.make_Node()
1695+ device = factory.make_Node(
1696+ owner=self.logged_in_user, installable=False, parent=node)
1697+ interface = factory.make_Interface(
1698+ INTERFACE_TYPE.PHYSICAL, node=device)
1699+ new_name = factory.make_name("name")
1700+ new_fabric = factory.make_Fabric()
1701+ new_vlan = new_fabric.get_default_vlan()
1702+ uri = get_interface_uri(interface)
1703+ response = self.client.put(uri, {
1704+ "name": new_name,
1705+ "vlan": new_vlan.id,
1706+ })
1707+ self.assertEqual(
1708+ httplib.OK, response.status_code, response.content)
1709+ parsed_interface = json.loads(response.content)
1710+ self.assertEquals(new_name, parsed_interface["name"])
1711+ self.assertEquals(new_vlan.vid, parsed_interface["vlan"]["vid"])
1712+
1713 def test_update_bond_interface(self):
1714 self.become_admin()
1715 for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
1716@@ -866,6 +994,7 @@
1717 http.client.NO_CONTENT, response.status_code, response.content)
1718 self.assertIsNone(reload_object(interface))
1719
1720+<<<<<<< TREE
1721 def test_delete_deletes_device_interface(self):
1722 parent = factory.make_Node()
1723 device = factory.make_Device(
1724@@ -878,6 +1007,20 @@
1725 http.client.NO_CONTENT, response.status_code, response.content)
1726 self.assertIsNone(reload_object(interface))
1727
1728+=======
1729+ def test_delete_deletes_device_interface(self):
1730+ parent = factory.make_Node()
1731+ device = factory.make_Node(
1732+ owner=self.logged_in_user, installable=False, parent=parent)
1733+ interface = factory.make_Interface(
1734+ INTERFACE_TYPE.PHYSICAL, node=device)
1735+ uri = get_interface_uri(interface)
1736+ response = self.client.delete(uri)
1737+ self.assertEqual(
1738+ httplib.NO_CONTENT, response.status_code, response.content)
1739+ self.assertIsNone(reload_object(interface))
1740+
1741+>>>>>>> MERGE-SOURCE
1742 def test_delete_403_when_not_admin(self):
1743 node = factory.make_Node(interface=True)
1744 interface = node.get_boot_interface()
1745@@ -943,6 +1086,7 @@
1746 "mode": Equals(INTERFACE_LINK_TYPE.DHCP),
1747 }))
1748
1749+<<<<<<< TREE
1750 def test_link_subnet_creates_link_on_device(self):
1751 parent = factory.make_Node()
1752 device = factory.make_Device(
1753@@ -983,6 +1127,47 @@
1754 http.client.BAD_REQUEST, response.status_code,
1755 response.content)
1756
1757+=======
1758+ def test_link_subnet_creates_link_on_device(self):
1759+ parent = factory.make_Node()
1760+ device = factory.make_Node(
1761+ owner=self.logged_in_user, installable=False, parent=parent)
1762+ interface = factory.make_Interface(
1763+ INTERFACE_TYPE.PHYSICAL, node=device)
1764+ subnet = factory.make_Subnet(vlan=interface.vlan)
1765+ uri = get_interface_uri(interface)
1766+ response = self.client.post(uri, {
1767+ "op": "link_subnet",
1768+ "mode": INTERFACE_LINK_TYPE.STATIC,
1769+ "subnet": subnet.id,
1770+ })
1771+ self.assertEqual(
1772+ httplib.OK, response.status_code, response.content)
1773+ parsed_response = json.loads(response.content)
1774+ self.assertThat(
1775+ parsed_response["links"][0], ContainsDict({
1776+ "mode": Equals(INTERFACE_LINK_TYPE.STATIC),
1777+ }))
1778+
1779+ def test_link_subnet_on_device_only_allows_static(self):
1780+ parent = factory.make_Node()
1781+ device = factory.make_Node(
1782+ owner=self.logged_in_user, installable=False, parent=parent)
1783+ interface = factory.make_Interface(
1784+ INTERFACE_TYPE.PHYSICAL, node=device)
1785+ for link_type in [
1786+ INTERFACE_LINK_TYPE.AUTO,
1787+ INTERFACE_LINK_TYPE.DHCP,
1788+ INTERFACE_LINK_TYPE.LINK_UP]:
1789+ uri = get_interface_uri(interface)
1790+ response = self.client.post(uri, {
1791+ "op": "link_subnet",
1792+ "mode": link_type,
1793+ })
1794+ self.assertEqual(
1795+ httplib.BAD_REQUEST, response.status_code, response.content)
1796+
1797+>>>>>>> MERGE-SOURCE
1798 def test_link_subnet_raises_error(self):
1799 self.become_admin()
1800 for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
1801@@ -1058,6 +1243,7 @@
1802 http.client.OK, response.status_code, response.content)
1803 self.assertIsNone(reload_object(dhcp_ip))
1804
1805+<<<<<<< TREE
1806 def test_unlink_subnet_deletes_link_on_device(self):
1807 parent = factory.make_Node()
1808 device = factory.make_Device(
1809@@ -1077,6 +1263,27 @@
1810 http.client.OK, response.status_code, response.content)
1811 self.assertIsNone(reload_object(static_ip))
1812
1813+=======
1814+ def test_unlink_subnet_deletes_link_on_device(self):
1815+ parent = factory.make_Node()
1816+ device = factory.make_Node(
1817+ owner=self.logged_in_user, installable=False, parent=parent)
1818+ interface = factory.make_Interface(
1819+ INTERFACE_TYPE.PHYSICAL, node=device)
1820+ subnet = factory.make_Subnet()
1821+ static_ip = factory.make_StaticIPAddress(
1822+ alloc_type=IPADDRESS_TYPE.STICKY,
1823+ subnet=subnet, interface=interface)
1824+ uri = get_interface_uri(interface)
1825+ response = self.client.post(uri, {
1826+ "op": "unlink_subnet",
1827+ "id": static_ip.id,
1828+ })
1829+ self.assertEqual(
1830+ httplib.OK, response.status_code, response.content)
1831+ self.assertIsNone(reload_object(static_ip))
1832+
1833+>>>>>>> MERGE-SOURCE
1834 def test_unlink_subnet_raises_error(self):
1835 self.become_admin()
1836 for status in (NODE_STATUS.READY, NODE_STATUS.BROKEN):
1837
1838=== added file 'src/maasserver/api/tests/test_pxeconfig.py.OTHER'
1839--- src/maasserver/api/tests/test_pxeconfig.py.OTHER 1970-01-01 00:00:00 +0000
1840+++ src/maasserver/api/tests/test_pxeconfig.py.OTHER 2016-06-10 16:34:39 +0000
1841@@ -0,0 +1,693 @@
1842+# Copyright 2013-2015 Canonical Ltd. This software is licensed under the
1843+# GNU Affero General Public License version 3 (see the file LICENSE).
1844+
1845+"""Tests for PXE configuration retrieval from the API."""
1846+
1847+from __future__ import (
1848+ absolute_import,
1849+ print_function,
1850+ unicode_literals,
1851+ )
1852+
1853+str = None
1854+
1855+__metaclass__ = type
1856+__all__ = []
1857+
1858+import httplib
1859+import json
1860+
1861+from crochet import TimeoutError
1862+from django.core.urlresolvers import reverse
1863+from django.test.client import RequestFactory
1864+from maasserver import (
1865+ preseed as preseed_module,
1866+ server_address,
1867+)
1868+from maasserver.api import pxeconfig as pxeconfig_module
1869+from maasserver.api.pxeconfig import (
1870+ event_log_pxe_request,
1871+ find_nodegroup_for_pxeconfig_request,
1872+ get_boot_image,
1873+)
1874+from maasserver.clusterrpc.testing.boot_images import make_rpc_boot_image
1875+from maasserver.enum import (
1876+ BOOT_RESOURCE_TYPE,
1877+ INTERFACE_TYPE,
1878+ NODE_STATUS,
1879+ NODEGROUPINTERFACE_MANAGEMENT,
1880+)
1881+from maasserver.models import (
1882+ Config,
1883+ Event,
1884+ Interface,
1885+ Node,
1886+)
1887+from maasserver.preseed import (
1888+ compose_enlistment_preseed_url,
1889+ compose_preseed_url,
1890+)
1891+from maasserver.testing.architecture import make_usable_architecture
1892+from maasserver.testing.config import RegionConfigurationFixture
1893+from maasserver.testing.factory import factory
1894+from maasserver.testing.orm import reload_object
1895+from maasserver.testing.testcase import MAASServerTestCase
1896+from maastesting.fakemethod import FakeMethod
1897+from maastesting.matchers import (
1898+ MockCalledOnceWith,
1899+ MockNotCalled,
1900+)
1901+from mock import sentinel
1902+from netaddr import IPNetwork
1903+from provisioningserver import kernel_opts
1904+from provisioningserver.kernel_opts import KernelParameters
1905+from provisioningserver.rpc.exceptions import NoConnectionsAvailable
1906+from testtools.matchers import (
1907+ Contains,
1908+ ContainsAll,
1909+ Equals,
1910+ Is,
1911+ MatchesListwise,
1912+ StartsWith,
1913+)
1914+
1915+
1916+class TestGetBootImage(MAASServerTestCase):
1917+
1918+ def test__returns_None_when_connection_unavailable(self):
1919+ self.patch(
1920+ pxeconfig_module,
1921+ 'get_boot_images_for').side_effect = NoConnectionsAvailable
1922+ self.assertEqual(
1923+ None,
1924+ get_boot_image(
1925+ sentinel.nodegroup, sentinel.osystem,
1926+ sentinel.architecture, sentinel.subarchitecture,
1927+ sentinel.series, sentinel.purpose))
1928+
1929+ def test__returns_None_when_timeout_error(self):
1930+ self.patch(
1931+ pxeconfig_module,
1932+ 'get_boot_images_for').side_effect = TimeoutError
1933+ self.assertEqual(
1934+ None,
1935+ get_boot_image(
1936+ sentinel.nodegroup, sentinel.osystem,
1937+ sentinel.architecture, sentinel.subarchitecture,
1938+ sentinel.series, sentinel.purpose))
1939+
1940+ def test__returns_matching_image(self):
1941+ subarch = factory.make_name('subarch')
1942+ purpose = factory.make_name('purpose')
1943+ boot_image = make_rpc_boot_image(
1944+ subarchitecture=subarch, purpose=purpose)
1945+ other_images = [make_rpc_boot_image() for _ in range(3)]
1946+ self.patch(
1947+ pxeconfig_module,
1948+ 'get_boot_images_for').return_value = other_images + [boot_image]
1949+ self.assertEqual(
1950+ boot_image,
1951+ get_boot_image(
1952+ sentinel.nodegroup, sentinel.osystem,
1953+ sentinel.architecture, subarch,
1954+ sentinel.series, purpose))
1955+
1956+ def test__returns_None_on_no_matching_image(self):
1957+ subarch = factory.make_name('subarch')
1958+ purpose = factory.make_name('purpose')
1959+ other_images = [make_rpc_boot_image() for _ in range(3)]
1960+ self.patch(
1961+ pxeconfig_module,
1962+ 'get_boot_images_for').return_value = other_images
1963+ self.assertEqual(
1964+ None,
1965+ get_boot_image(
1966+ sentinel.nodegroup, sentinel.osystem,
1967+ sentinel.architecture, subarch,
1968+ sentinel.series, purpose))
1969+
1970+ def test__returns_None_immediately_if_purpose_is_local(self):
1971+ self.patch(pxeconfig_module, 'get_boot_images_for')
1972+ self.expectThat(
1973+ get_boot_image(
1974+ sentinel.nodegroup, sentinel.osystem,
1975+ sentinel.architecture, sentinel.subarchitecture,
1976+ sentinel.series, "local"),
1977+ Is(None))
1978+ self.expectThat(pxeconfig_module.get_boot_images_for, MockNotCalled())
1979+
1980+
1981+class TestPXEConfigAPI(MAASServerTestCase):
1982+ def setUp(self):
1983+ super(TestPXEConfigAPI, self).setUp()
1984+ self.useFixture(RegionConfigurationFixture())
1985+
1986+ def get_default_params(self, nodegroup=None):
1987+ if nodegroup is None:
1988+ nodegroup = factory.make_NodeGroup()
1989+ return {
1990+ "local": factory.make_ipv4_address(),
1991+ "remote": factory.make_ipv4_address(),
1992+ "cluster_uuid": nodegroup.uuid,
1993+ }
1994+
1995+ def get_mac_params(self):
1996+ node = factory.make_Node(status=NODE_STATUS.DEPLOYING)
1997+ arch, subarch = node.split_arch()
1998+ image = make_rpc_boot_image(
1999+ osystem=node.get_osystem(), release=node.get_distro_series(),
2000+ architecture=arch, subarchitecture=subarch,
2001+ purpose='install')
2002+ self.patch(
2003+ preseed_module,
2004+ 'get_boot_images_for').return_value = [image]
2005+ params = self.get_default_params()
2006+ params['mac'] = factory.make_Interface(
2007+ INTERFACE_TYPE.PHYSICAL, node=node).mac_address
2008+ return params
2009+
2010+ def get_pxeconfig(self, params=None):
2011+ """Make a request to `pxeconfig`, and return its response dict."""
2012+ if params is None:
2013+ params = self.get_default_params()
2014+ response = self.client.get(reverse('pxeconfig'), params)
2015+ return json.loads(response.content)
2016+
2017+ def test_pxeconfig_returns_json(self):
2018+ params = self.get_default_params()
2019+ response = self.client.get(
2020+ reverse('pxeconfig'), params)
2021+ self.assertThat(
2022+ (
2023+ response.status_code,
2024+ response['Content-Type'],
2025+ response.content,
2026+ response.content,
2027+ ),
2028+ MatchesListwise(
2029+ (
2030+ Equals(httplib.OK),
2031+ Equals("application/json"),
2032+ StartsWith(b'{'),
2033+ Contains('arch'),
2034+ )),
2035+ response)
2036+
2037+ def test_pxeconfig_returns_all_kernel_parameters(self):
2038+ params = self.get_default_params()
2039+ self.assertThat(
2040+ self.get_pxeconfig(params),
2041+ ContainsAll(KernelParameters._fields))
2042+
2043+ def test_pxeconfig_returns_success_for_known_node(self):
2044+ params = self.get_mac_params()
2045+ response = self.client.get(reverse('pxeconfig'), params)
2046+ self.assertEqual(httplib.OK, response.status_code)
2047+
2048+ def test_pxeconfig_returns_no_content_for_unknown_node(self):
2049+ params = dict(
2050+ mac=factory.make_mac_address(delimiter='-'),
2051+ local=factory.make_ipv4_address())
2052+ response = self.client.get(reverse('pxeconfig'), params)
2053+ self.assertEqual(httplib.NO_CONTENT, response.status_code)
2054+
2055+ def test_pxeconfig_returns_success_for_detailed_but_unknown_node(self):
2056+ architecture = make_usable_architecture(self)
2057+ arch, subarch = architecture.split('/')
2058+ nodegroup = factory.make_NodeGroup()
2059+ params = dict(
2060+ self.get_default_params(),
2061+ mac=factory.make_mac_address(delimiter='-'),
2062+ arch=arch,
2063+ subarch=subarch,
2064+ cluster_uuid=nodegroup.uuid)
2065+ response = self.client.get(reverse('pxeconfig'), params)
2066+ self.assertEqual(httplib.OK, response.status_code)
2067+
2068+ def test_pxeconfig_returns_global_kernel_params_for_enlisting_node(self):
2069+ # An 'enlisting' node means it looks like a node with details but we
2070+ # don't know about it yet. It should still receive the global
2071+ # kernel options.
2072+ value = factory.make_string()
2073+ Config.objects.set_config("kernel_opts", value)
2074+ architecture = make_usable_architecture(self)
2075+ arch, subarch = architecture.split('/')
2076+ nodegroup = factory.make_NodeGroup()
2077+ params = dict(
2078+ self.get_default_params(),
2079+ mac=factory.make_mac_address(delimiter='-'),
2080+ arch=arch,
2081+ subarch=subarch,
2082+ cluster_uuid=nodegroup.uuid)
2083+ response = self.client.get(reverse('pxeconfig'), params)
2084+ response_dict = json.loads(response.content)
2085+ self.assertEqual(value, response_dict['extra_opts'])
2086+
2087+ def test_pxeconfig_uses_present_boot_image(self):
2088+ osystem = Config.objects.get_config('commissioning_osystem')
2089+ release = Config.objects.get_config('commissioning_distro_series')
2090+ resource_name = '%s/%s' % (osystem, release)
2091+ factory.make_usable_boot_resource(
2092+ rtype=BOOT_RESOURCE_TYPE.SYNCED,
2093+ name=resource_name, architecture='amd64/generic')
2094+ params = self.get_default_params()
2095+ params_out = self.get_pxeconfig(params)
2096+ self.assertEqual("amd64", params_out["arch"])
2097+
2098+ def test_pxeconfig_defaults_to_i386_for_default(self):
2099+ # As a lowest-common-denominator, i386 is chosen when the node is not
2100+ # yet known to MAAS.
2101+ expected_arch = tuple(
2102+ make_usable_architecture(
2103+ self, arch_name="i386", subarch_name="generic").split("/"))
2104+ params = self.get_default_params()
2105+ params_out = self.get_pxeconfig(params)
2106+ observed_arch = params_out["arch"], params_out["subarch"]
2107+ self.assertEqual(expected_arch, observed_arch)
2108+
2109+ def test_pxeconfig_uses_fixed_hostname_for_enlisting_node(self):
2110+ params = self.get_default_params()
2111+ self.assertEqual(
2112+ 'maas-enlist', self.get_pxeconfig(params).get('hostname'))
2113+
2114+ def test_pxeconfig_uses_nodegroup_domain_for_enlisting_node(self):
2115+ nodegroup = factory.make_NodeGroup()
2116+ params = self.get_default_params(nodegroup=nodegroup)
2117+ self.assertEqual(
2118+ nodegroup.name,
2119+ self.get_pxeconfig(params).get('domain'))
2120+
2121+ def test_pxeconfig_splits_domain_from_node_hostname(self):
2122+ host = factory.make_name('host')
2123+ domain = factory.make_name('domain')
2124+ full_hostname = '.'.join([host, domain])
2125+ node = factory.make_Node(hostname=full_hostname)
2126+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2127+ params = self.get_default_params()
2128+ params['mac'] = interface.mac_address
2129+ pxe_config = self.get_pxeconfig(params)
2130+ self.assertEqual(host, pxe_config.get('hostname'))
2131+ self.assertNotIn(domain, pxe_config.values())
2132+
2133+ def test_pxeconfig_uses_nodegroup_domain_for_node(self):
2134+ interface = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
2135+ params = self.get_default_params()
2136+ params['mac'] = interface.mac_address
2137+ self.assertEqual(
2138+ interface.node.nodegroup.name,
2139+ self.get_pxeconfig(params).get('domain'))
2140+
2141+ def get_without_param(self, param):
2142+ """Request a `pxeconfig()` response, but omit `param` from request."""
2143+ params = self.get_params()
2144+ del params[param]
2145+ return self.client.get(reverse('pxeconfig'), params)
2146+
2147+ def silence_get_ephemeral_name(self):
2148+ # Silence `get_ephemeral_name` to avoid having to fetch the
2149+ # ephemeral name from the filesystem.
2150+ self.patch(
2151+ kernel_opts, 'get_ephemeral_name',
2152+ FakeMethod(result=factory.make_string()))
2153+
2154+ def test_pxeconfig_has_enlistment_preseed_url_for_default(self):
2155+ self.silence_get_ephemeral_name()
2156+ params = self.get_default_params()
2157+ response = self.client.get(reverse('pxeconfig'), params)
2158+ self.assertEqual(
2159+ compose_enlistment_preseed_url(),
2160+ json.loads(response.content)["preseed_url"])
2161+
2162+ def test_pxeconfig_enlistment_preseed_url_detects_request_origin(self):
2163+ self.silence_get_ephemeral_name()
2164+ hostname = factory.make_hostname()
2165+ ng_url = 'http://%s' % hostname
2166+ network = IPNetwork("10.1.1/24")
2167+ ip = factory.pick_ip_in_network(network)
2168+ self.patch(server_address, 'resolve_hostname').return_value = {ip}
2169+ factory.make_NodeGroup(
2170+ maas_url=ng_url, network=network,
2171+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
2172+ params = self.get_default_params()
2173+ del params['cluster_uuid']
2174+
2175+ # Simulate that the request originates from ip by setting
2176+ # 'REMOTE_ADDR'.
2177+ response = self.client.get(
2178+ reverse('pxeconfig'), params, REMOTE_ADDR=ip)
2179+ self.assertThat(
2180+ json.loads(response.content)["preseed_url"],
2181+ StartsWith(ng_url))
2182+
2183+ def test_pxeconfig_enlistment_log_host_url_detects_request_origin(self):
2184+ self.silence_get_ephemeral_name()
2185+ hostname = factory.make_hostname()
2186+ ng_url = 'http://%s' % hostname
2187+ network = IPNetwork("10.1.1/24")
2188+ ip = factory.pick_ip_in_network(network)
2189+ mock = self.patch(server_address, 'resolve_hostname')
2190+ mock.return_value = {ip}
2191+ factory.make_NodeGroup(
2192+ maas_url=ng_url, network=network,
2193+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
2194+ params = self.get_default_params()
2195+ del params['cluster_uuid']
2196+
2197+ # Simulate that the request originates from ip by setting
2198+ # 'REMOTE_ADDR'.
2199+ response = self.client.get(
2200+ reverse('pxeconfig'), params, REMOTE_ADDR=ip)
2201+ self.assertEqual(
2202+ (ip, hostname),
2203+ (json.loads(response.content)["log_host"], mock.call_args[0][0]))
2204+
2205+ def test_pxeconfig_enlistment_checks_default_min_hwe_kernel(self):
2206+ params = self.get_default_params()
2207+ params['arch'] = 'armhf'
2208+ Config.objects.set_config('default_min_hwe_kernel', 'hwe-v')
2209+ response = self.client.get(reverse('pxeconfig'), params)
2210+ self.assertEqual(
2211+ "hwe-v",
2212+ json.loads(response.content)["subarch"])
2213+
2214+ def test_pxeconfig_has_preseed_url_for_known_node(self):
2215+ params = self.get_mac_params()
2216+ node = Interface.objects.get(mac_address=params['mac']).node
2217+ response = self.client.get(reverse('pxeconfig'), params)
2218+ self.assertEqual(
2219+ compose_preseed_url(node),
2220+ json.loads(response.content)["preseed_url"])
2221+
2222+ def test_find_nodegroup_for_pxeconfig_request_uses_cluster_uuid(self):
2223+ # find_nodegroup_for_pxeconfig_request returns the nodegroup
2224+ # identified by the cluster_uuid parameter, if given. It
2225+ # completely ignores the other node or request details, as shown
2226+ # here by passing a uuid for a different cluster.
2227+ params = self.get_mac_params()
2228+ nodegroup = factory.make_NodeGroup()
2229+ params['cluster_uuid'] = nodegroup.uuid
2230+ request = RequestFactory().get(reverse('pxeconfig'), params)
2231+ self.assertEqual(
2232+ nodegroup,
2233+ find_nodegroup_for_pxeconfig_request(request))
2234+
2235+ def test_preseed_url_for_known_node_uses_nodegroup_maas_url(self):
2236+ ng_url = 'http://%s' % factory.make_name('host')
2237+ network = IPNetwork("10.1.1/24")
2238+ ip = factory.pick_ip_in_network(network)
2239+ self.patch(server_address, 'resolve_hostname').return_value = {ip}
2240+ nodegroup = factory.make_NodeGroup(maas_url=ng_url, network=network)
2241+ params = self.get_mac_params()
2242+ node = Interface.objects.get(mac_address=params['mac']).node
2243+ node.nodegroup = nodegroup
2244+ node.save()
2245+
2246+ # Simulate that the request originates from ip by setting
2247+ # 'REMOTE_ADDR'.
2248+ response = self.client.get(
2249+ reverse('pxeconfig'), params, REMOTE_ADDR=ip)
2250+ self.assertThat(
2251+ json.loads(response.content)["preseed_url"],
2252+ StartsWith(ng_url))
2253+
2254+ def test_pxeconfig_uses_boot_purpose_enlistment(self):
2255+ # test that purpose is set to "commissioning" for
2256+ # enlistment (when node is None).
2257+ params = self.get_default_params()
2258+ params['arch'] = 'armhf'
2259+ response = self.client.get(reverse('pxeconfig'), params)
2260+ self.assertEqual(
2261+ "commissioning",
2262+ json.loads(response.content)["purpose"])
2263+
2264+ def test_pxeconfig_returns_enlist_config_if_no_architecture_provided(self):
2265+ params = self.get_default_params()
2266+ pxe_config = self.get_pxeconfig(params)
2267+ self.assertEqual('enlist', pxe_config['purpose'])
2268+
2269+ def test_pxeconfig_returns_fs_host_as_cluster_controller(self):
2270+ # The kernel parameter `fs_host` points to the cluster controller
2271+ # address, which is passed over within the `local` parameter.
2272+ params = self.get_default_params()
2273+ kernel_params = KernelParameters(**self.get_pxeconfig(params))
2274+ self.assertEqual(params["local"], kernel_params.fs_host)
2275+
2276+ def test_pxeconfig_returns_extra_kernel_options(self):
2277+ extra_kernel_opts = factory.make_string()
2278+ Config.objects.set_config('kernel_opts', extra_kernel_opts)
2279+ params = self.get_mac_params()
2280+ pxe_config = self.get_pxeconfig(params)
2281+ self.assertEqual(extra_kernel_opts, pxe_config['extra_opts'])
2282+
2283+ def test_pxeconfig_returns_None_for_extra_kernel_opts(self):
2284+ params = self.get_mac_params()
2285+ pxe_config = self.get_pxeconfig(params)
2286+ self.assertEqual(None, pxe_config['extra_opts'])
2287+
2288+ def test_pxeconfig_returns_commissioning_for_insane_state(self):
2289+ nic = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
2290+ params = self.get_default_params()
2291+ params['mac'] = nic.mac_address
2292+ pxe_config = self.get_pxeconfig(params)
2293+ # The 'purpose' of the PXE config is 'commissioning' here
2294+ # even if the 'purpose' returned by node.get_boot_purpose
2295+ # is 'poweroff' because MAAS needs to bring the machine
2296+ # up in a commissioning environment in order to power
2297+ # the machine down.
2298+ self.assertEqual('commissioning', pxe_config['purpose'])
2299+
2300+ def test_pxeconfig_returns_commissioning_for_ready_node(self):
2301+ nic = factory.make_Interface(INTERFACE_TYPE.PHYSICAL)
2302+ nic.node.status = NODE_STATUS.READY
2303+ nic.node.save()
2304+ params = self.get_default_params()
2305+ params['mac'] = nic.mac_address
2306+ pxe_config = self.get_pxeconfig(params)
2307+ self.assertEqual('commissioning', pxe_config['purpose'])
2308+
2309+ def test_pxeconfig_returns_image_subarch_not_node_subarch(self):
2310+ # In the scenario such as deploying trusty on an hwe-s subarch
2311+ # node, the code will have fallen back to using trusty's generic
2312+ # image as per the supported_subarches on the image. However,
2313+ # pxeconfig needs to make sure the image path refers to the
2314+ # subarch from the image, rather than the requested one.
2315+ osystem = 'ubuntu'
2316+ release = Config.objects.get_config('default_distro_series')
2317+ nodegroup = factory.make_NodeGroup()
2318+ generic_image = make_rpc_boot_image(
2319+ osystem=osystem, release=release,
2320+ architecture="amd64", subarchitecture="generic",
2321+ purpose='install')
2322+ hwe_s_image = make_rpc_boot_image(
2323+ osystem=osystem, release=release,
2324+ architecture="amd64", subarchitecture="hwe-s",
2325+ purpose='install')
2326+ self.patch(
2327+ preseed_module,
2328+ 'get_boot_images_for').return_value = [generic_image, hwe_s_image]
2329+ self.patch(
2330+ pxeconfig_module,
2331+ 'get_boot_images_for').return_value = [generic_image, hwe_s_image]
2332+ node = factory.make_Node(
2333+ interface=True, nodegroup=nodegroup, status=NODE_STATUS.DEPLOYING,
2334+ architecture="amd64/hwe-s")
2335+ params = self.get_default_params()
2336+ params['cluster_uuid'] = nodegroup.uuid
2337+ params['mac'] = node.get_boot_interface().mac_address
2338+ params['arch'] = "amd64"
2339+ params['subarch'] = "hwe-s"
2340+
2341+ params_out = self.get_pxeconfig(params)
2342+ self.assertEqual("hwe-s", params_out["subarch"])
2343+
2344+ def test_pxeconfig_calls_event_log_pxe_request(self):
2345+ node = factory.make_Node()
2346+ nic = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2347+ params = self.get_default_params()
2348+ params['mac'] = nic.mac_address
2349+ event_log_pxe_request = self.patch_autospec(
2350+ pxeconfig_module, 'event_log_pxe_request')
2351+ self.client.get(reverse('pxeconfig'), params)
2352+ self.assertThat(
2353+ event_log_pxe_request,
2354+ MockCalledOnceWith(node, node.get_boot_purpose()))
2355+
2356+ def test_event_log_pxe_request_for_known_boot_purpose(self):
2357+ purposes = [
2358+ ("commissioning", "commissioning"),
2359+ ("install", "d-i install"),
2360+ ("xinstall", "curtin install"),
2361+ ("local", "local boot"),
2362+ ("poweroff", "power off")]
2363+ for purpose, description in purposes:
2364+ node = factory.make_Node()
2365+ event_log_pxe_request(node, purpose)
2366+ self.assertEqual(
2367+ description,
2368+ Event.objects.get(node=node).description)
2369+
2370+ def test_pxeconfig_sets_boot_interface_when_empty(self):
2371+ node = factory.make_Node(interface=True)
2372+ nic = node.get_boot_interface()
2373+ node.boot_interface = None
2374+ node.save()
2375+ params = self.get_default_params()
2376+ params['mac'] = nic.mac_address
2377+ self.client.get(reverse('pxeconfig'), params)
2378+ node = reload_object(node)
2379+ self.assertEqual(nic, node.boot_interface)
2380+
2381+ def test_pxeconfig_updates_boot_interface_when_changed(self):
2382+ node = factory.make_Node(interface=True)
2383+ node.boot_interface = factory.make_Interface(
2384+ INTERFACE_TYPE.PHYSICAL, node=node)
2385+ node.save()
2386+ nic = factory.make_Interface(
2387+ INTERFACE_TYPE.PHYSICAL, node=node)
2388+ params = self.get_default_params()
2389+ params['mac'] = nic.mac_address
2390+ self.client.get(reverse('pxeconfig'), params)
2391+ node = reload_object(node)
2392+ self.assertEqual(nic, node.boot_interface)
2393+
2394+ def test_pxeconfig_doesnt_update_boot_interface_when_same(self):
2395+ node = factory.make_Node()
2396+ node.boot_interface = factory.make_Interface(
2397+ INTERFACE_TYPE.PHYSICAL, node=node)
2398+ params = self.get_default_params()
2399+ params['mac'] = node.boot_interface.mac_address
2400+ node.boot_cluster_ip = params['local']
2401+ node.save()
2402+ mock_save = self.patch(Node, 'save')
2403+ self.client.get(reverse('pxeconfig'), params)
2404+ self.assertThat(mock_save, MockNotCalled())
2405+
2406+ def test_pxeconfig_sets_boot_cluster_ip_when_empty(self):
2407+ node = factory.make_Node(interface=True)
2408+ params = self.get_default_params()
2409+ params['mac'] = node.get_boot_interface().mac_address
2410+ self.client.get(reverse('pxeconfig'), params)
2411+ node = reload_object(node)
2412+ self.assertEqual(params['local'], node.boot_cluster_ip)
2413+
2414+ def test_pxeconfig_updates_boot_cluster_ip_when_changed(self):
2415+ node = factory.make_Node(interface=True)
2416+ node.boot_cluster_ip = factory.make_ipv4_address()
2417+ node.save()
2418+ params = self.get_default_params()
2419+ params['mac'] = node.get_boot_interface().mac_address
2420+ self.client.get(reverse('pxeconfig'), params)
2421+ node = reload_object(node)
2422+ self.assertEqual(params['local'], node.boot_cluster_ip)
2423+
2424+ def test_pxeconfig_doesnt_update_boot_cluster_ip_when_same(self):
2425+ node = factory.make_Node()
2426+ node.boot_interface = factory.make_Interface(
2427+ INTERFACE_TYPE.PHYSICAL, node=node)
2428+ params = self.get_default_params()
2429+ params['mac'] = node.boot_interface.mac_address
2430+ node.boot_cluster_ip = params['local']
2431+ node.save()
2432+ mock_save = self.patch(Node, 'save')
2433+ self.client.get(reverse('pxeconfig'), params)
2434+ self.assertThat(mock_save, MockNotCalled())
2435+
2436+ def test_pxeconfig_updates_bios_boot_method(self):
2437+ node = factory.make_Node(interface=True)
2438+ nic = node.get_boot_interface()
2439+ params = self.get_default_params()
2440+ params['mac'] = nic.mac_address
2441+ params['bios_boot_method'] = 'pxe'
2442+ self.client.get(reverse('pxeconfig'), params)
2443+ node = reload_object(node)
2444+ self.assertEqual('pxe', node.bios_boot_method)
2445+
2446+ def test_pxeconfig_doesnt_update_bios_boot_method_when_same(self):
2447+ node = factory.make_Node(interface=True, bios_boot_method='uefi')
2448+ nic = node.get_boot_interface()
2449+ params = self.get_default_params()
2450+ params['mac'] = nic.mac_address
2451+ params['bios_boot_method'] = 'uefi'
2452+ node.boot_interface = nic
2453+ node.boot_cluster_ip = params['local']
2454+ node.save()
2455+ mock_save = self.patch(Node, 'save')
2456+ self.client.get(reverse('pxeconfig'), params)
2457+ self.assertThat(mock_save, MockNotCalled())
2458+
2459+ def test_pxeconfig_returns_commissioning_os_series_for_other_oses(self):
2460+ osystem = Config.objects.get_config('default_osystem')
2461+ release = Config.objects.get_config('commissioning_distro_series')
2462+ nodegroup = factory.make_NodeGroup()
2463+ os_image = make_rpc_boot_image(purpose='xinstall')
2464+ architecture = '%s/%s' % (
2465+ os_image['architecture'], os_image['subarchitecture'])
2466+ self.patch(
2467+ preseed_module,
2468+ 'get_boot_images_for').return_value = [os_image]
2469+ self.patch(
2470+ pxeconfig_module,
2471+ 'get_boot_images_for').return_value = [os_image]
2472+ node = factory.make_Node(
2473+ interface=True, nodegroup=nodegroup, status=NODE_STATUS.DEPLOYING,
2474+ osystem=os_image['osystem'],
2475+ distro_series=os_image['release'],
2476+ architecture=architecture)
2477+ params = self.get_default_params()
2478+ params['cluster_uuid'] = nodegroup.uuid
2479+ params['mac'] = node.get_boot_interface().mac_address
2480+ params_out = self.get_pxeconfig(params)
2481+ self.assertEqual(osystem, params_out["osystem"])
2482+ self.assertEqual(release, params_out["release"])
2483+
2484+ def test_pxeconfig_commissioning_node_uses_min_hwe_kernel(self):
2485+ node = factory.make_Node(min_hwe_kernel="hwe-v")
2486+ nic = factory.make_Interface(INTERFACE_TYPE.PHYSICAL, node=node)
2487+ self.patch(Node, 'get_boot_purpose').return_value = "commissioning"
2488+ params = self.get_default_params()
2489+ params['mac'] = nic.mac_address
2490+ response = self.client.get(reverse('pxeconfig'), params)
2491+ self.assertEqual(
2492+ "hwe-v",
2493+ json.loads(response.content)["subarch"])
2494+
2495+ def test_pxeconfig_returns_ubuntu_os_series_for_ubuntu_xinstall(self):
2496+ nodegroup = factory.make_NodeGroup()
2497+ ubuntu_image = make_rpc_boot_image(
2498+ osystem='ubuntu', purpose='xinstall')
2499+ architecture = '%s/%s' % (
2500+ ubuntu_image['architecture'], ubuntu_image['subarchitecture'])
2501+ self.patch(
2502+ preseed_module,
2503+ 'get_boot_images_for').return_value = [ubuntu_image]
2504+ self.patch(
2505+ pxeconfig_module,
2506+ 'get_boot_images_for').return_value = [ubuntu_image]
2507+ node = factory.make_Node(
2508+ interface=True, nodegroup=nodegroup, status=NODE_STATUS.DEPLOYING,
2509+ osystem='ubuntu', distro_series=ubuntu_image['release'],
2510+ architecture=architecture)
2511+ params = self.get_default_params()
2512+ params['cluster_uuid'] = nodegroup.uuid
2513+ params['mac'] = node.get_boot_interface().mac_address
2514+ params_out = self.get_pxeconfig(params)
2515+ self.assertEqual(ubuntu_image['release'], params_out["release"])
2516+
2517+ def test_pxeconfig_returns_commissioning_os_when_erasing_disks(self):
2518+ commissioning_osystem = factory.make_name("os")
2519+ Config.objects.set_config(
2520+ "commissioning_osystem", commissioning_osystem)
2521+ commissioning_series = factory.make_name("series")
2522+ Config.objects.set_config(
2523+ "commissioning_distro_series", commissioning_series)
2524+ nodegroup = factory.make_NodeGroup()
2525+ node = factory.make_Node(
2526+ nodegroup=nodegroup, status=NODE_STATUS.DISK_ERASING,
2527+ osystem=factory.make_name("centos"), interface=True,
2528+ distro_series=factory.make_name("release"))
2529+ params = self.get_default_params()
2530+ params['cluster_uuid'] = nodegroup.uuid
2531+ params['mac'] = node.get_boot_interface().mac_address
2532+ params_out = self.get_pxeconfig(params)
2533+ self.assertEqual(commissioning_osystem, params_out['osystem'])
2534+ self.assertEqual(commissioning_series, params_out['release'])
2535
2536=== modified file 'src/maasserver/api/tests/test_raid.py'
2537=== modified file 'src/maasserver/api/tests/test_support.py'
2538--- src/maasserver/api/tests/test_support.py 2016-05-24 21:29:53 +0000
2539+++ src/maasserver/api/tests/test_support.py 2016-06-10 16:34:39 +0000
2540@@ -37,6 +37,7 @@
2541 Equals,
2542 Is,
2543 )
2544+<<<<<<< TREE
2545
2546
2547 class StubHandler:
2548@@ -46,6 +47,22 @@
2549
2550
2551 class TestOperationsResource(APITestCase.ForUser):
2552+=======
2553+from piston.authentication import NoAuthentication
2554+from testtools.matchers import (
2555+ Equals,
2556+ Is,
2557+)
2558+
2559+
2560+class StubHandler:
2561+ """A stub handler class that breaks when called."""
2562+ def __call__(self, request):
2563+ raise AssertionError("Do not call the stub handler.")
2564+
2565+
2566+class TestOperationsResource(APITestCase):
2567+>>>>>>> MERGE-SOURCE
2568
2569 def test_type_error_is_not_hidden(self):
2570 # This tests that bug #1228205 is fixed (i.e. that a
2571@@ -81,6 +98,7 @@
2572 response["X-MAAS-API-Hash"],
2573 Equals(get_api_description_hash()))
2574
2575+<<<<<<< TREE
2576 def test_authenticated_is_False_when_no_authentication_provided(self):
2577 resource = OperationsResource(StubHandler)
2578 self.assertThat(resource.is_authentication_attempted, Is(False))
2579@@ -136,6 +154,63 @@
2580
2581
2582 class TestAdminMethodDecorator(MAASServerTestCase):
2583+=======
2584+ def test_authenticated_is_False_when_no_authentication_provided(self):
2585+ resource = OperationsResource(StubHandler)
2586+ self.assertThat(resource.is_authentication_attempted, Is(False))
2587+
2588+ def test_authenticated_is_False_when_authentication_is_empty(self):
2589+ resource = OperationsResource(StubHandler, authentication=[])
2590+ self.assertThat(resource.is_authentication_attempted, Is(False))
2591+
2592+ def test_authenticated_is_False_when_authentication_is_NoAuthn(self):
2593+ resource = OperationsResource(
2594+ StubHandler, authentication=NoAuthentication())
2595+ self.assertThat(resource.is_authentication_attempted, Is(False))
2596+
2597+ def test_authenticated_is_True_when_authentication_is_provided(self):
2598+ resource = OperationsResource(
2599+ StubHandler, authentication=sentinel.authentication)
2600+ self.assertThat(resource.is_authentication_attempted, Is(True))
2601+
2602+
2603+class TestRestrictedResources(APITestCase):
2604+ """Tests for `RestrictedResource` and `AdminRestrictedResource`."""
2605+
2606+ scenarios = (
2607+ ("user", dict(resource_type=RestrictedResource)),
2608+ ("admin", dict(resource_type=AdminRestrictedResource)),
2609+ )
2610+
2611+ def test_authentication_must_not_be_None(self):
2612+ error = self.assertRaises(
2613+ AssertionError, self.resource_type, StubHandler,
2614+ authentication=None)
2615+ self.assertThat(unicode(error), Equals(
2616+ "Authentication must be attempted."))
2617+
2618+ def test_authentication_must_be_non_empty(self):
2619+ error = self.assertRaises(
2620+ AssertionError, self.resource_type, StubHandler,
2621+ authentication=[])
2622+ self.assertThat(unicode(error), Equals(
2623+ "Authentication must be attempted."))
2624+
2625+ def test_authentication_must_be_meaningful(self):
2626+ error = self.assertRaises(
2627+ AssertionError, self.resource_type, StubHandler,
2628+ authentication=NoAuthentication())
2629+ self.assertThat(unicode(error), Equals(
2630+ "Authentication must be attempted."))
2631+
2632+ def test_authentication_is_okay(self):
2633+ resource = self.resource_type(
2634+ StubHandler, authentication=sentinel.authentication)
2635+ self.assertThat(resource.is_authentication_attempted, Is(True))
2636+
2637+
2638+class TestAdminMethodDecorator(APITestCase):
2639+>>>>>>> MERGE-SOURCE
2640
2641 def test_non_admin_are_rejected(self):
2642 FakeRequest = namedtuple('FakeRequest', ['user'])
2643
2644=== modified file 'src/maasserver/api/tests/test_version.py'
2645--- src/maasserver/api/tests/test_version.py 2016-05-24 14:43:27 +0000
2646+++ src/maasserver/api/tests/test_version.py 2016-06-10 16:34:39 +0000
2647@@ -9,13 +9,24 @@
2648 import http.client
2649 import json
2650
2651+<<<<<<< TREE
2652 from django.conf import settings
2653+=======
2654+from django.contrib.auth.models import AnonymousUser
2655+>>>>>>> MERGE-SOURCE
2656 from django.core.urlresolvers import reverse
2657 from maasserver.api.version import API_CAPABILITIES_LIST
2658+<<<<<<< TREE
2659 from maasserver.testing.api import APITestCase
2660+=======
2661+from maasserver.testing.api import MultipleUsersScenarios
2662+from maasserver.testing.factory import factory
2663+from maasserver.testing.testcase import MAASServerTestCase
2664+>>>>>>> MERGE-SOURCE
2665 from maasserver.utils import version as version_module
2666
2667
2668+<<<<<<< TREE
2669 class TestVersionAPIBasics(APITestCase.ForAnonymousAndUserAndAdmin):
2670 """Basic tests for /version/ API."""
2671
2672@@ -26,7 +37,27 @@
2673
2674 class TestVersionAPI(APITestCase.ForAnonymousAndUser):
2675 """Tests for /version/ API."""
2676-
2677+=======
2678+class TestVersionAPIBasics(MAASServerTestCase):
2679+ """Basic tests for /version/ API."""
2680+>>>>>>> MERGE-SOURCE
2681+
2682+<<<<<<< TREE
2683+=======
2684+ def test_handler_path(self):
2685+ self.assertEqual(
2686+ '/api/1.0/version/', reverse('version_handler'))
2687+
2688+
2689+class TestVersionAPI(MultipleUsersScenarios, MAASServerTestCase):
2690+ """Tests for /version/ API."""
2691+
2692+ scenarios = [
2693+ ('anon', dict(userfactory=AnonymousUser)),
2694+ ('user', dict(userfactory=factory.make_User)),
2695+ ]
2696+
2697+>>>>>>> MERGE-SOURCE
2698 def test_GET_returns_details(self):
2699 mock_apt = self.patch(version_module, "get_version_from_apt")
2700 mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
2701
2702=== modified file 'src/maasserver/api/tests/test_vlans.py'
2703--- src/maasserver/api/tests/test_vlans.py 2016-05-24 21:29:53 +0000
2704+++ src/maasserver/api/tests/test_vlans.py 2016-06-10 16:34:39 +0000
2705@@ -76,6 +76,7 @@
2706 "vid": vid,
2707 "mtu": mtu,
2708 })
2709+<<<<<<< TREE
2710 self.assertEqual(
2711 http.client.OK, response.status_code, response.content)
2712 response_data = json.loads(
2713@@ -83,6 +84,12 @@
2714 self.assertEqual(vlan_name, response_data['name'])
2715 self.assertEqual(vid, response_data['vid'])
2716 self.assertEqual(mtu, response_data['mtu'])
2717+=======
2718+ self.assertEqual(httplib.OK, response.status_code, response.content)
2719+ self.assertEqual(vlan_name, json.loads(response.content)['name'])
2720+ self.assertEqual(vid, json.loads(response.content)['vid'])
2721+ self.assertEqual(mtu, json.loads(response.content)['mtu'])
2722+>>>>>>> MERGE-SOURCE
2723
2724 def test_create_admin_only(self):
2725 fabric = factory.make_Fabric()
2726
2727=== modified file 'src/maasserver/api/tests/test_volume_groups.py'
2728--- src/maasserver/api/tests/test_volume_groups.py 2016-05-24 21:29:53 +0000
2729+++ src/maasserver/api/tests/test_volume_groups.py 2016-06-10 16:34:39 +0000
2730@@ -25,11 +25,19 @@
2731 from maasserver.models.partitiontable import PARTITION_TABLE_EXTRA_SPACE
2732 from maasserver.testing.api import APITestCase
2733 from maasserver.testing.factory import factory
2734+<<<<<<< TREE
2735 from maasserver.utils.converters import (
2736 human_readable_bytes,
2737 round_size_to_nearest_block,
2738 )
2739 from maasserver.utils.orm import reload_object
2740+=======
2741+from maasserver.testing.orm import reload_object
2742+from maasserver.utils.converters import (
2743+ human_readable_bytes,
2744+ round_size_to_nearest_block,
2745+)
2746+>>>>>>> MERGE-SOURCE
2747 from testtools.matchers import (
2748 ContainsDict,
2749 Equals,
2750@@ -416,12 +424,19 @@
2751 "uuid": vguuid,
2752 "size": size,
2753 })
2754+<<<<<<< TREE
2755 self.assertEqual(
2756 http.client.OK, response.status_code, response.content)
2757 logical_volume = json.loads(
2758 response.content.decode(settings.DEFAULT_CHARSET))
2759 expected_size = round_size_to_nearest_block(
2760 size, PARTITION_ALIGNMENT_SIZE, False)
2761+=======
2762+ self.assertEqual(httplib.OK, response.status_code, response.content)
2763+ logical_volume = json.loads(response.content)
2764+ expected_size = round_size_to_nearest_block(
2765+ size, PARTITION_ALIGNMENT_SIZE, False)
2766+>>>>>>> MERGE-SOURCE
2767 self.assertThat(logical_volume, ContainsDict({
2768 "name": Equals("%s-%s" % (volume_group.name, name)),
2769 "uuid": Equals(vguuid),
2770
2771=== modified file 'src/maasserver/api/vlans.py'
2772--- src/maasserver/api/vlans.py 2016-04-27 20:40:24 +0000
2773+++ src/maasserver/api/vlans.py 2016-06-10 16:34:39 +0000
2774@@ -21,11 +21,15 @@
2775 'name',
2776 'vid',
2777 'fabric',
2778+<<<<<<< TREE
2779 'mtu',
2780 'primary_rack',
2781 'secondary_rack',
2782 'dhcp_on',
2783 'external_dhcp',
2784+=======
2785+ 'mtu',
2786+>>>>>>> MERGE-SOURCE
2787 )
2788
2789
2790@@ -162,6 +166,7 @@
2791 :param description: Description of the VLAN.
2792 :type description: unicode
2793 :param vid: VLAN ID of the VLAN.
2794+<<<<<<< TREE
2795 :type vid: integer
2796 :param mtu: The MTU to use on the VLAN.
2797 :type mtu: integer
2798@@ -171,6 +176,11 @@
2799 :type primary_rack: system_id
2800 :param secondary_rack: The secondary rack controller manging the VLAN.
2801 :type secondary_rack: system_id
2802+=======
2803+ :type vid: integer
2804+ :param mtu: The MTU to use on the VLAN.
2805+ :type mtu: integer
2806+>>>>>>> MERGE-SOURCE
2807
2808 Returns 404 if the fabric or VLAN is not found.
2809 """
2810
2811=== modified file 'src/maasserver/dns/tests/test_config.py'
2812=== modified file 'src/maasserver/forms.py'
2813--- src/maasserver/forms.py 2016-05-17 20:28:10 +0000
2814+++ src/maasserver/forms.py 2016-06-10 16:34:39 +0000
2815@@ -120,7 +120,20 @@
2816 Zone,
2817 )
2818 from maasserver.models.blockdevice import MIN_BLOCK_DEVICE_SIZE
2819-from maasserver.models.partition import MIN_PARTITION_SIZE
2820+<<<<<<< TREE
2821+from maasserver.models.partition import MIN_PARTITION_SIZE
2822+=======
2823+from maasserver.models.node import (
2824+ fqdn_is_duplicate,
2825+ nodegroup_fqdn,
2826+)
2827+from maasserver.models.nodegroup import NODEGROUP_CLUSTER_NAME_TEMPLATE
2828+from maasserver.models.partition import MIN_PARTITION_SIZE
2829+from maasserver.models.subnet import (
2830+ create_cidr,
2831+ Subnet,
2832+)
2833+>>>>>>> MERGE-SOURCE
2834 from maasserver.node_action import (
2835 ACTION_CLASSES,
2836 ACTIONS_DICT,
2837
2838=== modified file 'src/maasserver/forms_interface.py'
2839--- src/maasserver/forms_interface.py 2016-04-22 17:28:15 +0000
2840+++ src/maasserver/forms_interface.py 2016-06-10 16:34:39 +0000
2841@@ -213,6 +213,18 @@
2842 "untagged VLAN.")
2843 return new_vlan
2844
2845+ def clean_vlan(self):
2846+ new_vlan = self.cleaned_data.get('vlan')
2847+ if new_vlan and new_vlan.fabric.get_default_vlan() != new_vlan:
2848+ # A device's physical interface can be connected to a tagged VLAN.
2849+ # This is because a container or VM could be bridged on the machine
2850+ # to a tagged VLAN interface. See lp:1572070 for details.
2851+ if self.node.installable:
2852+ raise ValidationError(
2853+ "A physical interface can only belong to an "
2854+ "untagged VLAN.")
2855+ return new_vlan
2856+
2857 def clean(self):
2858 cleaned_data = super(PhysicalInterfaceForm, self).clean()
2859 new_name = cleaned_data.get('name')
2860@@ -399,6 +411,33 @@
2861 'name',
2862 )
2863
2864+<<<<<<< TREE
2865+ def clean_vlan(self):
2866+ new_vlan = self.cleaned_data.get('vlan')
2867+ if new_vlan and new_vlan.fabric.get_default_vlan() != new_vlan:
2868+ raise ValidationError(
2869+ "A bond interface can only belong to an untagged VLAN.")
2870+ return new_vlan
2871+=======
2872+ def __init__(self, *args, **kwargs):
2873+ super(BondInterfaceForm, self).__init__(*args, **kwargs)
2874+ # Allow VLAN to be blank when creating.
2875+ instance = kwargs.get("instance", None)
2876+ if instance is not None and instance.id is not None:
2877+ self.fields['vlan'].required = True
2878+ else:
2879+ self.fields['vlan'].required = False
2880+
2881+ def clean_parents(self):
2882+ parents = self.get_clean_parents()
2883+ if parents is None:
2884+ return
2885+ if len(parents) < 1:
2886+ msg = "A Bond interface must have one or more parents."
2887+ raise ValidationError({'parents': [msg]})
2888+ return parents
2889+>>>>>>> MERGE-SOURCE
2890+
2891 def clean_vlan(self):
2892 new_vlan = self.cleaned_data.get('vlan')
2893 if new_vlan and new_vlan.fabric.get_default_vlan() != new_vlan:
2894@@ -407,8 +446,13 @@
2895 return new_vlan
2896
2897 def clean(self):
2898+<<<<<<< TREE
2899 cleaned_data = super().clean()
2900 if self.fields_ok(['vlan', 'parents']):
2901+=======
2902+ cleaned_data = super(BondInterfaceForm, self).clean()
2903+ if self.fields_ok(['vlan', 'parents']):
2904+>>>>>>> MERGE-SOURCE
2905 parents = self.cleaned_data.get('parents')
2906 # Set the mac_address if its missing and the interface is being
2907 # created.
2908@@ -428,10 +472,36 @@
2909 parent.vlan
2910 for parent in parents
2911 }
2912+<<<<<<< TREE
2913 if parent_vlans != set([vlan]):
2914 set_form_error(
2915 self, 'parents',
2916 "All parents must belong to the same VLAN.")
2917+=======
2918+ if parents_with_other_children:
2919+ set_form_error(
2920+ self, 'parents',
2921+ "%s is already in-use by another interface." % (
2922+ ', '.join(sorted(parents_with_other_children))))
2923+
2924+ # When creating the bond set VLAN to the same as the parents
2925+ # and check that the parents all belong to the same VLAN.
2926+ if self.instance.id is None:
2927+ vlan = self.cleaned_data.get('vlan')
2928+ if vlan is None:
2929+ vlan = parents[0].vlan
2930+ self.cleaned_data['vlan'] = vlan
2931+ parent_vlans = {
2932+ parent.vlan
2933+ for parent in parents
2934+ }
2935+ if parent_vlans != set([vlan]):
2936+ set_form_error(
2937+ self, 'parents',
2938+ "All parents must belong to the same VLAN.")
2939+
2940+ return cleaned_data
2941+>>>>>>> MERGE-SOURCE
2942
2943 def set_extra_parameters(self, interface, created):
2944 """Set the bond parameters as well."""
2945@@ -454,6 +524,7 @@
2946 elif created:
2947 interface.params[bond_field] = self.fields[bond_field].initial
2948
2949+<<<<<<< TREE
2950
2951 class BridgeInterfaceForm(ChildInterfaceForm):
2952 """Form used to create/edit a bridge interface."""
2953@@ -476,6 +547,8 @@
2954 self._validate_parental_fidelity(parents)
2955 self._set_default_vlan(parents)
2956 return cleaned_data
2957+=======
2958+>>>>>>> MERGE-SOURCE
2959
2960 INTERFACE_FORM_MAPPING = {
2961 INTERFACE_TYPE.PHYSICAL: PhysicalInterfaceForm,
2962
2963=== modified file 'src/maasserver/forms_interface_link.py'
2964=== modified file 'src/maasserver/forms_subnet.py'
2965--- src/maasserver/forms_subnet.py 2016-05-11 19:01:48 +0000
2966+++ src/maasserver/forms_subnet.py 2016-06-10 16:34:39 +0000
2967@@ -8,8 +8,12 @@
2968 ]
2969
2970 from django import forms
2971+<<<<<<< TREE
2972 from maasserver.enum import RDNS_MODE_CHOICES
2973 from maasserver.fields import IPListFormField
2974+=======
2975+from maasserver.fields import IPListFormField
2976+>>>>>>> MERGE-SOURCE
2977 from maasserver.forms import MAASModelForm
2978 from maasserver.models.fabric import Fabric
2979 from maasserver.models.space import Space
2980@@ -60,12 +64,19 @@
2981
2982 def clean(self):
2983 cleaned_data = super(SubnetForm, self).clean()
2984+<<<<<<< TREE
2985 # The default value for allow_proxy is True.
2986 if 'allow_proxy' not in self.data:
2987 cleaned_data['allow_proxy'] = True
2988 # The ArrayField form has a bug which leaves out the first entry.
2989 if 'dns_servers' in self.data and self.data['dns_servers'] != '':
2990 cleaned_data['dns_servers'] = self.data.getlist('dns_servers')
2991+=======
2992+ # The djorm_pgarray.fields.ArrayField form has a bug which leaves out
2993+ # the first entry.
2994+ if 'dns_servers' in self.data and self.data['dns_servers'] != '':
2995+ cleaned_data['dns_servers'] = self.data.getlist('dns_servers')
2996+>>>>>>> MERGE-SOURCE
2997 cleaned_data = self._clean_name(cleaned_data)
2998 cleaned_data = self._clean_dns_servers(cleaned_data)
2999 if self.instance.id is None:
3000@@ -133,6 +144,7 @@
3001 if space is None:
3002 cleaned_data["space"] = Space.objects.get_default_space()
3003 return cleaned_data
3004+<<<<<<< TREE
3005
3006 def _clean_dns_servers(self, cleaned_data):
3007 dns_servers = cleaned_data.get("dns_servers", None)
3008@@ -146,3 +158,17 @@
3009 clean_dns_servers += ip_list_cleaned.split(" ")
3010 cleaned_data["dns_servers"] = clean_dns_servers
3011 return cleaned_data
3012+=======
3013+
3014+ def _clean_dns_servers(self, cleaned_data):
3015+ dns_servers = cleaned_data.get("dns_servers", None)
3016+ if dns_servers is None:
3017+ return cleaned_data
3018+ clean_dns_servers = []
3019+ for dns_server in dns_servers:
3020+ ip_list_form = IPListFormField()
3021+ ip_list_cleaned = ip_list_form.clean(dns_server)
3022+ clean_dns_servers += ip_list_cleaned.split(" ")
3023+ cleaned_data["dns_servers"] = clean_dns_servers
3024+ return cleaned_data
3025+>>>>>>> MERGE-SOURCE
3026
3027=== modified file 'src/maasserver/forms_vlan.py'
3028--- src/maasserver/forms_vlan.py 2016-04-27 20:38:06 +0000
3029+++ src/maasserver/forms_vlan.py 2016-06-10 16:34:39 +0000
3030@@ -8,8 +8,11 @@
3031 ]
3032
3033 from django import forms
3034+<<<<<<< TREE
3035 from django.core.exceptions import ValidationError
3036 from maasserver.fields import NodeChoiceField
3037+=======
3038+>>>>>>> MERGE-SOURCE
3039 from maasserver.forms import MAASModelForm
3040 from maasserver.models import RackController
3041 from maasserver.models.vlan import VLAN
3042@@ -63,6 +66,7 @@
3043
3044 def clean(self):
3045 cleaned_data = super(VLANForm, self).clean()
3046+<<<<<<< TREE
3047 # Automatically promote the secondary rack controller to the primary
3048 # if the primary is removed.
3049 if (not cleaned_data.get('primary_rack') and
3050@@ -103,6 +107,8 @@
3051 if (cleaned_data.get('secondary_rack') is None and
3052 self.instance.secondary_rack is not None):
3053 self.instance.secondary_rack = None
3054+=======
3055+>>>>>>> MERGE-SOURCE
3056 return cleaned_data
3057
3058 def clean_dhcp_on(self):
3059
3060=== modified file 'src/maasserver/management/commands/_config.py'
3061=== modified file 'src/maasserver/management/commands/tests/test_config.py'
3062=== modified file 'src/maasserver/middleware.py'
3063=== modified file 'src/maasserver/migrations/south/migrations/0181_initial_storage_layouts.py'
3064=== modified file 'src/maasserver/migrations/south/migrations/0182_initial_networking_layout.py'
3065=== modified file 'src/maasserver/migrations/south/migrations/0188_dli_power_driver_system_id_to_outlet_id.py'
3066=== modified file 'src/maasserver/models/__init__.py'
3067--- src/maasserver/models/__init__.py 2016-04-21 16:32:47 +0000
3068+++ src/maasserver/models/__init__.py 2016-06-10 16:34:39 +0000
3069@@ -256,6 +256,7 @@
3070 raise NotImplementedError(
3071 'Invalid permission check (invalid permission name: %s).' %
3072 perm)
3073+<<<<<<< TREE
3074 elif isinstance(obj, Interface):
3075 if perm == NODE_PERMISSION.VIEW:
3076 # Any registered user can view a interface regardless
3077@@ -287,6 +288,28 @@
3078 'Invalid permission check (invalid permission name: %s).' %
3079 perm)
3080 elif isinstance(obj, (Fabric, FanNetwork, Subnet, Space, VLAN)):
3081+=======
3082+ elif isinstance(obj, Interface):
3083+ if perm == NODE_PERMISSION.VIEW:
3084+ # Any registered user can view a interface regardless
3085+ # of its state.
3086+ return True
3087+ elif perm in NODE_PERMISSION.EDIT:
3088+ # A device can be editted by its owner a node must be admin.
3089+ node = obj.get_node()
3090+ if node is None or node.installable:
3091+ return user.is_superuser
3092+ else:
3093+ return node.owner == user
3094+ elif perm in NODE_PERMISSION.ADMIN:
3095+ # Admin permission is solely granted to superusers.
3096+ return user.is_superuser
3097+ else:
3098+ raise NotImplementedError(
3099+ 'Invalid permission check (invalid permission name: %s).' %
3100+ perm)
3101+ elif isinstance(obj, (Fabric, FanNetwork, Subnet, Space)):
3102+>>>>>>> MERGE-SOURCE
3103 if perm == NODE_PERMISSION.VIEW:
3104 # Any registered user can view a fabric or interface regardless
3105 # of its state.
3106
3107=== modified file 'src/maasserver/models/blockdevice.py'
3108=== modified file 'src/maasserver/models/config.py'
3109=== modified file 'src/maasserver/models/fabric.py'
3110--- src/maasserver/models/fabric.py 2016-04-27 20:18:20 +0000
3111+++ src/maasserver/models/fabric.py 2016-06-10 16:34:39 +0000
3112@@ -164,8 +164,13 @@
3113
3114 objects = FabricManager()
3115
3116+<<<<<<< TREE
3117 # We don't actually allow blank or null name, but that is enforced in
3118 # clean() and save().
3119+=======
3120+ # We don't actually allow blank or null name, but that is enforced in
3121+ # clean() and save(). Ditto for unique.
3122+>>>>>>> MERGE-SOURCE
3123 name = CharField(
3124 max_length=256, editable=True, null=True, blank=True, unique=True,
3125 validators=[validate_fabric_name])
3126@@ -224,6 +229,7 @@
3127 # Name will get set by clean_name() if None or empty, and there is an
3128 # id. We just need to handle names here for creation.
3129 created = self.id is None
3130+<<<<<<< TREE
3131 super().save(*args, **kwargs)
3132 if self.name is None or self.name == '':
3133 # If we got here, then we have a newly created fabric that needs a
3134@@ -231,21 +237,46 @@
3135 self.name = "fabric-%d" % self.id
3136 fabric = Fabric.objects.get(id=self.id)
3137 fabric.save()
3138+=======
3139+ super(Fabric, self).save(*args, **kwargs)
3140+ if self.name is None or self.name == '':
3141+ # If we got here, then we have a newly created fabric that needs a
3142+ # default name.
3143+ self.name = "fabric-%d" % self.id
3144+ fabric = Fabric.objects.get(id=self.id)
3145+ fabric.save()
3146+>>>>>>> MERGE-SOURCE
3147 # Create default VLAN if this is a fabric creation.
3148 if created:
3149 self._create_default_vlan()
3150
3151 def clean_name(self):
3152- reserved = re.compile('^fabric-\d+$')
3153- if self.name is not None and self.name != '':
3154- if reserved.search(self.name):
3155- if (self.id is None or self.name != 'fabric-%d' % self.id):
3156- raise ValidationError(
3157- {'name': ["Reserved fabric name."]})
3158- elif self.id is not None:
3159- # Since we are not creating the fabric, force the (null or empty)
3160- # name to be the default name.
3161- self.name = "fabric-%d" % self.id
3162+<<<<<<< TREE
3163+ reserved = re.compile('^fabric-\d+$')
3164+ if self.name is not None and self.name != '':
3165+ if reserved.search(self.name):
3166+ if (self.id is None or self.name != 'fabric-%d' % self.id):
3167+ raise ValidationError(
3168+ {'name': ["Reserved fabric name."]})
3169+ elif self.id is not None:
3170+ # Since we are not creating the fabric, force the (null or empty)
3171+ # name to be the default name.
3172+ self.name = "fabric-%d" % self.id
3173+=======
3174+ reserved = re.compile('^fabric-\d+$')
3175+ if self.name is not None and self.name != '':
3176+ if reserved.search(self.name):
3177+ if (self.id is None or self.name != 'fabric-%d' % self.id):
3178+ raise ValidationError(
3179+ {'name': ["Reserved fabric name."]})
3180+ elif self.id is not None:
3181+ # Since we are not creating the fabric, force the (null or empty)
3182+ # name to be the default name.
3183+ self.name = "fabric-%d" % self.id
3184+ name_count = Fabric.objects.filter(name=self.name).count()
3185+ if name_count > 0 + (self.id is not None):
3186+ raise ValidationError("Duplicate name: %s" % self.name)
3187+>>>>>>> MERGE-SOURCE
3188
3189 def clean(self, *args, **kwargs):
3190 super().clean(*args, **kwargs)
3191
3192=== modified file 'src/maasserver/models/filestorage.py'
3193=== modified file 'src/maasserver/models/filesystemgroup.py'
3194=== modified file 'src/maasserver/models/interface.py'
3195--- src/maasserver/models/interface.py 2016-05-12 19:07:37 +0000
3196+++ src/maasserver/models/interface.py 2016-06-10 16:34:39 +0000
3197@@ -36,6 +36,7 @@
3198 IPADDRESS_TYPE,
3199 )
3200 from maasserver.exceptions import (
3201+ StaticIPAddressExhaustion,
3202 StaticIPAddressOutOfRange,
3203 StaticIPAddressUnavailable,
3204 )
3205@@ -420,6 +421,7 @@
3206 def get_node(self):
3207 return self.node
3208
3209+<<<<<<< TREE
3210 def get_log_string(self):
3211 hostname = "<unknown-node>"
3212 node = self.get_node()
3213@@ -427,6 +429,15 @@
3214 hostname = node.hostname
3215 return "%s (%s) on %s" % (self.get_name(), self.type, hostname)
3216
3217+=======
3218+ def get_log_string(self):
3219+ hostname = "<unknown-node>"
3220+ node = self.get_node()
3221+ if node is not None:
3222+ hostname = node.hostname
3223+ return "%s on %s" % (self.get_name(), hostname)
3224+
3225+>>>>>>> MERGE-SOURCE
3226 def get_name(self):
3227 return self.name
3228
3229@@ -748,11 +759,24 @@
3230 the same VLAN as the interface. If no subnet could be identified then
3231 its just set to DHCP.
3232 """
3233+<<<<<<< TREE
3234 # XXX mpontillo 2015-11-29: since we tend to dump a large number of
3235 # subnets into the default VLAN, this assumption might be incorrect in
3236 # many cases, leading to interfaces being configured as AUTO when
3237 # they should be configured as DHCP.
3238 found_subnet = self.vlan.subnet_set.first()
3239+=======
3240+ found_subnet = None
3241+ # XXX mpontillo 2015-11-29: since we tend to dump a large number of
3242+ # subnets into the default VLAN, this assumption might be incorrect in
3243+ # many cases, leading to interfaces being configured as AUTO when
3244+ # they should be configured as DHCP.
3245+ for subnet in self.vlan.subnet_set.all():
3246+ ngi = subnet.get_managed_cluster_interface()
3247+ if ngi is not None:
3248+ found_subnet = subnet
3249+ break
3250+>>>>>>> MERGE-SOURCE
3251 if found_subnet is not None:
3252 return self.link_subnet(INTERFACE_LINK_TYPE.AUTO, found_subnet)
3253 else:
3254@@ -890,16 +914,36 @@
3255 for auto_ip in self.ip_addresses.filter(
3256 alloc_type=IPADDRESS_TYPE.AUTO):
3257 if not auto_ip.ip:
3258+<<<<<<< TREE
3259 assigned_ip = self._claim_auto_ip(
3260 auto_ip, exclude_addresses=exclude_addresses)
3261 if assigned_ip is not None:
3262 assigned_addresses.append(assigned_ip)
3263 exclude_addresses.add(str(assigned_ip.ip))
3264+=======
3265+ ngi, assigned_ip = self._claim_auto_ip(
3266+ auto_ip, exclude_addresses)
3267+ if ngi is not None:
3268+ affected_nodegroups.add(ngi.nodegroup)
3269+ if assigned_ip is not None:
3270+ assigned_addresses.append(assigned_ip)
3271+ exclude_addresses.add(unicode(assigned_ip.ip))
3272+ self._update_dns_zones(affected_nodegroups)
3273+>>>>>>> MERGE-SOURCE
3274 return assigned_addresses
3275
3276 def _claim_auto_ip(self, auto_ip, exclude_addresses=[]):
3277+<<<<<<< TREE
3278 """Claim an IP address for the `auto_ip`."""
3279+=======
3280+ """Claim an IP address for the `auto_ip`.
3281+
3282+ :returns:NodeGroupInterface, new_ip_address
3283+ """
3284+ # Check if already has a hostmap allocated for this MAC address.
3285+>>>>>>> MERGE-SOURCE
3286 subnet = auto_ip.subnet
3287+<<<<<<< TREE
3288 if subnet is None:
3289 maaslog.error(
3290 "Could not find subnet for interface %s." %
3291@@ -910,14 +954,93 @@
3292
3293 # Allocate a new IP address from the entire subnet, excluding already
3294 # allocated addresses and ranges.
3295+=======
3296+ if subnet is None:
3297+ maaslog.error(
3298+ "Could not find subnet for interface %s." %
3299+ (self.get_log_string()))
3300+ raise StaticIPAddressUnavailable(
3301+ "Automatic IP address cannot be configured on interface %s "
3302+ "without an associated subnet." % self.get_name())
3303+
3304+ ngi = subnet.get_managed_cluster_interface()
3305+ if ngi is None:
3306+ # Couldn't find a managed cluster interface for this node. So look
3307+ # for any interface (must be an UNMANAGED interface, since any
3308+ # managed NodeGroupInterface MUST have a Subnet link) whose
3309+ # static or dynamic range is within the given subnet.
3310+ ngi = NodeGroupInterface.objects.get_by_managed_range_for_subnet(
3311+ subnet)
3312+
3313+ has_existing_mapping = False
3314+ has_static_range = False
3315+ has_dynamic_range = False
3316+
3317+ if ngi is not None:
3318+ has_existing_mapping = self._has_static_allocation_on_cluster(
3319+ ngi.nodegroup, get_subnet_family(subnet))
3320+ has_static_range = ngi.has_static_ip_range()
3321+ has_dynamic_range = ngi.has_dynamic_ip_range()
3322+
3323+ if not has_static_range and has_dynamic_range:
3324+ # This means we found a matching NodeGroupInterface, but only its
3325+ # dynamic range is defined. Since a dynamic range is defined, that
3326+ # means this subnet is NOT managed by MAAS (or it's misconfigured),
3327+ # so we cannot just hand out a random IP address and risk a
3328+ # duplicate IP address.
3329+ maaslog.error(
3330+ "Found matching NodeGroupInterface, but no static range has "
3331+ "been defined for %s. (did you mean to configure DHCP?) " %
3332+ (self.get_log_string()))
3333+ raise StaticIPAddressUnavailable(
3334+ "Cluster interface for %s only has a dynamic range. Configure "
3335+ "a static range, or reconfigure the interface." %
3336+ (self.get_name()))
3337+
3338+ if has_static_range:
3339+ # Allocate a new AUTO address from the static range.
3340+ network = ngi.network
3341+ static_ip_range_low = ngi.static_ip_range_low
3342+ static_ip_range_high = ngi.static_ip_range_high
3343+ else:
3344+ # We either found a NodeGroupInterface with no static or dynamic
3345+ # range, or we have a Subnet not associated with a
3346+ # NodeGroupInterface. This implies that it's okay to assign any
3347+ # unused IP address on the subnet.
3348+ network = subnet.get_ipnetwork()
3349+ static_ip_range_low, static_ip_range_high = (
3350+ get_first_and_last_usable_host_in_network(network))
3351+ in_use_ipset = subnet.get_ipranges_in_use()
3352+>>>>>>> MERGE-SOURCE
3353 new_ip = StaticIPAddress.objects.allocate_new(
3354+<<<<<<< TREE
3355 subnet=subnet, alloc_type=IPADDRESS_TYPE.AUTO,
3356 exclude_addresses=exclude_addresses)
3357+=======
3358+ network, static_ip_range_low, static_ip_range_high,
3359+ None, None, alloc_type=IPADDRESS_TYPE.AUTO,
3360+ subnet=subnet, exclude_addresses=exclude_addresses,
3361+ in_use_ipset=in_use_ipset)
3362+>>>>>>> MERGE-SOURCE
3363 self.ip_addresses.add(new_ip)
3364+<<<<<<< TREE
3365 maaslog.info("Allocated automatic IP address %s for %s." % (
3366 new_ip.ip,
3367 self.get_log_string()))
3368
3369+=======
3370+ maaslog.info("Allocated automatic%s IP address %s for %s." % (
3371+ " static" if has_static_range else "", new_ip.ip,
3372+ self.get_log_string()))
3373+
3374+ if ngi is not None and not has_existing_mapping:
3375+ # Update DHCP (if needed).
3376+ self._update_host_maps(ngi.nodegroup, new_ip)
3377+
3378+ # If we made it this far, then the AUTO IP address has been assigned
3379+ # and the hostmap has been updated if needed. We can now remove the
3380+ # original empty AUTO IP address.
3381+>>>>>>> MERGE-SOURCE
3382 auto_ip.delete()
3383 return new_ip
3384
3385@@ -940,6 +1063,7 @@
3386 subnet = auto_ip.subnet
3387 auto_ip.ip = None
3388 auto_ip.save()
3389+<<<<<<< TREE
3390 return subnet, auto_ip
3391
3392 def get_ancestors(self):
3393@@ -971,6 +1095,154 @@
3394 for ancestor in ancestors:
3395 all_related |= ancestor.get_successors()
3396 return all_related
3397+=======
3398+
3399+ # If this IP address was registered on the cluster and now has been
3400+ # deleted we need to register the next assigned IP address to the
3401+ # cluster hostmap.
3402+ if registered_on_cluster and ngi is not None:
3403+ new_hostmap_ip = self._get_first_static_allocation_for_cluster(
3404+ ngi.nodegroup, ip_family)
3405+ if new_hostmap_ip is not None:
3406+ self._update_host_maps(ngi.nodegroup, new_hostmap_ip)
3407+ return ngi, auto_ip
3408+
3409+ def claim_static_ips(self, requested_address=None):
3410+ """Assign static IP addresses to this Interface.
3411+
3412+ Allocates one address per managed cluster interface connected to this
3413+ MAC. Typically this will be either just one IPv4 address, or an IPv4
3414+ address and an IPv6 address.
3415+
3416+ :param requested_address: Optional IP address to claim. Must be in
3417+ the range defined on some cluter interface to which this
3418+ interface is related. If given, no allocations will be made on
3419+ any other cluster interfaces the MAC may be connected to.
3420+ :return: A list of :class:`StaticIPAddress`. Returns empty if
3421+ the cluster_interface is not yet known, or the
3422+ static_ip_range_low/high values values are not set on the
3423+ cluster_interface.
3424+ """
3425+ # This method depends on a database isolation level of SERIALIZABLE
3426+ # (or perhaps REPEATABLE READ) to avoid race conditions.
3427+
3428+ # If the interface already has static addresses then we just return
3429+ # those.
3430+ existing_statics = [
3431+ ip_address
3432+ for ip_address in self.ip_addresses.filter(
3433+ alloc_type=IPADDRESS_TYPE.STICKY, ip__isnull=False)
3434+ if ip_address.ip
3435+ ]
3436+ if len(existing_statics) > 0:
3437+ return existing_statics
3438+
3439+ parent = self._get_parent_node()
3440+ # Get the last subnets this interface DHCP'd from. This with be either
3441+ # one IPv4, one IPv6, or both IPv4 and IPv6.
3442+ if parent is not None:
3443+ # If this interface is on a device then we need to look for
3444+ # discovered addresses on all the Node's interfaces.
3445+ discovered_ips = StaticIPAddress.objects.none()
3446+ for interface in parent.interface_set.all():
3447+ ip_addresses = interface.ip_addresses.filter(
3448+ alloc_type=IPADDRESS_TYPE.DISCOVERED)
3449+ ip_addresses = ip_addresses.order_by(
3450+ 'id').select_related("subnet")
3451+ discovered_ips |= ip_addresses
3452+ else:
3453+ discovered_ips = self.ip_addresses.filter(
3454+ alloc_type=IPADDRESS_TYPE.DISCOVERED)
3455+ discovered_ips = discovered_ips.order_by(
3456+ 'id').select_related("subnet")
3457+
3458+ discovered_subnets = [
3459+ discovered_ip.subnet for discovered_ip in discovered_ips
3460+ ]
3461+
3462+ if len(discovered_subnets) == 0:
3463+ # Backward compatibility code. When databases are migrated from 1.8
3464+ # and earlier, we may not have DISCOVERED addresses yet. So
3465+ # try to find a subnet on an attached cluster interface.
3466+ # (Note that get_cluster_interface() handles getting the parent
3467+ # node, if needed.)
3468+ for ngi in self.get_cluster_interfaces():
3469+ if ngi is not None and ngi.subnet is not None:
3470+ discovered_subnets.append(ngi.subnet)
3471+
3472+ # This must be a set because it is highly possible that the parent
3473+ # has multiple subnets on the same interface or same subnet on multiple
3474+ # interfaces. We only want to allocate one ip address per subnet.
3475+ discovered_subnets = set(discovered_subnets)
3476+
3477+ if len(discovered_subnets) == 0:
3478+ node = self.node
3479+ if parent is not None:
3480+ node = parent
3481+ if node is None:
3482+ hostname = "<unknown>"
3483+ else:
3484+ hostname = "'%s'" % node.hostname
3485+ log_string = (
3486+ "%s: Attempted to claim a static IP address, but no "
3487+ "associated subnet could be found. (Recommission node %s "
3488+ "in order for MAAS to discover the subnet.)" %
3489+ (self.get_log_string(), hostname)
3490+ )
3491+ maaslog.warning(log_string)
3492+ raise StaticIPAddressExhaustion(log_string)
3493+
3494+ if requested_address is None:
3495+ # No requested address so claim a STATIC IP on all DISCOVERED
3496+ # subnets for this interface.
3497+ static_ips = []
3498+ for discovered_subnet in discovered_subnets:
3499+ ngi = discovered_subnet.get_managed_cluster_interface()
3500+ if ngi is not None:
3501+ static_ips.append(
3502+ self.link_subnet(
3503+ INTERFACE_LINK_TYPE.STATIC, discovered_subnet))
3504+
3505+ # No valid subnets could be used to claim a STATIC IP address.
3506+ if not any(static_ips):
3507+ maaslog.error(
3508+ "Attempted sticky IP allocation failed for %s: could not "
3509+ "find a cluster interface.", self.get_log_string())
3510+ return []
3511+ else:
3512+ return static_ips
3513+ else:
3514+ # Find the DISCOVERED subnet that the requested_address falls into.
3515+ found_subnet = None
3516+ for discovered_subnet in discovered_subnets:
3517+ if (IPAddress(requested_address) in
3518+ discovered_subnet.get_ipnetwork()):
3519+ found_subnet = discovered_subnet
3520+ break
3521+
3522+ if found_subnet:
3523+ return [
3524+ self.link_subnet(
3525+ INTERFACE_LINK_TYPE.STATIC, found_subnet,
3526+ ip_address=requested_address),
3527+ ]
3528+ else:
3529+ raise StaticIPAddressOutOfRange(
3530+ "requested_address '%s' is not in a managed subnet for "
3531+ "interface '%s'." % (
3532+ requested_address, self.get_name()))
3533+
3534+ def _get_parent_node(self):
3535+ """Return the parent node for this interface, if it exists (and this
3536+ interface belongs to a Device). Otherwise, return None.
3537+ """
3538+ if (self.node is not None and
3539+ not self.node.installable and
3540+ self.node.parent is not None):
3541+ return self.node.parent
3542+ else:
3543+ return None
3544+>>>>>>> MERGE-SOURCE
3545
3546 def delete(self, remove_ip_address=True):
3547 # We set the _skip_ip_address_removal so the signal can use it to
3548
3549=== modified file 'src/maasserver/models/migrations/create_default_storage_layout.py'
3550--- src/maasserver/models/migrations/create_default_storage_layout.py 2016-05-11 19:01:48 +0000
3551+++ src/maasserver/models/migrations/create_default_storage_layout.py 2016-06-10 16:34:39 +0000
3552@@ -1,97 +1,201 @@
3553-# Copyright 2015 Canonical Ltd. This software is licensed under the
3554-# GNU Affero General Public License version 3 (see the file LICENSE).
3555-
3556-"""Migration to create the default storage layout on the boot disk.
3557-
3558-WARNING: Although these methods will become obsolete very quickly, they
3559-cannot be removed, since they are used by the
3560-0181_initial_storage_layouts DataMigration. (changing them might also
3561-be futile unless a customer restores from a backup, since any bugs that occur
3562-will have already occurred, and this code will not be executed again.)
3563-
3564-Note: Each helper must have its dependencies on any model classes injected,
3565-since the migration environment is a skeletal replication of the 'real'
3566-database model. So each function takes as parameters the model classes it
3567-requires. Importing from the model is not allowed here. (but the unit tests
3568-do it, to ensure that the migrations meet validation requirements.)
3569-"""
3570-
3571-from __future__ import (
3572- absolute_import,
3573- print_function,
3574- unicode_literals,
3575- )
3576-
3577-str = None
3578-
3579-__metaclass__ = type
3580-__all__ = [
3581- "clear_full_storage_configuration",
3582-]
3583-
3584-from datetime import datetime
3585-from uuid import uuid4
3586-
3587-from maasserver.enum import (
3588- FILESYSTEM_TYPE,
3589- PARTITION_TABLE_TYPE,
3590-)
3591-from maasserver.models.partition import (
3592- MAX_PARTITION_SIZE_FOR_MBR,
3593- PARTITION_ALIGNMENT_SIZE,
3594-)
3595-from maasserver.models.partitiontable import PARTITION_TABLE_EXTRA_SPACE
3596-from maasserver.utils.converters import round_size_to_nearest_block
3597-
3598-
3599-def clear_full_storage_configuration(
3600- node,
3601- PhysicalBlockDevice, VirtualBlockDevice,
3602- PartitionTable, Filesystem, FilesystemGroup):
3603- """Clear's the full storage configuration for this node."""
3604- physical_block_devices = PhysicalBlockDevice.objects.filter(node=node)
3605- PartitionTable.objects.filter(
3606- block_device__in=physical_block_devices).delete()
3607- Filesystem.objects.filter(
3608- block_device__in=physical_block_devices).delete()
3609- for block_device in VirtualBlockDevice.objects.filter(node=node):
3610- try:
3611- block_device.filesystem_group.virtual_devices.all().delete()
3612- block_device.filesystem_group.delete()
3613- except FilesystemGroup.DoesNotExist:
3614- # When a filesystem group has multiple virtual block devices
3615- # it is possible that accessing `filesystem_group` will
3616- # result in it already being deleted.
3617- pass
3618-
3619-
3620-def create_flat_layout(
3621- node, boot_disk,
3622- PartitionTable, Partition, Filesystem):
3623- """Create the flat layout for the boot disk."""
3624- # Create the partition table and root partition.
3625- now = datetime.now()
3626- partition_table = PartitionTable.objects.create(
3627- block_device=boot_disk, table_type=PARTITION_TABLE_TYPE.MBR,
3628- created=now, updated=now)
3629- total_size = 0
3630- available_size = boot_disk.size - PARTITION_TABLE_EXTRA_SPACE
3631- partition_size = round_size_to_nearest_block(
3632- available_size, PARTITION_ALIGNMENT_SIZE, False)
3633- max_mbr_size = round_size_to_nearest_block(
3634- MAX_PARTITION_SIZE_FOR_MBR, PARTITION_ALIGNMENT_SIZE, False)
3635- if partition_size > max_mbr_size:
3636- partition_size = max_mbr_size
3637- available_size -= partition_size
3638- total_size += partition_size
3639- root_partition = Partition.objects.create(
3640- partition_table=partition_table, size=partition_size, bootable=True,
3641- created=now, updated=now, uuid=uuid4())
3642- Filesystem.objects.create(
3643- partition=root_partition,
3644- fstype=FILESYSTEM_TYPE.EXT4,
3645- label="root",
3646- mount_point="/",
3647- created=now,
3648- updated=now,
3649- uuid=uuid4())
3650+<<<<<<< TREE
3651+# Copyright 2015 Canonical Ltd. This software is licensed under the
3652+# GNU Affero General Public License version 3 (see the file LICENSE).
3653+
3654+"""Migration to create the default storage layout on the boot disk.
3655+
3656+WARNING: Although these methods will become obsolete very quickly, they
3657+cannot be removed, since they are used by the
3658+0181_initial_storage_layouts DataMigration. (changing them might also
3659+be futile unless a customer restores from a backup, since any bugs that occur
3660+will have already occurred, and this code will not be executed again.)
3661+
3662+Note: Each helper must have its dependencies on any model classes injected,
3663+since the migration environment is a skeletal replication of the 'real'
3664+database model. So each function takes as parameters the model classes it
3665+requires. Importing from the model is not allowed here. (but the unit tests
3666+do it, to ensure that the migrations meet validation requirements.)
3667+"""
3668+
3669+from __future__ import (
3670+ absolute_import,
3671+ print_function,
3672+ unicode_literals,
3673+ )
3674+
3675+str = None
3676+
3677+__metaclass__ = type
3678+__all__ = [
3679+ "clear_full_storage_configuration",
3680+]
3681+
3682+from datetime import datetime
3683+from uuid import uuid4
3684+
3685+from maasserver.enum import (
3686+ FILESYSTEM_TYPE,
3687+ PARTITION_TABLE_TYPE,
3688+)
3689+from maasserver.models.partition import (
3690+ MAX_PARTITION_SIZE_FOR_MBR,
3691+ PARTITION_ALIGNMENT_SIZE,
3692+)
3693+from maasserver.models.partitiontable import PARTITION_TABLE_EXTRA_SPACE
3694+from maasserver.utils.converters import round_size_to_nearest_block
3695+
3696+
3697+def clear_full_storage_configuration(
3698+ node,
3699+ PhysicalBlockDevice, VirtualBlockDevice,
3700+ PartitionTable, Filesystem, FilesystemGroup):
3701+ """Clear's the full storage configuration for this node."""
3702+ physical_block_devices = PhysicalBlockDevice.objects.filter(node=node)
3703+ PartitionTable.objects.filter(
3704+ block_device__in=physical_block_devices).delete()
3705+ Filesystem.objects.filter(
3706+ block_device__in=physical_block_devices).delete()
3707+ for block_device in VirtualBlockDevice.objects.filter(node=node):
3708+ try:
3709+ block_device.filesystem_group.virtual_devices.all().delete()
3710+ block_device.filesystem_group.delete()
3711+ except FilesystemGroup.DoesNotExist:
3712+ # When a filesystem group has multiple virtual block devices
3713+ # it is possible that accessing `filesystem_group` will
3714+ # result in it already being deleted.
3715+ pass
3716+
3717+
3718+def create_flat_layout(
3719+ node, boot_disk,
3720+ PartitionTable, Partition, Filesystem):
3721+ """Create the flat layout for the boot disk."""
3722+ # Create the partition table and root partition.
3723+ now = datetime.now()
3724+ partition_table = PartitionTable.objects.create(
3725+ block_device=boot_disk, table_type=PARTITION_TABLE_TYPE.MBR,
3726+ created=now, updated=now)
3727+ total_size = 0
3728+ available_size = boot_disk.size - PARTITION_TABLE_EXTRA_SPACE
3729+ partition_size = round_size_to_nearest_block(
3730+ available_size, PARTITION_ALIGNMENT_SIZE, False)
3731+ max_mbr_size = round_size_to_nearest_block(
3732+ MAX_PARTITION_SIZE_FOR_MBR, PARTITION_ALIGNMENT_SIZE, False)
3733+ if partition_size > max_mbr_size:
3734+ partition_size = max_mbr_size
3735+ available_size -= partition_size
3736+ total_size += partition_size
3737+ root_partition = Partition.objects.create(
3738+ partition_table=partition_table, size=partition_size, bootable=True,
3739+ created=now, updated=now, uuid=uuid4())
3740+ Filesystem.objects.create(
3741+ partition=root_partition,
3742+ fstype=FILESYSTEM_TYPE.EXT4,
3743+ label="root",
3744+ mount_point="/",
3745+ created=now,
3746+ updated=now,
3747+ uuid=uuid4())
3748+=======
3749+# Copyright 2015 Canonical Ltd. This software is licensed under the
3750+# GNU Affero General Public License version 3 (see the file LICENSE).
3751+
3752+"""Migration to create the default storage layout on the boot disk.
3753+
3754+WARNING: Although these methods will become obsolete very quickly, they
3755+cannot be removed, since they are used by the
3756+0181_initial_storage_layouts DataMigration. (changing them might also
3757+be futile unless a customer restores from a backup, since any bugs that occur
3758+will have already occurred, and this code will not be executed again.)
3759+
3760+Note: Each helper must have its dependencies on any model classes injected,
3761+since the migration environment is a skeletal replication of the 'real'
3762+database model. So each function takes as parameters the model classes it
3763+requires. Importing from the model is not allowed here. (but the unit tests
3764+do it, to ensure that the migrations meet validation requirements.)
3765+"""
3766+
3767+from __future__ import (
3768+ absolute_import,
3769+ print_function,
3770+ unicode_literals,
3771+ )
3772+
3773+str = None
3774+
3775+__metaclass__ = type
3776+__all__ = [
3777+ "clear_full_storage_configuration",
3778+ "create_lvm_layout",
3779+]
3780+
3781+from datetime import datetime
3782+from uuid import uuid4
3783+
3784+from maasserver.enum import (
3785+ FILESYSTEM_GROUP_TYPE,
3786+ FILESYSTEM_TYPE,
3787+ PARTITION_TABLE_TYPE,
3788+)
3789+from maasserver.models.filesystemgroup import LVM_PE_SIZE
3790+from maasserver.models.partition import (
3791+ MAX_PARTITION_SIZE_FOR_MBR,
3792+ MIN_PARTITION_SIZE,
3793+ PARTITION_ALIGNMENT_SIZE,
3794+)
3795+from maasserver.models.partitiontable import PARTITION_TABLE_EXTRA_SPACE
3796+from maasserver.utils.converters import round_size_to_nearest_block
3797+
3798+
3799+def clear_full_storage_configuration(
3800+ node,
3801+ PhysicalBlockDevice, VirtualBlockDevice,
3802+ PartitionTable, Filesystem, FilesystemGroup):
3803+ """Clear's the full storage configuration for this node."""
3804+ physical_block_devices = PhysicalBlockDevice.objects.filter(node=node)
3805+ PartitionTable.objects.filter(
3806+ block_device__in=physical_block_devices).delete()
3807+ Filesystem.objects.filter(
3808+ block_device__in=physical_block_devices).delete()
3809+ for block_device in VirtualBlockDevice.objects.filter(node=node):
3810+ try:
3811+ block_device.filesystem_group.virtual_devices.all().delete()
3812+ block_device.filesystem_group.delete()
3813+ except FilesystemGroup.DoesNotExist:
3814+ # When a filesystem group has multiple virtual block devices
3815+ # it is possible that accessing `filesystem_group` will
3816+ # result in it already being deleted.
3817+ pass
3818+
3819+
3820+def create_flat_layout(
3821+ node, boot_disk,
3822+ PartitionTable, Partition, Filesystem):
3823+ """Create the flat layout for the boot disk."""
3824+ # Create the partition table and root partition.
3825+ now = datetime.now()
3826+ partition_table = PartitionTable.objects.create(
3827+ block_device=boot_disk, table_type=PARTITION_TABLE_TYPE.MBR,
3828+ created=now, updated=now)
3829+ total_size = 0
3830+ available_size = boot_disk.size - PARTITION_TABLE_EXTRA_SPACE
3831+ partition_size = round_size_to_nearest_block(
3832+ available_size, PARTITION_ALIGNMENT_SIZE, False)
3833+ max_mbr_size = round_size_to_nearest_block(
3834+ MAX_PARTITION_SIZE_FOR_MBR, PARTITION_ALIGNMENT_SIZE, False)
3835+ if partition_size > max_mbr_size:
3836+ partition_size = max_mbr_size
3837+ available_size -= partition_size
3838+ total_size += partition_size
3839+ root_partition = Partition.objects.create(
3840+ partition_table=partition_table, size=partition_size, bootable=True,
3841+ created=now, updated=now, uuid=uuid4())
3842+ Filesystem.objects.create(
3843+ partition=root_partition,
3844+ fstype=FILESYSTEM_TYPE.EXT4,
3845+ label="root",
3846+ mount_point="/",
3847+ created=now,
3848+ updated=now,
3849+ uuid=uuid4())
3850+>>>>>>> MERGE-SOURCE
3851
3852=== modified file 'src/maasserver/models/migrations/populate_subnets_helper.py'
3853=== modified file 'src/maasserver/models/migrations/tests/test_create_default_storage_layout.py'
3854--- src/maasserver/models/migrations/tests/test_create_default_storage_layout.py 2016-05-11 19:01:48 +0000
3855+++ src/maasserver/models/migrations/tests/test_create_default_storage_layout.py 2016-06-10 16:34:39 +0000
3856@@ -1,3 +1,4 @@
3857+<<<<<<< TREE
3858 # Copyright 2015-2016 Canonical Ltd. This software is licensed under the
3859 # GNU Affero General Public License version 3 (see the file LICENSE).
3860
3861@@ -135,3 +136,152 @@
3862 label="root",
3863 mount_point="/",
3864 ))
3865+=======
3866+# Copyright 2015 Canonical Ltd. This software is licensed under the
3867+# GNU Affero General Public License version 3 (see the file LICENSE).
3868+
3869+"""Tests for the `0181_initial_storage_layouts` migration.
3870+
3871+WARNING: These tests will become obsolete very quickly, as they are testing
3872+migrations against fields that may be removed. When these tests become
3873+obsolete, they should be skipped. The tests should be kept until at least
3874+the next release cycle (through MAAS 1.9) in case any bugs with this migration
3875+occur.
3876+"""
3877+
3878+from __future__ import (
3879+ absolute_import,
3880+ print_function,
3881+ unicode_literals,
3882+ )
3883+
3884+str = None
3885+
3886+__metaclass__ = type
3887+__all__ = []
3888+
3889+from maasserver.enum import (
3890+ FILESYSTEM_GROUP_TYPE,
3891+ FILESYSTEM_TYPE,
3892+)
3893+from maasserver.models import (
3894+ Filesystem,
3895+ FilesystemGroup,
3896+ Partition,
3897+ PartitionTable,
3898+ PhysicalBlockDevice,
3899+ VirtualBlockDevice,
3900+ VolumeGroup,
3901+)
3902+from maasserver.models.migrations.create_default_storage_layout import (
3903+ clear_full_storage_configuration,
3904+ create_flat_layout,
3905+)
3906+from maasserver.testing.factory import factory
3907+from maasserver.testing.orm import reload_object
3908+from maasserver.testing.testcase import MAASServerTestCase
3909+from testtools.matchers import (
3910+ Is,
3911+ MatchesStructure,
3912+ Not,
3913+)
3914+
3915+
3916+class TestClearFullStorageConfigration(MAASServerTestCase):
3917+
3918+ def test__clears_all_objects(self):
3919+ node = factory.make_Node()
3920+ physical_block_devices = [
3921+ factory.make_PhysicalBlockDevice(node=node, size=10 * 1000 ** 3)
3922+ for _ in range(3)
3923+ ]
3924+ filesystem = factory.make_Filesystem(
3925+ block_device=physical_block_devices[0])
3926+ partition_table = factory.make_PartitionTable(
3927+ block_device=physical_block_devices[1])
3928+ partition = factory.make_Partition(partition_table=partition_table)
3929+ fslvm = factory.make_Filesystem(
3930+ block_device=physical_block_devices[2],
3931+ fstype=FILESYSTEM_TYPE.LVM_PV)
3932+ vgroup = factory.make_FilesystemGroup(
3933+ group_type=FILESYSTEM_GROUP_TYPE.LVM_VG, filesystems=[fslvm])
3934+ vbd1 = factory.make_VirtualBlockDevice(
3935+ filesystem_group=vgroup, size=2 * 1000 ** 3)
3936+ vbd2 = factory.make_VirtualBlockDevice(
3937+ filesystem_group=vgroup, size=3 * 1000 ** 3)
3938+ filesystem_on_vbd1 = factory.make_Filesystem(
3939+ block_device=vbd1, fstype=FILESYSTEM_TYPE.LVM_PV)
3940+ vgroup_on_vgroup = factory.make_FilesystemGroup(
3941+ group_type=FILESYSTEM_GROUP_TYPE.LVM_VG,
3942+ filesystems=[filesystem_on_vbd1])
3943+ vbd3_on_vbd1 = factory.make_VirtualBlockDevice(
3944+ filesystem_group=vgroup_on_vgroup, size=1 * 1000 ** 3)
3945+ clear_full_storage_configuration(
3946+ node,
3947+ PhysicalBlockDevice=PhysicalBlockDevice,
3948+ VirtualBlockDevice=VirtualBlockDevice,
3949+ PartitionTable=PartitionTable,
3950+ Filesystem=Filesystem,
3951+ FilesystemGroup=FilesystemGroup)
3952+ for pbd in physical_block_devices:
3953+ self.expectThat(
3954+ reload_object(pbd), Not(Is(None)),
3955+ "Physical block device should not have been deleted.")
3956+ self.expectThat(
3957+ reload_object(filesystem), Is(None),
3958+ "Filesystem should have been removed.")
3959+ self.expectThat(
3960+ reload_object(partition_table), Is(None),
3961+ "PartitionTable should have been removed.")
3962+ self.expectThat(
3963+ reload_object(partition), Is(None),
3964+ "Partition should have been removed.")
3965+ self.expectThat(
3966+ reload_object(fslvm), Is(None),
3967+ "LVM PV Filesystem should have been removed.")
3968+ self.expectThat(
3969+ reload_object(vgroup), Is(None),
3970+ "Volume group should have been removed.")
3971+ self.expectThat(
3972+ reload_object(vbd1), Is(None),
3973+ "Virtual block device should have been removed.")
3974+ self.expectThat(
3975+ reload_object(vbd2), Is(None),
3976+ "Virtual block device should have been removed.")
3977+ self.expectThat(
3978+ reload_object(filesystem_on_vbd1), Is(None),
3979+ "Filesystem on virtual block device should have been removed.")
3980+ self.expectThat(
3981+ reload_object(vgroup_on_vgroup), Is(None),
3982+ "Volume group on virtual block device should have been removed.")
3983+ self.expectThat(
3984+ reload_object(vbd3_on_vbd1), Is(None),
3985+ "Virtual block device on another virtual block device should have "
3986+ "been removed.")
3987+
3988+
3989+class TestCreateFlatLayout(MAASServerTestCase):
3990+
3991+ def test__creates_layout_for_1TiB_disk(self):
3992+ node = factory.make_Node(with_boot_disk=False)
3993+ boot_disk = factory.make_PhysicalBlockDevice(
3994+ node=node, size=1024 ** 4, block_size=512)
3995+ create_flat_layout(
3996+ node,
3997+ boot_disk,
3998+ PartitionTable=PartitionTable,
3999+ Partition=Partition,
4000+ Filesystem=Filesystem)
4001+
4002+ # Validate the filesystem on the root partition.
4003+ partition_table = boot_disk.get_partitiontable()
4004+ partitions = partition_table.partitions.order_by('id').all()
4005+ root_partition = partitions[0]
4006+ self.assertThat(
4007+ root_partition.get_effective_filesystem(),
4008+ MatchesStructure.byEquality(
4009+ fstype=FILESYSTEM_TYPE.EXT4,
4010+ label="root",
4011+ mount_point="/",
4012+ ))
4013+>>>>>>> MERGE-SOURCE
4014
4015=== modified file 'src/maasserver/models/node.py'
4016--- src/maasserver/models/node.py 2016-06-08 16:11:14 +0000
4017+++ src/maasserver/models/node.py 2016-06-10 16:34:39 +0000
4018@@ -996,6 +996,7 @@
4019 event_action=action,
4020 event_description=description)
4021
4022+<<<<<<< TREE
4023 def storage_layout_issues(self):
4024 """Return any errors with the storage layout.
4025
4026@@ -1067,6 +1068,78 @@
4027 "deploy this node.")
4028 return issues
4029
4030+=======
4031+ def storage_layout_issues(self):
4032+ """Return any errors with the storage layout.
4033+
4034+ Checks that the node has / mounted. If / is mounted on bcache check
4035+ that /boot is mounted and is not on bcache."""
4036+ def on_bcache(obj):
4037+ if obj.type == "physical":
4038+ return False
4039+ elif obj.type == "partition":
4040+ return on_bcache(obj.partition_table.block_device)
4041+ for parent in obj.virtualblockdevice.get_parents():
4042+ if((parent.type != "physical" and on_bcache(parent)) or
4043+ (parent.get_effective_filesystem().fstype in
4044+ [FILESYSTEM_TYPE.BCACHE_CACHE,
4045+ FILESYSTEM_TYPE.BCACHE_BACKING])):
4046+ return True
4047+ return False
4048+
4049+ has_boot = False
4050+ root_mounted = False
4051+ root_on_bcache = False
4052+ boot_mounted = False
4053+
4054+ for block_device in self.blockdevice_set.all():
4055+ if block_device.is_boot_disk():
4056+ has_boot = True
4057+ pt = block_device.get_partitiontable()
4058+ if pt is not None:
4059+ for partition in pt.partitions.all():
4060+ fs = partition.get_effective_filesystem()
4061+ if fs is None:
4062+ continue
4063+ if fs.mount_point == '/':
4064+ root_mounted = True
4065+ if on_bcache(block_device):
4066+ root_on_bcache = True
4067+ elif (fs.mount_point == '/boot' and
4068+ not on_bcache(block_device)):
4069+ boot_mounted = True
4070+ else:
4071+ fs = block_device.get_effective_filesystem()
4072+ if fs is None:
4073+ continue
4074+ if fs.mount_point == '/':
4075+ root_mounted = True
4076+ if on_bcache(block_device):
4077+ root_on_bcache = True
4078+ elif fs.mount_point == '/boot' and not on_bcache(block_device):
4079+ boot_mounted = True
4080+ issues = []
4081+ if not has_boot:
4082+ issues.append(
4083+ "Specify a storage device to be able to deploy this node.")
4084+ if not root_mounted:
4085+ issues.append(
4086+ "Mount the root '/' filesystem to be able to deploy this "
4087+ "node.")
4088+ if root_mounted and root_on_bcache and not boot_mounted:
4089+ issues.append(
4090+ "This node cannot be deployed because it cannot boot from a "
4091+ "bcache volume. Mount /boot on a non-bcache device to be "
4092+ "able to deploy this node.")
4093+ if (not boot_mounted and "arm64" in self.architecture and
4094+ self.get_bios_boot_method() != "uefi"):
4095+ issues.append(
4096+ "This node cannot be deployed because it needs a separate "
4097+ "/boot partition. Mount /boot on a device to be able to "
4098+ "deploy this node.")
4099+ return issues
4100+
4101+>>>>>>> MERGE-SOURCE
4102 def on_network(self):
4103 """Return true if the node is connected to a managed network."""
4104 for interface in self.interface_set.all():
4105
4106=== added file 'src/maasserver/models/nodegroupinterface.py.OTHER'
4107--- src/maasserver/models/nodegroupinterface.py.OTHER 1970-01-01 00:00:00 +0000
4108+++ src/maasserver/models/nodegroupinterface.py.OTHER 2016-06-10 16:34:39 +0000
4109@@ -0,0 +1,764 @@
4110+# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
4111+# GNU Affero General Public License version 3 (see the file LICENSE).
4112+
4113+"""Model definition for NodeGroupInterface."""
4114+
4115+from __future__ import (
4116+ absolute_import,
4117+ print_function,
4118+ unicode_literals,
4119+ )
4120+
4121+str = None
4122+
4123+__metaclass__ = type
4124+__all__ = [
4125+ 'NodeGroupInterface',
4126+ ]
4127+
4128+
4129+from textwrap import dedent
4130+
4131+from django.core.exceptions import ValidationError
4132+from django.db.models import (
4133+ CharField,
4134+ ForeignKey,
4135+ IntegerField,
4136+ Manager,
4137+ PROTECT,
4138+)
4139+from django.db.models.signals import post_save
4140+from django.dispatch import receiver
4141+from maasserver import DefaultMeta
4142+from maasserver.enum import (
4143+ IPADDRESS_TYPE,
4144+ NODEGROUP_STATUS,
4145+ NODEGROUPINTERFACE_MANAGEMENT,
4146+ NODEGROUPINTERFACE_MANAGEMENT_CHOICES,
4147+ NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT,
4148+)
4149+from maasserver.exceptions import StaticIPAddressForbidden
4150+from maasserver.fields import (
4151+ MAASIPAddressField,
4152+ VerboseRegexValidator,
4153+)
4154+from maasserver.models.cleansave import CleanSave
4155+from maasserver.models.nodegroup import NodeGroup
4156+from maasserver.models.staticipaddress import StaticIPAddress
4157+from maasserver.models.timestampedmodel import TimestampedModel
4158+from netaddr import (
4159+ IPAddress,
4160+ IPNetwork,
4161+)
4162+from netaddr.core import AddrFormatError
4163+from provisioningserver.logger import get_maas_logger
4164+from provisioningserver.utils.network import (
4165+ intersect_iprange,
4166+ make_ipaddress,
4167+ make_iprange,
4168+ make_network,
4169+)
4170+
4171+
4172+maaslog = get_maas_logger("nodegroupinterface")
4173+
4174+
4175+class NodeGroupInterfaceManager(Manager):
4176+ """Manager for NodeGroupInterface objects"""
4177+
4178+ find_by_network_for_static_allocation_query = dedent("""\
4179+ SELECT ngi.*
4180+ FROM maasserver_nodegroup AS ng,
4181+ maasserver_nodegroupinterface AS ngi,
4182+ maasserver_subnet AS subnet
4183+ WHERE ng.status = %s
4184+ AND ngi.nodegroup_id = ng.id
4185+ AND ngi.subnet_id = subnet.id
4186+ AND ngi.static_ip_range_low IS NOT NULL
4187+ AND ngi.static_ip_range_high IS NOT NULL
4188+ AND (ngi.ip & netmask(subnet.cidr)) = (INET %s)
4189+ ORDER BY ng.id, ngi.id
4190+ """)
4191+
4192+ def find_by_network_for_static_allocation(self, network):
4193+ """Find all cluster interfaces with the given network.
4194+
4195+ Furthermore, each interface must also have a static range defined.
4196+ """
4197+ assert isinstance(network, IPNetwork), (
4198+ "%r is not an IPNetwork" % (network,))
4199+ return self.raw(
4200+ self.find_by_network_for_static_allocation_query,
4201+ [NODEGROUP_STATUS.ENABLED, network.network.format()])
4202+
4203+ def get_by_network_for_static_allocation(self, network):
4204+ """Return the first cluster interface with the given network.
4205+
4206+ Furthermore, the interface must also have a static range defined.
4207+ """
4208+ assert isinstance(network, IPNetwork), (
4209+ "%r is not an IPNetwork" % (network,))
4210+ interfaces = self.raw(
4211+ self.find_by_network_for_static_allocation_query + " LIMIT 1",
4212+ [NODEGROUP_STATUS.ENABLED, network.network.format()])
4213+ for interface in interfaces:
4214+ return interface # This is stable because the query is ordered.
4215+ else:
4216+ return None
4217+
4218+ find_by_address_query = dedent("""\
4219+ SELECT ngi.*
4220+ FROM maasserver_nodegroup AS ng,
4221+ maasserver_nodegroupinterface AS ngi,
4222+ maasserver_subnet AS subnet
4223+ WHERE ng.status = %s
4224+ AND ngi.nodegroup_id = ng.id
4225+ AND ngi.subnet_id = subnet.id
4226+ AND (ngi.ip & netmask(subnet.cidr)) = (INET %s & netmask(subnet.cidr))
4227+ ORDER BY ng.id, ngi.id
4228+ """)
4229+
4230+ def find_by_address(self, address):
4231+ """Find all cluster interfaces for a given address."""
4232+ assert isinstance(address, IPAddress), (
4233+ "%r is not an IPAddress" % (address,))
4234+ return self.raw(
4235+ self.find_by_address_query,
4236+ [NODEGROUP_STATUS.ENABLED, address.format()])
4237+
4238+ def get_by_address(self, address):
4239+ """Return the first interface that could contain `address`."""
4240+ assert isinstance(address, IPAddress), (
4241+ "%r is not an IPAddress" % (address,))
4242+ interfaces = self.raw(
4243+ self.find_by_address_query + " LIMIT 1",
4244+ [NODEGROUP_STATUS.ENABLED, address.format()])
4245+ for interface in interfaces:
4246+ return interface # This is stable because the query is ordered.
4247+ else:
4248+ return None
4249+
4250+ find_by_address_for_static_allocation_query = dedent("""\
4251+ SELECT ngi.*
4252+ FROM maasserver_nodegroup AS ng,
4253+ maasserver_nodegroupinterface AS ngi,
4254+ maasserver_subnet AS subnet
4255+ WHERE ng.status = %s
4256+ AND ngi.nodegroup_id = ng.id
4257+ AND ngi.subnet_id = subnet.id
4258+ AND ngi.static_ip_range_low IS NOT NULL
4259+ AND ngi.static_ip_range_high IS NOT NULL
4260+ AND (ngi.ip & netmask(subnet.cidr)) = (INET %s & netmask(subnet.cidr))
4261+ ORDER BY ng.id, ngi.id
4262+ """)
4263+
4264+ def find_by_address_for_static_allocation(self, address):
4265+ """Find all cluster interfaces for the given address.
4266+
4267+ Furthermore, each interface must also have a static range defined.
4268+ """
4269+ assert isinstance(address, IPAddress), (
4270+ "%r is not an IPAddress" % (address,))
4271+ return self.raw(
4272+ self.find_by_address_for_static_allocation_query,
4273+ [NODEGROUP_STATUS.ENABLED, address.format()])
4274+
4275+ def get_by_address_for_static_allocation(self, address):
4276+ """Return the first interface that could contain `address`.
4277+
4278+ Furthermore, the interface must also have a static range defined.
4279+ """
4280+ assert isinstance(address, IPAddress), (
4281+ "%r is not an IPAddress" % (address,))
4282+ interfaces = self.raw(
4283+ self.find_by_address_for_static_allocation_query + " LIMIT 1",
4284+ [NODEGROUP_STATUS.ENABLED, address.format()])
4285+ for interface in interfaces:
4286+ return interface # This is stable because the query is ordered.
4287+ else:
4288+ return None
4289+
4290+ find_by_managed_range_for_subnet_query = dedent("""\
4291+ SELECT ngi.*
4292+ FROM
4293+ maasserver_subnet AS subnet,
4294+ maasserver_nodegroupinterface AS ngi,
4295+ maasserver_nodegroup AS ng
4296+ WHERE
4297+ ngi.nodegroup_id = ng.id AND
4298+ ng.status = 1 AND /* NodeGroup must be ENABLED */
4299+ ((inet(ngi.ip_range_low) << network(subnet.cidr) AND
4300+ inet(ngi.ip_range_high) << network(subnet.cidr))
4301+ OR (inet(ngi.static_ip_range_low) << network(subnet.cidr) AND
4302+ inet(ngi.static_ip_range_high) << network(subnet.cidr)))
4303+ AND subnet.id = %s
4304+ /* Prefer static ranges, since that's how we'll allocate addresses. */
4305+ ORDER BY ngi.static_ip_range_low DESC NULLS LAST, ngi.id
4306+ """)
4307+
4308+ def get_by_managed_range_for_subnet(self, subnet):
4309+ """Return the first interface that could contain `address` in its
4310+ dynamic or static range. (Prefer interfaces static ranges.)
4311+ """
4312+ # Circular imports
4313+ from maasserver.models import Subnet
4314+ assert isinstance(subnet, Subnet), (
4315+ "%r is not a Subnet" % (subnet,))
4316+ interfaces = self.raw(
4317+ self.find_by_managed_range_for_subnet_query + " LIMIT 1",
4318+ [subnet.id])
4319+ for interface in interfaces:
4320+ return interface # This is stable because the query is ordered.
4321+ else:
4322+ return None
4323+
4324+ def clear_dynamic_range_for_colliding_allocations(self):
4325+ """If there are any managed NodeGroupInterfaces that have allocated
4326+ addresses inside the dynamic range, then log an error and clear the
4327+ dynamic range. Called from inner_start_up.
4328+
4329+ See https://launchpad.net/bugs/1536604.
4330+ """
4331+ for ngi in NodeGroupInterface.objects.exclude(
4332+ management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED):
4333+ used = StaticIPAddress.objects.filter(
4334+ ip__gte=ngi.ip_range_low).filter(
4335+ ip__lte=ngi.ip_range_high).exclude(
4336+ alloc_type=IPADDRESS_TYPE.DHCP).exclude(
4337+ alloc_type=IPADDRESS_TYPE.DISCOVERED)
4338+ if used.exists():
4339+ maaslog.error((
4340+ "Disabling DHCP on cluster %s, interface %s because "
4341+ "dynamic range includes reserved/allocated addresses." % (
4342+ ngi.nodegroup.cluster_name, ngi.name)))
4343+ ngi.management = NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED
4344+ ngi.save()
4345+
4346+
4347+def get_default_vlan():
4348+ from maasserver.models.vlan import VLAN
4349+ return VLAN.objects.get_default_vlan()
4350+
4351+
4352+def raise_if_address_inside_dynamic_range(requested_address, fabric=None):
4353+ """
4354+ Checks if the specified IP address, inside the specified fabric,
4355+ is inside a MAAS-managed dynamic range.
4356+
4357+ :raises: StaticIPAddressForbidden if the address occurs within
4358+ an existing dynamic range within the specified fabric.
4359+ """
4360+ if fabric is not None:
4361+ raise NotImplementedError("Fabrics are not yet supported.")
4362+
4363+ requested_address_ip = IPAddress(requested_address)
4364+ for interface in NodeGroupInterface.objects.all():
4365+ if interface.is_managed:
4366+ dynamic_range = interface.get_dynamic_ip_range()
4367+ if requested_address_ip in dynamic_range:
4368+ raise StaticIPAddressForbidden(
4369+ "Requested IP address %s is in a dynamic range." %
4370+ requested_address)
4371+
4372+
4373+class NodeGroupInterface(CleanSave, TimestampedModel):
4374+ """Cluster interface.
4375+
4376+ Represents a network to which a given cluster controller is connected.
4377+ These interfaces are discovered automatically, but an admin can also
4378+ add/edit/remove them.
4379+
4380+ This class duplicates some of :class:`Network`, and adds settings for
4381+ managing DHCP. Some day we hope to delegate the duplicated fields, and
4382+ have auto-discovery populate the :class:`Network` model along the way.
4383+ """
4384+
4385+ class Meta(DefaultMeta):
4386+ # The API identifies a NodeGroupInterface by cluster and name.
4387+ unique_together = ('nodegroup', 'name')
4388+
4389+ objects = NodeGroupInterfaceManager()
4390+
4391+ # Static IP of the network interface.
4392+ ip = MAASIPAddressField(
4393+ null=False, editable=True,
4394+ help_text="Static IP Address of the interface",
4395+ verbose_name="IP")
4396+
4397+ # The `NodeGroup` this interface belongs to.
4398+ nodegroup = ForeignKey(
4399+ 'maasserver.NodeGroup', editable=True, null=False, blank=False)
4400+
4401+ vlan = ForeignKey(
4402+ 'VLAN', default=get_default_vlan, editable=True, blank=False,
4403+ null=False, on_delete=PROTECT)
4404+
4405+ # Note: adding null=True temporarily; will be adjusted after a data
4406+ # migration occurs to create each subnet link.
4407+ subnet = ForeignKey(
4408+ 'Subnet', editable=True, blank=True, null=True, on_delete=PROTECT)
4409+
4410+ # Name for this interface. It must be unique within the cluster.
4411+ # The code ensures that this is never an empty string, but we do allow
4412+ # an empty string on the form. The field defaults to a unique name based
4413+ # on the network interface name.
4414+ name = CharField(
4415+ blank=True, null=False, editable=True, max_length=255, default='',
4416+ validators=[VerboseRegexValidator('^[\w:.-]+$')],
4417+ help_text=(
4418+ "Identifying name for this cluster interface. "
4419+ "Must be unique within the cluster, and consist only of letters, "
4420+ "digits, dashes, and colons."))
4421+
4422+ management = IntegerField(
4423+ choices=NODEGROUPINTERFACE_MANAGEMENT_CHOICES, editable=True,
4424+ default=NODEGROUPINTERFACE_MANAGEMENT.DEFAULT)
4425+
4426+ # DHCP server settings.
4427+ interface = CharField(
4428+ blank=True, editable=True, max_length=255, default='',
4429+ help_text="Network interface (e.g. 'eth1').")
4430+
4431+ ip_range_low = MAASIPAddressField(
4432+ editable=True, unique=False, blank=True, null=True, default=None,
4433+ verbose_name="DHCP dynamic IP range low value",
4434+ help_text="Lowest IP number of the range for dynamic IPs, used for "
4435+ "enlistment, commissioning and unknown devices.")
4436+ ip_range_high = MAASIPAddressField(
4437+ editable=True, unique=False, blank=True, null=True, default=None,
4438+ verbose_name="DHCP dynamic IP range high value",
4439+ help_text="Highest IP number of the range for dynamic IPs, used for "
4440+ "enlistment, commissioning and unknown devices.")
4441+ static_ip_range_low = MAASIPAddressField(
4442+ editable=True, unique=False, blank=True, null=True, default=None,
4443+ verbose_name="Static IP range low value",
4444+ help_text="Lowest IP number of the range for IPs given to allocated "
4445+ "nodes, must be in same network as dynamic range.")
4446+ static_ip_range_high = MAASIPAddressField(
4447+ editable=True, unique=False, blank=True, null=True, default=None,
4448+ verbose_name="Static IP range high value",
4449+ help_text="Highest IP number of the range for IPs given to allocated "
4450+ "nodes, must be in same network as dynamic range.")
4451+
4452+ # Foreign DHCP server address, if any, that was detected on this
4453+ # interface.
4454+ foreign_dhcp_ip = MAASIPAddressField(
4455+ null=True, default=None, editable=True, blank=True, unique=False)
4456+
4457+ @property
4458+ def broadcast_ip(self):
4459+ """Compatibility layer to return the broadcast IP of the attached
4460+ Subnet's CIDR (or an empty string if there is no Subnet).
4461+
4462+ (This property exists because NodeGroupInterface previously contained
4463+ a broadcast_ip field.)
4464+ """
4465+ if self.subnet is None:
4466+ return ''
4467+ return unicode(IPNetwork(self.subnet.cidr).broadcast)
4468+
4469+ @broadcast_ip.setter
4470+ def broadcast_ip(self, value):
4471+ # This is a derived field, so setting it is a no-op.
4472+ pass
4473+
4474+ @property
4475+ def subnet_mask(self):
4476+ """Compatibility layer to return the subnet mask of the attached
4477+ Subnet's CIDR (or an empty string if there is no Subnet).
4478+
4479+ (This property exists because NodeGroupInterface previously contained
4480+ a subnet_mask field.)
4481+ """
4482+ if self.subnet is None:
4483+ return ''
4484+ return unicode(IPNetwork(self.subnet.cidr).netmask)
4485+
4486+ @property
4487+ def router_ip(self):
4488+ """Compatibility layer to return the router IP address for the attached
4489+ Subnet (or an empty string if a router_id cannot be found).
4490+
4491+ (This property exists because NodeGroupInterface previously contained
4492+ a router_ip field.)
4493+ """
4494+ if self.subnet is None or not self.subnet.gateway_ip:
4495+ return ''
4496+ return unicode(self.subnet.gateway_ip)
4497+
4498+ @subnet_mask.setter
4499+ def subnet_mask(self, value):
4500+ """Compatability layer to create a Subnet model object, and link
4501+ it to this NodeGroupInterface (or use an existing Subnet).
4502+
4503+ Note: currently, this will create stale Subnet objects in the database
4504+ if the NodeGroupInterface is edited multiple times. In the future,
4505+ when a Subnet is "unlinked", we should check all objects that depend
4506+ on it to see if they should be moved to the new subnet, and/or
4507+ reject the change.
4508+ """
4509+ if value is None or value == "":
4510+ self.subnet = None
4511+ return
4512+
4513+ cidr = unicode(make_network(self.ip, value).cidr)
4514+ if self.subnet:
4515+ self.subnet.update_cidr(cidr)
4516+ else:
4517+ # Circular imports
4518+ from maasserver.models import Subnet, Space
4519+ subnet, _ = Subnet.objects.get_or_create(
4520+ cidr=cidr, defaults={
4521+ 'name': cidr,
4522+ 'cidr': cidr,
4523+ 'space': Space.objects.get_default_space()
4524+ })
4525+ self.subnet = subnet
4526+
4527+ @property
4528+ def network(self):
4529+ """Return the network defined by the interface's address and netmask.
4530+
4531+ :return: :class:`IPNetwork`, or `None` if the netmask is unset.
4532+ :raise AddrFormatError: If the combination of interface address and
4533+ subnet mask is malformed.
4534+ """
4535+ if self.subnet is None:
4536+ return None
4537+
4538+ netmask = IPNetwork(self.subnet.cidr).netmask
4539+ # Nullness check for GenericIPAddress fields is deliberately kept
4540+ # vague: MAASIPAddressField seems to represent nulls as empty
4541+ # strings.
4542+ if netmask:
4543+ return make_network(self.ip, netmask).cidr
4544+ else:
4545+ return None
4546+
4547+ @property
4548+ def is_managed(self):
4549+ """Return true if this interface is managed by MAAS."""
4550+ return self.management != NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED
4551+
4552+ def check_for_network_interface_clashes(self, exclude):
4553+ """Validate uniqueness rules for network interfaces.
4554+
4555+ This enforces the rules that there can be only one IPv4 cluster
4556+ interface on a given network interface on the cluster controller, and
4557+ that there can be only one IPv6 cluster interface with a static range
4558+ on a given network interface on the cluster controller. Aliases and
4559+ VLANs count as separate network interfaces.
4560+
4561+ The IPv4 rule is inherent: a network interface (as seen in userspace)
4562+ can only be on one IPv4 subnet. The IPv6 rule is needed because our
4563+ current way of configuring IPv6 addresses on nodes, in `/etc/network`,
4564+ does not support multiple addresses. So a network interface on a node
4565+ can only be on one IPv6 subnet. Since the node's network interface is
4566+ connected to the same network segment as the cluster controller's, that
4567+ means that the cluster controller can only manage static IP addresses
4568+ on one IPv6 subnet per network interface.
4569+ """
4570+ if 'ip' in exclude or 'nodegroup' in exclude or 'interface' in exclude:
4571+ return
4572+ ip_version = IPAddress(self.ip).version
4573+ similar_interfaces = self.nodegroup.nodegroupinterface_set.filter(
4574+ interface=self.interface).exclude(
4575+ management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
4576+ if self.id is not None:
4577+ similar_interfaces = similar_interfaces.exclude(id=self.id)
4578+ potential_clashes = [
4579+ itf
4580+ for itf in similar_interfaces
4581+ if IPAddress(itf.ip).version == ip_version
4582+ ]
4583+ if ip_version == 4:
4584+ if potential_clashes != []:
4585+ raise ValidationError(
4586+ "Another cluster interface already connects "
4587+ "network interface %s to an IPv4 network."
4588+ % self.interface)
4589+ elif self.static_ip_range_low and self.static_ip_range_high:
4590+ # Nullness checks for these IP addresses are deliberately vague
4591+ # because Django may represent them as either empty strings or
4592+ # None.
4593+ clashes = [
4594+ itf
4595+ for itf in potential_clashes
4596+ if itf.static_ip_range_low and itf.static_ip_range_high
4597+ ]
4598+ if clashes != []:
4599+ raise ValidationError(
4600+ "Another cluster interface with a static address range "
4601+ "already connects network interface %s to an IPv6 network."
4602+ % self.interface)
4603+
4604+ def validate_unique(self, exclude=None):
4605+ """Validate against conflicting `NodeGroupInterface` objects."""
4606+ super(NodeGroupInterface, self).validate_unique(exclude=exclude)
4607+ if exclude is None:
4608+ exclude = []
4609+ self.check_for_network_interface_clashes(exclude)
4610+
4611+ def has_dynamic_ip_range(self):
4612+ """Returns `True` if this `NodeGroupInterface` has a dynamic IP
4613+ range specified."""
4614+ return self.ip_range_low and self.ip_range_high
4615+
4616+ def get_dynamic_ip_range(self):
4617+ """Returns a `MAASIPRange` for this `NodeGroupInterface`, if a dynamic
4618+ range is specified. Otherwise, returns `None`."""
4619+ if self.has_dynamic_ip_range():
4620+ return make_iprange(
4621+ self.ip_range_low, self.ip_range_high,
4622+ purpose='dynamic-range')
4623+ else:
4624+ return None
4625+
4626+ def has_static_ip_range(self):
4627+ """Returns `True` if this `NodeGroupInterface` has a static IP
4628+ range specified."""
4629+ return self.static_ip_range_low and self.static_ip_range_high
4630+
4631+ def get_static_ip_range(self):
4632+ """Returns a `MAASIPRange` for this `NodeGroupInterface`, if a static
4633+ range is specified. Otherwise, returns `None`."""
4634+ if self.has_static_ip_range():
4635+ return make_iprange(
4636+ self.static_ip_range_low, self.static_ip_range_high,
4637+ purpose='static-range')
4638+ else:
4639+ return None
4640+
4641+ def display_management(self):
4642+ """Return management status text as displayed to the user."""
4643+ return NODEGROUPINTERFACE_MANAGEMENT_CHOICES_DICT[self.management]
4644+
4645+ def __repr__(self):
4646+ return "<NodeGroupInterface %s,%s>" % (
4647+ self.nodegroup.uuid if self.nodegroup_id else None, self.name)
4648+
4649+ def clean_network_valid(self):
4650+ """Validate the network.
4651+
4652+ This validates that the network defined by `ip` and `subnet_mask` is
4653+ valid.
4654+ """
4655+ try:
4656+ network = self.network
4657+ if network and IPAddress(self.ip) not in self.network:
4658+ raise ValidationError(
4659+ {'ip':
4660+ ["Cluster IP address is not within specified subnet."]})
4661+ if (network and network.size < 8 and self.is_managed):
4662+ raise ValidationError(
4663+ {'subnet_mask': ['Subnet is too small to manage.']})
4664+ except AddrFormatError as e:
4665+ # The interface's address is validated separately. If the
4666+ # combination with the netmask is invalid, either there's already
4667+ # going to be a specific validation error for the IP address, or
4668+ # the failure is due to an invalid netmask.
4669+ # XXX mpontillo 2015-07-23: subnet - now that this is a property,
4670+ # is this appropriate?
4671+ raise ValidationError({'subnet_mask': [e.message]})
4672+
4673+ def clean_network_config_if_managed(self):
4674+ # If management is not 'UNMANAGED', all the network information
4675+ # should be provided.
4676+ if self.is_managed:
4677+ mandatory_fields = [
4678+ 'interface',
4679+ 'ip_range_low',
4680+ 'ip_range_high',
4681+ ]
4682+ errors = {}
4683+ for field in mandatory_fields:
4684+ if not getattr(self, field):
4685+ errors[field] = [
4686+ "That field cannot be empty (unless that interface is "
4687+ "'unmanaged')"]
4688+ if len(errors) == 0 and self.ip_range_low == self.ip_range_high:
4689+ errors.setdefault('ip_range_low', []).append([
4690+ "Dynamic range cannot be one address."])
4691+ errors.setdefault('ip_range_high', []).append([
4692+ "Dynamic range cannot be one address."])
4693+ if len(errors) != 0:
4694+ raise ValidationError(errors)
4695+
4696+ def manages_static_range(self):
4697+ """Is this a managed interface with a static IP range configured?"""
4698+ # Deliberately vague implicit conversion to bool: a blank IP address
4699+ # can show up internally as either None or an empty string.
4700+ return (
4701+ self.is_managed and
4702+ self.static_ip_range_low and
4703+ self.static_ip_range_high)
4704+
4705+ def clean_ip_ranges(self):
4706+ """Ensure that the static and dynamic ranges don't overlap."""
4707+ if not self.manages_static_range():
4708+ # Nothing to do; bail out.
4709+ return
4710+
4711+ errors = {}
4712+ ip_range_low = IPAddress(self.ip_range_low)
4713+ ip_range_high = IPAddress(self.ip_range_high)
4714+ static_ip_range_low = IPAddress(self.static_ip_range_low)
4715+ static_ip_range_high = IPAddress(self.static_ip_range_high)
4716+
4717+ message_base = (
4718+ "Lower bound %s is higher than upper bound %s")
4719+
4720+ static_range = {}
4721+ dynamic_range = {}
4722+
4723+ try:
4724+ static_range = make_iprange(
4725+ static_ip_range_low, static_ip_range_high)
4726+ except AddrFormatError:
4727+ message = message_base % (
4728+ static_ip_range_low, static_ip_range_high)
4729+ errors.update({
4730+ 'static_ip_range_low': [message],
4731+ 'static_ip_range_high': [message],
4732+ })
4733+
4734+ try:
4735+ dynamic_range = make_iprange(
4736+ ip_range_low, ip_range_high)
4737+ except AddrFormatError:
4738+ message = message_base % (ip_range_low, ip_range_high)
4739+ errors.update({
4740+ 'ip_range_low': [message],
4741+ 'ip_range_high': [message],
4742+ })
4743+
4744+ # This is a bit unattractive, but we can't use IPSet for
4745+ # large networks - it's far too slow. What we actually care
4746+ # about is whether the lows and highs of the static range
4747+ # fall within the dynamic range and vice-versa, which
4748+ # IPRange gives us.
4749+ ranges_overlap = (
4750+ static_ip_range_low in dynamic_range or
4751+ static_ip_range_high in dynamic_range or
4752+ ip_range_low in static_range or
4753+ ip_range_high in static_range
4754+ )
4755+ if ranges_overlap:
4756+ message = "Static and dynamic IP ranges may not overlap."
4757+ errors.update({
4758+ 'ip_range_low': [message],
4759+ 'ip_range_high': [message],
4760+ 'static_ip_range_low': [message],
4761+ 'static_ip_range_high': [message],
4762+ })
4763+
4764+ # And finally, make sure that there are no allocated addresses in the
4765+ # dynamic range. This leads to lease parser errors when DHCP tries to
4766+ # give out the address that we already in use.
4767+ in_use = StaticIPAddress.objects.filter(
4768+ ip__gte=self.ip_range_low).filter(
4769+ ip__lte=self.ip_range_high).exclude(
4770+ alloc_type=IPADDRESS_TYPE.DHCP).exclude(
4771+ alloc_type=IPADDRESS_TYPE.DISCOVERED)
4772+ if in_use.exists():
4773+ message = (
4774+ "Dynamic range may not have addresses already in use. "
4775+ "Conflicts with: %s" % (
4776+ " ".join([sip.ip for sip in in_use])))
4777+ errors.update({
4778+ 'ip_range_low': [message],
4779+ 'ip_range_high': [message],
4780+ })
4781+
4782+ if errors:
4783+ raise ValidationError(errors)
4784+
4785+ def clean_overlapping_networks(self):
4786+ """Ensure that this interface's network doesn't overlap those of
4787+ other interfaces on this cluster.
4788+ """
4789+ try:
4790+ nodegroup = self.nodegroup
4791+ except NodeGroup.DoesNotExist:
4792+ # We're likely being called on nodegroup creation, so
4793+ # there's no nodegroup linked to this interface yet. Since
4794+ # we don't have a nodegroup we can't check whether any other
4795+ # interfaces on this nodegroup have overlapping network
4796+ # settings.
4797+ return
4798+
4799+ if not self.is_managed:
4800+ return
4801+
4802+ network = self.network
4803+ current_cluster_interfaces = (
4804+ NodeGroupInterface.objects.filter(nodegroup=nodegroup)
4805+ .exclude(management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
4806+ .exclude(id=self.id))
4807+ network_overlaps_other_interfaces = any(
4808+ intersect_iprange(network, interface.network) is not None
4809+ for interface in current_cluster_interfaces
4810+ )
4811+ if network_overlaps_other_interfaces:
4812+ message = (
4813+ "This interface's network must not overlap with other "
4814+ "networks on this cluster.")
4815+ errors = {
4816+ 'ip': [message],
4817+ 'subnet_mask': [message],
4818+ }
4819+ raise ValidationError(errors)
4820+
4821+ def clean_fields(self, *args, **kwargs):
4822+ super(NodeGroupInterface, self).clean_fields(*args, **kwargs)
4823+ self.clean_network_valid()
4824+ self.clean_network_config_if_managed()
4825+ self.clean_ip_ranges()
4826+ self.clean_overlapping_networks()
4827+
4828+ def get_ipranges_in_use_on_ipnetwork(
4829+ self, ipnetwork, include_static_range=True):
4830+ """Returns a `set` of `MAASIPRange` objects for this
4831+ `NodeGroupInterface`, annotated with a purpose string indicating if
4832+ they are part of the dynamic range, static range, cluster IP, gateway
4833+ IP, or DNS server."""
4834+ assert isinstance(ipnetwork, IPNetwork)
4835+
4836+ ranges = set()
4837+ if self.has_dynamic_ip_range():
4838+ dynamic_range = self.get_dynamic_ip_range()
4839+ if dynamic_range in ipnetwork:
4840+ ranges.add(dynamic_range)
4841+
4842+ # The caller might want to exclude static ranges, if they will be
4843+ # separately looking at the static IP addresses assigned to the subnet.
4844+ if include_static_range:
4845+ if self.has_static_ip_range():
4846+ static_range = self.get_static_ip_range()
4847+ if static_range in ipnetwork:
4848+ ranges.add(static_range)
4849+
4850+ if make_ipaddress(self.ip) in ipnetwork:
4851+ ranges.add(make_iprange(self.ip, purpose="cluster-ip"))
4852+
4853+ if self.subnet is not None:
4854+ gateway_ip = make_ipaddress(self.subnet.gateway_ip)
4855+ if gateway_ip and gateway_ip in ipnetwork:
4856+ ranges.add(make_iprange(
4857+ self.subnet.gateway_ip, purpose="gateway-ip"))
4858+ if self.subnet.dns_servers is not None:
4859+ for server in self.subnet.dns_servers:
4860+ if make_ipaddress(server) in ipnetwork:
4861+ ranges.add(make_iprange(server, purpose="dns-server"))
4862+
4863+ return ranges
4864+
4865+
4866+@receiver(post_save, sender=NodeGroupInterface)
4867+def post_save_NodeGroupInterface(sender, instance, created, **kwargs):
4868+ """If a NodeGroupInterface is saved, ensure its corresponding Subnet
4869+ is also updated."""
4870+ # Note: If the NodeGroupInterface is being saved for the first time,
4871+ # the Subnet will have already been explicitly created already.
4872+ if not created and instance.subnet:
4873+ instance.subnet.save()
4874
4875=== modified file 'src/maasserver/models/partition.py'
4876=== modified file 'src/maasserver/models/partitiontable.py'
4877=== modified file 'src/maasserver/models/signals/interfaces.py'
4878--- src/maasserver/models/signals/interfaces.py 2016-05-12 19:07:37 +0000
4879+++ src/maasserver/models/signals/interfaces.py 2016-06-10 16:34:39 +0000
4880@@ -3,6 +3,7 @@
4881
4882 """Respond to interface changes."""
4883
4884+<<<<<<< TREE
4885 __all__ = [
4886 "signals",
4887 ]
4888@@ -14,6 +15,20 @@
4889 post_save,
4890 pre_delete,
4891 )
4892+=======
4893+from __future__ import (
4894+ absolute_import,
4895+ print_function,
4896+ unicode_literals,
4897+ )
4898+
4899+str = None
4900+
4901+__metaclass__ = type
4902+__all__ = []
4903+
4904+from django.db.models.signals import post_save
4905+>>>>>>> MERGE-SOURCE
4906 from maasserver.enum import (
4907 INTERFACE_TYPE,
4908 IPADDRESS_TYPE,
4909@@ -45,6 +60,14 @@
4910 signals = SignalsManager()
4911
4912
4913+INTERFACE_CLASSES = [
4914+ Interface,
4915+ PhysicalInterface,
4916+ BondInterface,
4917+ VLANInterface,
4918+]
4919+
4920+
4921 def interface_enabled_or_disabled(instance, old_values, **kwargs):
4922 """When an interface is enabled be sure at minimum a LINK_UP is created.
4923 When an interface is disabled make sure that all its links are removed,
4924@@ -70,10 +93,17 @@
4925 ip_address, clearing_config=True)
4926
4927
4928+<<<<<<< TREE
4929 for klass in INTERFACE_CLASSES:
4930 signals.watch_fields(
4931 interface_enabled_or_disabled,
4932 klass, ['enabled'], delete=False)
4933+=======
4934+for klass in INTERFACE_CLASSES:
4935+ connect_to_field_change(
4936+ interface_enabled_or_disabled,
4937+ klass, ['enabled'], delete=False)
4938+>>>>>>> MERGE-SOURCE
4939
4940
4941 def interface_mtu_params_update(instance, old_values, **kwargs):
4942@@ -134,6 +164,7 @@
4943 parent.save()
4944
4945
4946+<<<<<<< TREE
4947 for klass in INTERFACE_CLASSES:
4948 signals.watch_fields(
4949 interface_mtu_params_update,
4950@@ -305,3 +336,50 @@
4951
4952 # Enable all signals by default.
4953 signals.enable()
4954+=======
4955+for klass in INTERFACE_CLASSES:
4956+ connect_to_field_change(
4957+ interface_mtu_params_update,
4958+ klass, ['params'], delete=False)
4959+
4960+
4961+def update_bond_parents(sender, instance, created, **kwargs):
4962+ """Update bond parents when interface created."""
4963+ if instance.type == INTERFACE_TYPE.BOND:
4964+ for parent in instance.parents.all():
4965+ # Make sure the parent has not links as well, just to be sure.
4966+ parent.clear_all_links(clearing_config=True)
4967+ if parent.vlan != instance.vlan:
4968+ parent.vlan = instance.vlan
4969+ parent.save()
4970+
4971+
4972+for klass in INTERFACE_CLASSES:
4973+ post_save.connect(
4974+ update_bond_parents, sender=klass)
4975+
4976+
4977+def interface_vlan_update(instance, old_values, **kwargs):
4978+ """When an interfaces VLAN is changed we need to remove all
4979+ links if the VLAN is different.
4980+ """
4981+ new_vlan_id = instance.vlan_id
4982+ [old_vlan_id] = old_values
4983+ if new_vlan_id == old_vlan_id:
4984+ # Nothing changed do nothing.
4985+ return
4986+ if instance.node is None:
4987+ # Not assigned to a node. Nothing to do.
4988+ return
4989+
4990+ # Interface VLAN was changed on a machine or device. Remove all its
4991+ # links except the DISCOVERED ones.
4992+ instance.ip_addresses.exclude(
4993+ alloc_type=IPADDRESS_TYPE.DISCOVERED).delete()
4994+
4995+
4996+for klass in INTERFACE_CLASSES:
4997+ connect_to_field_change(
4998+ interface_vlan_update,
4999+ klass, ['vlan_id'], delete=False)
5000+>>>>>>> MERGE-SOURCE
The diff has been truncated for viewing.