Merge lp:~andreserl/maas/lp1591093_1.9 into lp:~maas-committers/maas/trunk
- lp1591093_1.9
- Merge into 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 |
Related bugs: |
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.
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '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.
Self approving as this was approved in: https:/ /code.launchpad .net/~andreserl /maas/fix_ hdpsa_repo2/ +merge/ 297086