Merge lp:~mpontillo/maas/migrate-dns-settings-1.7 into lp:maas/trunk

Proposed by Mike Pontillo on 2015-07-08
Status: Superseded
Proposed branch: lp:~mpontillo/maas/migrate-dns-settings-1.7
Merge into: lp:maas/trunk
Diff against target: 13596 lines (+10612/-113) (has conflicts)
106 files modified
Makefile (+7/-0)
contrib/maas-http.conf (+19/-0)
docs/changelog.rst (+163/-0)
docs/conf.py (+1/-1)
etc/maas/pserv.yaml.OTHER (+25/-0)
etc/maas/templates/commissioning-user-data/user_data_poweroff.template (+31/-0)
required-packages/base (+5/-0)
src/maas/development.py (+12/-1)
src/maas/settings.py (+6/-0)
src/maascli/api.py (+5/-0)
src/maascli/tests/test_api.py (+48/-0)
src/maasserver/api/doc.py (+11/-0)
src/maasserver/api/doc_handler.py (+5/-0)
src/maasserver/api/ip_addresses.py (+138/-0)
src/maasserver/api/nodegroups.py (+17/-0)
src/maasserver/api/nodes.py (+77/-1)
src/maasserver/api/pxeconfig.py (+19/-0)
src/maasserver/api/support.py (+16/-6)
src/maasserver/api/tests/test_doc.py (+79/-0)
src/maasserver/api/tests/test_ipaddresses.py (+274/-71)
src/maasserver/api/tests/test_node.py (+70/-0)
src/maasserver/api/tests/test_nodegroup.py (+75/-0)
src/maasserver/api/tests/test_pxeconfig.py (+17/-0)
src/maasserver/api/tests/test_support.py (+10/-0)
src/maasserver/api/tests/test_version.py (+8/-0)
src/maasserver/api/version.py (+13/-0)
src/maasserver/clusterrpc/tests/test_boot_images.py (+24/-0)
src/maasserver/dns/config.py (+28/-0)
src/maasserver/dns/tests/test_config.py (+11/-0)
src/maasserver/exceptions.py (+9/-0)
src/maasserver/fields.py (+26/-0)
src/maasserver/forms.py (+32/-1)
src/maasserver/forms_settings.py (+17/-0)
src/maasserver/management/commands/edit_named_options.py (+24/-0)
src/maasserver/models/config.py (+5/-0)
src/maasserver/models/macaddress.py (+74/-0)
src/maasserver/models/node.py (+102/-0)
src/maasserver/models/nodegroup.py (+27/-0)
src/maasserver/models/signals/power.py (+10/-0)
src/maasserver/models/tests/test_bootsource.py (+6/-0)
src/maasserver/models/tests/test_macaddress.py (+90/-0)
src/maasserver/models/tests/test_node.py (+159/-18)
src/maasserver/models/tests/test_nodegroup.py (+11/-0)
src/maasserver/rpc/nodes.py (+14/-0)
src/maasserver/rpc/tests/test_nodes.py (+32/-0)
src/maasserver/start_up.py (+93/-0)
src/maasserver/static/js/node_check.js.OTHER (+186/-0)
src/maasserver/static/js/node_views.js.OTHER (+680/-0)
src/maasserver/static/js/tests/test_node_check.html.OTHER (+36/-0)
src/maasserver/static/js/tests/test_node_check.js.OTHER (+128/-0)
src/maasserver/static/js/tests/test_node_views.html.OTHER (+55/-0)
src/maasserver/static/js/tests/test_node_views.js.OTHER (+1035/-0)
src/maasserver/static/js/utils.js.OTHER (+350/-0)
src/maasserver/templates/maasserver/base.html (+85/-0)
src/maasserver/templates/maasserver/node_actions.html (+29/-0)
src/maasserver/templates/maasserver/node_event_list_snippet.html.OTHER (+32/-0)
src/maasserver/templates/maasserver/node_view.html.OTHER (+292/-0)
src/maasserver/templates/maasserver/node_view_mac_display.html (+10/-0)
src/maasserver/templates/maasserver/nodes_listing.html.OTHER (+111/-0)
src/maasserver/templates/maasserver/snippets.html.OTHER (+67/-0)
src/maasserver/testing/factory.py (+30/-0)
src/maasserver/tests/test_dhcp.py (+11/-0)
src/maasserver/tests/test_fields.py (+38/-0)
src/maasserver/tests/test_forms_network.py (+76/-0)
src/maasserver/tests/test_forms_node.py (+9/-0)
src/maasserver/tests/test_js.py (+11/-0)
src/maasserver/tests/test_start_up.py (+64/-0)
src/maasserver/utils/isc.py (+286/-0)
src/maasserver/utils/mac.py (+34/-0)
src/maasserver/utils/tests/test_isc.py (+182/-0)
src/maasserver/utils/tests/test_mac.py (+35/-0)
src/maasserver/utils/tests/test_version.py (+217/-0)
src/maasserver/utils/version.py (+131/-0)
src/maasserver/views/nodes.py (+377/-6)
src/maasserver/views/tags.py.OTHER (+52/-0)
src/maasserver/views/tests/test_nodes.py.OTHER (+2766/-0)
src/maasserver/views/tests/test_rpc.py (+16/-2)
src/maasserver/views/tests/test_snippets.py (+47/-0)
src/maastesting/fixtures.py (+37/-0)
src/metadataserver/api.py (+10/-2)
src/metadataserver/models/commissioningscript.py (+41/-4)
src/metadataserver/models/tests/test_noderesults.py (+99/-0)
src/metadataserver/tests/test_api.py (+5/-0)
src/metadataserver/user_data/poweroff.py (+37/-0)
src/provisioningserver/boot/tests/test_boot.py (+7/-0)
src/provisioningserver/boot/tests/test_windows.py (+11/-0)
src/provisioningserver/boot/windows.py (+6/-0)
src/provisioningserver/config.py (+24/-0)
src/provisioningserver/dns/tests/test_config.py (+14/-0)
src/provisioningserver/drivers/hardware/tests/test_ucsm.py (+6/-0)
src/provisioningserver/drivers/hardware/ucsm.py (+21/-0)
src/provisioningserver/drivers/hardware/virsh.py (+12/-0)
src/provisioningserver/drivers/osystem/custom.py (+19/-0)
src/provisioningserver/drivers/osystem/tests/test_custom.py (+20/-0)
src/provisioningserver/plugin.py (+148/-0)
src/provisioningserver/rpc/boot_images.py (+22/-0)
src/provisioningserver/rpc/cluster.py (+4/-0)
src/provisioningserver/rpc/clusterservice.py (+34/-0)
src/provisioningserver/rpc/power.py (+38/-0)
src/provisioningserver/rpc/tests/test_boot_images.py (+53/-0)
src/provisioningserver/rpc/tests/test_clusterservice.py (+205/-0)
src/provisioningserver/rpc/tests/test_power.py (+57/-0)
src/provisioningserver/tests/test_config.py (+152/-0)
src/provisioningserver/tests/test_plugin.py (+5/-0)
src/provisioningserver/tests/test_plugin_services.py.OTHER (+87/-0)
utilities/update-new-and-modified-copyright (+15/-0)
Text conflict in Makefile
Text conflict in contrib/maas-http.conf
Text conflict in docs/changelog.rst
Contents conflict in etc/maas/pserv.yaml
Text conflict in etc/maas/templates/commissioning-user-data/user_data_poweroff.template
Text conflict in required-packages/base
Text conflict in src/maas/development.py
Text conflict in src/maas/settings.py
Text conflict in src/maascli/api.py
Text conflict in src/maasserver/api/doc.py
Text conflict in src/maasserver/api/doc_handler.py
Text conflict in src/maasserver/api/ip_addresses.py
Text conflict in src/maasserver/api/nodegroups.py
Text conflict in src/maasserver/api/nodes.py
Text conflict in src/maasserver/api/pxeconfig.py
Text conflict in src/maasserver/api/support.py
Text conflict in src/maasserver/api/tests/test_doc.py
Text conflict in src/maasserver/api/tests/test_ipaddresses.py
Text conflict in src/maasserver/api/tests/test_node.py
Text conflict in src/maasserver/api/tests/test_nodegroup.py
Text 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/version.py
Text conflict in src/maasserver/clusterrpc/tests/test_boot_images.py
Text conflict in src/maasserver/dns/config.py
Text conflict in src/maasserver/dns/tests/test_config.py
Text conflict in src/maasserver/exceptions.py
Text conflict in src/maasserver/fields.py
Text conflict in src/maasserver/forms.py
Text conflict in src/maasserver/forms_settings.py
Text conflict in src/maasserver/management/commands/edit_named_options.py
Text conflict in src/maasserver/models/config.py
Text conflict in src/maasserver/models/macaddress.py
Text conflict in src/maasserver/models/node.py
Text conflict in src/maasserver/models/nodegroup.py
Text conflict in src/maasserver/models/signals/power.py
Text conflict in src/maasserver/models/tests/test_bootsource.py
Text conflict in src/maasserver/models/tests/test_macaddress.py
Text conflict in src/maasserver/models/tests/test_node.py
Text conflict in src/maasserver/models/tests/test_nodegroup.py
Text conflict in src/maasserver/rpc/nodes.py
Text conflict in src/maasserver/rpc/tests/test_nodes.py
Text conflict in src/maasserver/start_up.py
Contents conflict in src/maasserver/static/js/node_check.js
Contents conflict in src/maasserver/static/js/node_views.js
Contents conflict in src/maasserver/static/js/tests/test_node_check.html
Contents conflict in src/maasserver/static/js/tests/test_node_check.js
Contents conflict in src/maasserver/static/js/tests/test_node_views.html
Contents conflict in src/maasserver/static/js/tests/test_node_views.js
Contents conflict in src/maasserver/static/js/utils.js
Text conflict in src/maasserver/templates/maasserver/base.html
Contents conflict in src/maasserver/templates/maasserver/node_event_list_snippet.html
Contents conflict in src/maasserver/templates/maasserver/node_view.html
Contents conflict in src/maasserver/templates/maasserver/nodes_listing.html
Contents conflict in src/maasserver/templates/maasserver/snippets.html
Text conflict in src/maasserver/testing/factory.py
Text conflict in src/maasserver/tests/test_dhcp.py
Text conflict in src/maasserver/tests/test_fields.py
Text conflict in src/maasserver/tests/test_forms_network.py
Text conflict in src/maasserver/tests/test_forms_node.py
Text conflict in src/maasserver/tests/test_js.py
Text conflict in src/maasserver/tests/test_start_up.py
Text conflict in src/maasserver/utils/isc.py
Text conflict in src/maasserver/utils/mac.py
Text conflict in src/maasserver/utils/tests/test_isc.py
Text conflict in src/maasserver/utils/tests/test_mac.py
Conflict adding file src/maasserver/utils/tests/test_version.py.  Moved existing file to src/maasserver/utils/tests/test_version.py.moved.
Conflict adding file src/maasserver/utils/version.py.  Moved existing file to src/maasserver/utils/version.py.moved.
Text conflict in src/maasserver/views/nodes.py
Contents conflict in src/maasserver/views/tags.py
Contents conflict in src/maasserver/views/tests/test_nodes.py
Text conflict in src/maasserver/views/tests/test_rpc.py
Text conflict in src/maastesting/fixtures.py
Text conflict in src/metadataserver/api.py
Text conflict in src/metadataserver/models/commissioningscript.py
Text conflict in src/metadataserver/models/tests/test_noderesults.py
Text conflict in src/metadataserver/tests/test_api.py
Text conflict in src/metadataserver/user_data/poweroff.py
Text conflict in src/provisioningserver/boot/tests/test_boot.py
Text conflict in src/provisioningserver/boot/tests/test_windows.py
Text conflict in src/provisioningserver/boot/windows.py
Text conflict in src/provisioningserver/config.py
Text conflict in src/provisioningserver/dns/tests/test_config.py
Text conflict in src/provisioningserver/drivers/hardware/tests/test_ucsm.py
Text conflict in src/provisioningserver/drivers/hardware/ucsm.py
Text conflict in src/provisioningserver/drivers/hardware/virsh.py
Text conflict in src/provisioningserver/drivers/osystem/custom.py
Text conflict in src/provisioningserver/drivers/osystem/tests/test_custom.py
Text conflict in src/provisioningserver/plugin.py
Text conflict in src/provisioningserver/rpc/boot_images.py
Text conflict in src/provisioningserver/rpc/cluster.py
Text conflict in src/provisioningserver/rpc/clusterservice.py
Text conflict in src/provisioningserver/rpc/power.py
Text conflict in src/provisioningserver/rpc/tests/test_boot_images.py
Text conflict in src/provisioningserver/rpc/tests/test_clusterservice.py
Text conflict in src/provisioningserver/rpc/tests/test_power.py
Text conflict in src/provisioningserver/tests/test_config.py
Text conflict in src/provisioningserver/tests/test_plugin.py
Contents conflict in src/provisioningserver/tests/test_plugin_services.py
To merge this branch: bzr merge lp:~mpontillo/maas/migrate-dns-settings-1.7
Reviewer Review Type Date Requested Status
MAAS Maintainers 2015-07-08 Pending
Review via email: mp+264087@code.launchpad.net

This proposal has been superseded by a proposal from 2015-07-08.

Commit message

Backport DNS migration fixes (Merge revision 4077 from trunk)

To post a comment you must log in.
3375. By Mike Pontillo on 2015-07-08

Backport DNS migration fixes (Merge revision 4077 from trunk)

3376. By Mike Pontillo on 2015-07-08

Update changelog

Unmerged revisions

3376. By Mike Pontillo on 2015-07-08

Update changelog

3375. By Mike Pontillo on 2015-07-08

Backport DNS migration fixes (Merge revision 4077 from trunk)

3374. By Raphaël Badin on 2015-07-06

[r=allenap][bug=1470585][author=rvb] Backport 4062: upstream_dns: accept a list of forwarders (and not just *one* forwarder).

3373. By Mike Pontillo on 2015-07-03

[r=rvb][bug=1413388][author=mpontillo] Remove dependency on iscpy. Add MAAS utility (based on iscpy) for parsing named.options. Add tests for iscpy-derived code.

3372. By Raphaël Badin on 2015-07-01

[r=rvb][bug=1470575][author=rvb] Backport 4009: Ignore IP ordering in test.

3371. By Mike Pontillo on 2015-07-01

[r=rvb][bug=][author=mpontillo] Fix test cases on 1.7 (failing 50% of the time, due to use of the factory to create a Node)

3370. By Raphaël Badin on 2015-06-09

[r=rvb][bug=][author=rvb] Trivial fix: set version to '1.7' so that the header of the 1.7 doc says '1.7'.

3369. By Raphaël Badin on 2015-05-26

[r=blake-rouse][bug=1456969][author=rvb] Backport 3921: Expose the 'boot_type' field on the API. This controls whether the node will use curtin or di to install itself. The di installation is deprecated but before we fully support advanced networking and storage configuration in curtin, we still have to support di.

3368. By Raphaël Badin on 2015-05-21

[r=blake-rouse][bug=][author=rvb] Update 1.7.4 changelog to mention bug #1456892.

3367. By Raphaël Badin on 2015-05-21

[r=rvb][bug=1456892][author=rvb] Backport 3920: Do not break the computation of the initial distro series if the OS associated with a node isn't part of the list of supported OS.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2015-07-03 11:18:16 +0000
3+++ Makefile 2015-07-08 00:07:16 +0000
4@@ -493,6 +493,7 @@
5
6 # This ought to be as simple as using bzr builddeb --export-upstream but it
7 # has a bug and always considers apt-source tarballs before the specified
8+<<<<<<< TREE
9 # branch. Instead, export to a local tarball which is always found. Make sure
10 # the packages listed in `required-packages/build` are installed before using
11 # this.
12@@ -500,6 +501,12 @@
13 # Old names.
14 PACKAGING := $(abspath ../packaging.trunk)
15 PACKAGING_BRANCH := lp:~maas-maintainers/maas/packaging
16+=======
17+# branch. So instead, export to a local tarball which is always found.
18+# Make sure debhelper and dh-apport packages are installed before using this.
19+PACKAGING := $(CURDIR)/../packaging.utopic
20+PACKAGING_BRANCH := lp:~maas-maintainers/maas/packaging.utopic
21+>>>>>>> MERGE-SOURCE
22
23 packaging-tree = $(PACKAGING)
24 packaging-branch = $(PACKAGING_BRANCH)
25
26=== modified file 'buildout.cfg'
27=== modified file 'contrib/maas-http.conf'
28--- contrib/maas-http.conf 2015-06-26 13:07:33 +0000
29+++ contrib/maas-http.conf 2015-07-08 00:07:16 +0000
30@@ -1,3 +1,22 @@
31+<<<<<<< TREE
32+=======
33+WSGIDaemonProcess maas user=maas group=maas home=/var/lib/maas processes=2 threads=1 display-name=%{GROUP}
34+
35+# Without this, defining a tag as a malformed xpath expression will hang
36+# the region controller.
37+# See https://techknowhow.library.emory.edu/blogs/branker/2010/07/30/django-lxml-wsgi-and-python-sub-interpreter-magic
38+WSGIApplicationGroup %{GLOBAL}
39+
40+WSGIScriptAlias /MAAS /usr/share/maas/wsgi.py
41+# Preload application when process starts.
42+WSGIImportScript /usr/share/maas/wsgi.py process-group=maas application-group=%{GLOBAL}
43+WSGIPassAuthorization On
44+
45+<Directory /usr/share/maas/>
46+ WSGIProcessGroup maas
47+</Directory>
48+
49+>>>>>>> MERGE-SOURCE
50 <IfModule mod_ssl.c>
51 <VirtualHost *:443>
52 SSLEngine On
53
54=== modified file 'docs/changelog.rst'
55--- docs/changelog.rst 2015-06-08 15:05:00 +0000
56+++ docs/changelog.rst 2015-07-08 00:07:16 +0000
57@@ -2,6 +2,7 @@
58 Changelog
59 =========
60
61+<<<<<<< TREE
62 1.8.0
63 =====
64
65@@ -636,6 +637,168 @@
66 #1414036 Trying to add an empty network crashes (AddrFormatError)
67
68
69+=======
70+1.7.5
71+=====
72+
73+Bug Fix Update
74+--------------
75+
76+#1456969 MAAS cli/API: missing option set use-fast-installer /
77+ use-debian-installer
78+
79+1.7.4
80+=====
81+
82+Bug Fix Update
83+--------------
84+
85+#1456892 500 error: UnboundLocalError: local variable 'key_required'
86+ referenced before assignment
87+#1387859 When MAAS has too many leases, and lease parsing fails, MAAS fails
88+ to auto-map NIC with network
89+#1329267 Alert a command-line user of `maas` when their local API
90+ description is out-of-date.
91+
92+1.7.3
93+=====
94+
95+Bug Fix Update
96+--------------
97+
98+#1441933 Internal Server Error when saving a cluster without Router IP
99+#1441133 MAAS version not exposed over the API
100+#1437094 Sorting by mac address on webui causes internal server error
101+#1439359 Automatically set correct boot resources selection and start import
102+ after upgrade from MAAS 1.5; Ensures MAAS is usable after upgrade.
103+#1439366 Backwards compatibility with MAAS 1.5 preseeds and custom preseeds.
104+ Ensures that users dont have to manually change preseeds names.
105+
106+1.7.2
107+=====
108+
109+Bug Fix Update
110+--------------
111+
112+For full details see https://launchpad.net/maas/+milestone/1.7.2
113+
114+#1331214 Support AMT Version > 8
115+#1397567 Fix call to amttool when restarting a node to not fail disk erasing.
116+#1415538 Do not generate the 'option routers' stanza if router IP is None.
117+#1403909 Do not deallocate StaticIPAddress before node has powered off.
118+#1405998 Remove all OOPS reporting.
119+#1423931 Update the nodes host maps when a sticky ip address is claimed over the API.
120+#1433697 Look for bootloaders in /usr/lib/EXTLINUX
121+
122+
123+1.7.1
124+=====
125+
126+Minor feature improvements
127+--------------------------
128+
129+New CentOS Release support.
130+ Further to the work done in the 1.7.0 MAAS Release, MAAS now supports
131+ uploading various versions of CentOS. Previously MAAS would only
132+ officially support 6.5.
133+
134+Power Monitoring for Seamicro 15000, Cisco UCS and HP Moonshot Chassis
135+ Further the work done in the 1.7.0 MAAS release, it now supports power
136+ query and monitoring for the Seamicro 15000 Chassis, the Cisco UCS
137+ Chassis Manager and the HP Moonshot Chassis Manager.
138+
139+Node Listing Page and Node Event Log live refresh
140+ The Node Listing page and the Node Event Log now have live refresh
141+ every 10 seconds. This allows MAAS to display the latest node status
142+ and events without forcing a browser refresh.
143+
144+IP Address Reservation
145+ The static IP address reservation API now has an optional "mac"
146+ parameter. Specifying a MAC address here will link the new static IP
147+ to that MAC address. A DHCP host map will be created for the MAC
148+ address. No other IPs may be reserved for that MAC address until the
149+ current one is released.
150+
151+Bug fix update
152+--------------
153+
154+For full details see https://launchpad.net/maas/+milestone/1.7.1
155+
156+#1330765 If start_nodes() fails, it doesn't clean up after itself.
157+#1373261 pserv.yaml rewrite breaks when previous generator URL uses IPv6 address
158+#1386432 After update to the latest curtin that changes the log to install.log MAAS show's two installation logs
159+#1386488 If rndc fails, you get an Internal Server Error page
160+#1386502 No "failed" transition from "new"
161+#1386914 twisted Unhandled Error when region can't reach upstream boot resource
162+#1391139 Tagged VLAN on aliased NIC breaks migration 0099
163+#1391161 Failure: twisted.internet.error.ConnectionDone: Connection was closed cleanly.
164+#1391411 metadata API signal() is releasing host maps at the end of installation
165+#1391897 Network names with dots cause internal server error when on node pages
166+#1394382 maas does not know about VM "paused" state
167+#1396308 Removing managed interface causes maas to delete nodes
168+#1397356 Disk Wiping fails if installation is not Ubuntu
169+#1398405 MAAS UI reports storage size in Gibibytes (base 2) but is labeled GB - Gigabytes (base 10).
170+#1399331 MAAS leaking sensitive information in ps ax output
171+#1400849 Check Power State disappears after upgrade to 1.7 bzr 3312
172+#1401241 custom dd-tgz format images looked for in wrong path, so they don't work
173+#1401983 Exception: deadlock detected
174+#1403609 can not enlist chassis with maas admin node-group probe-and-enlist-mscm
175+#1283106 MAAS allows the same subnet to be defined on two managed interfaces of the same cluster
176+#1303925 commissioning fails silently if a node can't reach the region controller
177+#1357073 power state changes are not reflected quickly enough in the UI
178+#1360280 boot-source-selections api allows adding bogus and duplicated values
179+#1368400 Can't power off nodes that are in Ready state but on
180+#1370897 The node power monitoring service does not check nodes in parallel
181+#1376024 gpg --batch [...]` error caused by race in BootSourceCacheService
182+#1376716 AMT NUC stuck at boot prompt instead of powering down (no ACPI support in syslinux poweroff)
183+#1378835 Config does not have a unique index on name
184+#1379370 Consider removing transaction in claim_static_ip_addresses().
185+#1379556 Panicky log warning that is irrelevant
186+#1381444 Misleading error message in log "Unknown power_type 'sm15k'"
187+#1382166 Message disclosing image import necessary visible while not logged in
188+#1382237 UnicodeEncodeError when unable to create host maps
189+#1383231 Error message when trying to reserve the same static IP twice is unhelpful
190+#1383237 Error message trying to reserve an IP address when no static range is defined is misleading
191+#1384424 Seamicro Machines do not have Power Status Tracking
192+#1384428 HP Moonshot Chassis Manager lacks power status monitoring
193+#1384924 need to provide a better upgrade message for images on the cluster but not on the region
194+#1386517 DHCP leases are not released at the end of commissioning and possibly enlistment
195+#1387239 MAAS does not provide an API for reserving a static IP for a given MAC address
196+#1387414 Race when registering new event type
197+#1388033 Trying to reserve a static IP when no more IPs are available results in 503 Service Unavailable with no error text
198+#1389602 Inconsistent behavior in the checks to delete a node
199+#1389733 node listing does not update the status and power of nodes
200+#1390144 Node 'releasing' should have a timeout
201+#1391193 API error documentation
202+#1391421 Names of custom boot-resources not visible in the web UI
203+#1391891 Spurious test failure: TestDNSForwardZoneConfig_GetGenerateDirectives.test_returns_single_entry_for_tiny_network
204+#1393423 PowerKVM / VIrsh import should allow you to specify a prefix to filter VM's to import
205+#1393953 dd-format images fail to deploy
206+#1400909 Networks are being autocreated like eth0-eth0 instead of maas-eth0
207+#1401349 Memory size changes to incorrect size when page is refreshed
208+#1402237 Node event log queries are slow (over 1 second)
209+#1402243 Nodes in 'Broken' state are being power queried constantly
210+#1402736 clicking on zone link from node page - requested URL was not found on this server
211+#1403043 Wrong top-level tab is selected when viewing a node
212+#1381609 Misleading log message when a node has a MAC address not attached to a cluster interface
213+#1386909 Misleading Error: Unable to identify boot image for (ubuntu/amd64/generic/trusty/local): cluster 'maas' does not have matching boot image.
214+#1388373 Fresh image import of 3 archs displaying multiple rows for armhf and amd64
215+#1398159 TFTP into MAAS server to get pxelinux.0 causes unhandled error
216+#1383651 Node.start() and Node.stop() raise MulltipleFailures unnecessarily
217+#1383668 null" when releasing an IP address is confusing
218+#1389416 Power querying for UCSM not working
219+#1399676 UX bug: mac address on the nodes page should be the MAC address it pxe booted from
220+#1399736 MAAS should display memory sizes in properly labeld base 2 units - MiB, GiB, etc.
221+#1401643 Documentation has wrong pattern for user provided preseeds
222+#1401707 Slow web performance (5+ minute response time) on MAAS with many nodes
223+#1403609 Fix MSCM chassis enlistment.
224+#1409952 Correctly parse MAC Address for Power8 VM enlistment.
225+#1409852 Do not fail when trying to perform an IP Address Reservation.
226+#1413030 OS and Release no longer populate on Add Node page
227+#1414036 Trying to add an empty network crashes (AddrFormatError)
228+
229+
230+>>>>>>> MERGE-SOURCE
231 1.7.0
232 =====
233
234
235=== modified file 'docs/conf.py'
236--- docs/conf.py 2015-05-28 14:48:15 +0000
237+++ docs/conf.py 2015-07-08 00:07:16 +0000
238@@ -100,7 +100,7 @@
239 # built documents.
240 #
241 # The short X.Y version.
242-version = doc_versions.items()[0][0]
243+version = '1.7'
244 # The full version, including alpha/beta/rc tags.
245 release = version
246
247
248=== modified file 'docs/index.rst'
249=== added file 'etc/maas/pserv.yaml.OTHER'
250--- etc/maas/pserv.yaml.OTHER 1970-01-01 00:00:00 +0000
251+++ etc/maas/pserv.yaml.OTHER 2015-07-08 00:07:16 +0000
252@@ -0,0 +1,25 @@
253+##
254+## Provisioning Server (pserv) configuration.
255+##
256+
257+## Where to log. This log can be rotated by sending SIGUSR1 to the
258+## running server.
259+#
260+# logfile: "pserv.log"
261+logfile: "/dev/null"
262+
263+## TFTP configuration.
264+#
265+tftp:
266+ # The "root" setting has been replaced by "resource_root". The old setting
267+ # is used one final time when upgrading a pre-14.04 cluster controller to a
268+ # 14.04 version. After that upgrade, it can be removed.
269+ #
270+ # resource_root: /var/lib/maas/boot-resources/current/
271+
272+ # port: 69
273+ port: 5244
274+ ## The URL to be contacted to generate PXE configurations.
275+ # generator: http://localhost/MAAS/api/1.0/pxeconfig/
276+ generator: http://localhost:5243/api/1.0/pxeconfig/
277+
278
279=== modified file 'etc/maas/templates/commissioning-user-data/snippets/maas_enlist.sh'
280=== modified file 'etc/maas/templates/commissioning-user-data/user_data_poweroff.template'
281--- etc/maas/templates/commissioning-user-data/user_data_poweroff.template 2015-03-26 22:43:10 +0000
282+++ etc/maas/templates/commissioning-user-data/user_data_poweroff.template 2015-07-08 00:07:16 +0000
283@@ -1,3 +1,4 @@
284+<<<<<<< TREE
285 #!/bin/sh
286
287 #### script setup ######
288@@ -44,3 +45,33 @@
289
290 main
291 exit
292+=======
293+#!/bin/sh
294+
295+#### script setup ######
296+PATH="$BIN_D:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
297+
298+write_poweroff_job() {
299+ cat >/etc/init/maas-poweroff.conf <<EOF
300+ description "Power-off when MAAS task is done"
301+ start on stopped cloud-final
302+ console output
303+ task
304+ script
305+ [ ! -e /tmp/block-poweroff ] || exit 0
306+ /sbin/poweroff
307+ end script
308+EOF
309+ # reload required due to lack of inotify in overlayfs (LP: #882147)
310+ initctl reload-configuration
311+}
312+
313+main() {
314+ write_poweroff_job
315+
316+ echo "Powering node off."
317+}
318+
319+main
320+exit
321+>>>>>>> MERGE-SOURCE
322
323=== modified file 'etc/maas/templates/dns/named.conf.options.inside.maas.template'
324=== modified file 'required-packages/base'
325--- required-packages/base 2015-07-07 09:36:31 +0000
326+++ required-packages/base 2015-07-08 00:07:16 +0000
327@@ -12,8 +12,13 @@
328 libjs-angularjs
329 libpq-dev
330 postgresql
331+<<<<<<< TREE
332 python-apt
333 python-bson
334+=======
335+python-amqplib
336+python-bson
337+>>>>>>> MERGE-SOURCE
338 python-bzrlib
339 python-convoy
340 python-crochet
341
342=== modified file 'setup.py'
343=== modified file 'src/maas/development.py'
344--- src/maas/development.py 2015-06-26 10:17:33 +0000
345+++ src/maas/development.py 2015-07-08 00:07:16 +0000
346@@ -93,7 +93,18 @@
347 )
348
349 # Inject custom code for setting up the test database.
350-patch_db_creation(abspath('db'), abspath('schema/baseline.sql'))
351+<<<<<<< TREE
352+patch_db_creation(abspath('db'), abspath('schema/baseline.sql'))
353+=======
354+patch_db_creation(abspath('db'), abspath('schema/baseline.sql'))
355+
356+# Override the default provisioning config filename.
357+provisioningserver.config.Config.DEFAULT_FILENAME = abspath(
358+ "etc/maas/pserv.yaml")
359+
360+# Use the in-branch development version of maas_cluster.conf.
361+LOCAL_CLUSTER_CONFIG = abspath("etc/demo_maas_cluster.conf")
362+>>>>>>> MERGE-SOURCE
363
364 PASSWORD_HASHERS = (
365 'django.contrib.auth.hashers.MD5PasswordHasher',
366
367=== modified file 'src/maas/settings.py'
368--- src/maas/settings.py 2015-06-26 10:17:33 +0000
369+++ src/maas/settings.py 2015-07-08 00:07:16 +0000
370@@ -39,10 +39,16 @@
371
372 MANAGERS = ADMINS
373
374+<<<<<<< TREE
375 # The following specify named URL patterns.
376 LOGOUT_URL = 'logout'
377 LOGIN_REDIRECT_URL = 'index'
378 LOGIN_URL = 'login'
379+=======
380+LOGOUT_URL = '/'
381+LOGIN_REDIRECT_URL = '/'
382+LOGIN_URL = '/accounts/login/'
383+>>>>>>> MERGE-SOURCE
384
385 # Should the DNS features be enabled? Having this config option is a
386 # debugging/testing feature to be able to quickly disconnect the DNS
387
388=== modified file 'src/maascli/api.py'
389--- src/maascli/api.py 2015-06-22 15:53:34 +0000
390+++ src/maascli/api.py 2015-07-08 00:07:16 +0000
391@@ -27,8 +27,13 @@
392 from textwrap import (
393 dedent,
394 fill,
395+<<<<<<< TREE
396 wrap,
397 )
398+=======
399+ wrap,
400+ )
401+>>>>>>> MERGE-SOURCE
402 from urlparse import (
403 urljoin,
404 urlparse,
405
406=== modified file 'src/maascli/tests/test_api.py'
407--- src/maascli/tests/test_api.py 2015-06-22 15:53:27 +0000
408+++ src/maascli/tests/test_api.py 2015-07-08 00:07:16 +0000
409@@ -248,6 +248,54 @@
410 **********************************************************************
411 """)))
412
413+ def test_compare_api_hashes_prints_nothing_if_hashes_match(self):
414+ example_hash = factory.make_name("hash")
415+ profile = {"description": {"hash": example_hash}}
416+ response = {"x-maas-api-hash": example_hash}
417+ with CaptureStandardIO() as stdio:
418+ api.Action.compare_api_hashes(profile, response)
419+ self.assertThat(stdio.getOutput(), Equals(""))
420+ self.assertThat(stdio.getError(), Equals(""))
421+
422+ def test_compare_api_hashes_prints_nothing_if_remote_has_no_hash(self):
423+ example_hash = factory.make_name("hash")
424+ profile = {"description": {"hash": example_hash}}
425+ response = {}
426+ with CaptureStandardIO() as stdio:
427+ api.Action.compare_api_hashes(profile, response)
428+ self.assertThat(stdio.getOutput(), Equals(""))
429+ self.assertThat(stdio.getError(), Equals(""))
430+
431+ def test_compare_api_hashes_prints_warning_if_local_has_no_hash(self):
432+ example_hash = factory.make_name("hash")
433+ profile = {"description": {}}
434+ response = {"x-maas-api-hash": example_hash}
435+ with CaptureStandardIO() as stdio:
436+ api.Action.compare_api_hashes(profile, response)
437+ self.assertThat(stdio.getOutput(), Equals(""))
438+ self.assertThat(stdio.getError(), Equals(dedent("""\
439+ **********************************************************************
440+ *** WARNING! The API on the server differs from the description that
441+ *** is cached locally. This may result in failed API calls. Refresh
442+ *** the local API description with `maas refresh`.
443+ **********************************************************************
444+ """)))
445+
446+ def test_compare_api_hashes_prints_warning_if_hashes_dont_match(self):
447+ example_hash = factory.make_name("hash")
448+ profile = {"description": {"hash": example_hash + "foo"}}
449+ response = {"x-maas-api-hash": example_hash + "bar"}
450+ with CaptureStandardIO() as stdio:
451+ api.Action.compare_api_hashes(profile, response)
452+ self.assertThat(stdio.getOutput(), Equals(""))
453+ self.assertThat(stdio.getError(), Equals(dedent("""\
454+ **********************************************************************
455+ *** WARNING! The API on the server differs from the description that
456+ *** is cached locally. This may result in failed API calls. Refresh
457+ *** the local API description with `maas refresh`.
458+ **********************************************************************
459+ """)))
460+
461
462 class TestActionHelp(MAASTestCase):
463
464
465=== modified file 'src/maascli/tests/test_utils.py'
466=== modified file 'src/maascli/utils.py'
467=== modified file 'src/maasserver/api/doc.py'
468--- src/maasserver/api/doc.py 2015-04-21 22:51:42 +0000
469+++ src/maasserver/api/doc.py 2015-07-08 00:07:16 +0000
470@@ -21,10 +21,17 @@
471 "get_api_description_hash",
472 ]
473
474+<<<<<<< TREE
475 from collections import (
476 Mapping,
477 Sequence,
478 )
479+=======
480+from collections import (
481+ Mapping,
482+ Sequence,
483+ )
484+>>>>>>> MERGE-SOURCE
485 from cStringIO import StringIO
486 from functools import partial
487 import hashlib
488@@ -37,7 +44,11 @@
489 get_resolver,
490 RegexURLPattern,
491 RegexURLResolver,
492+<<<<<<< TREE
493 )
494+=======
495+ )
496+>>>>>>> MERGE-SOURCE
497 from piston.authentication import NoAuthentication
498 from piston.doc import generate_doc
499 from piston.handler import BaseHandler
500
501=== modified file 'src/maasserver/api/doc_handler.py'
502--- src/maasserver/api/doc_handler.py 2015-04-21 22:51:42 +0000
503+++ src/maasserver/api/doc_handler.py 2015-07-08 00:07:16 +0000
504@@ -77,8 +77,13 @@
505 find_api_resources,
506 generate_api_docs,
507 generate_power_types_doc,
508+<<<<<<< TREE
509 get_api_description_hash,
510 )
511+=======
512+ get_api_description_hash,
513+ )
514+>>>>>>> MERGE-SOURCE
515 from maasserver.utils import build_absolute_uri
516 import simplejson as json
517
518
519=== modified file 'src/maasserver/api/files.py'
520=== modified file 'src/maasserver/api/ip_addresses.py'
521--- src/maasserver/api/ip_addresses.py 2015-06-13 06:45:07 +0000
522+++ src/maasserver/api/ip_addresses.py 2015-07-08 00:07:16 +0000
523@@ -17,6 +17,7 @@
524 'IPAddressesHandler',
525 ]
526
527+from django.db import transaction
528 from django.shortcuts import get_object_or_404
529 from maasserver.api.support import (
530 operation,
531@@ -25,6 +26,7 @@
532 from maasserver.api.utils import (
533 get_mandatory_param,
534 get_optional_param,
535+<<<<<<< TREE
536 )
537 from maasserver.clusterrpc import dhcp
538 from maasserver.enum import IPADDRESS_TYPE
539@@ -49,6 +51,27 @@
540 IPNetwork,
541 )
542 from netaddr.core import AddrFormatError
543+=======
544+ )
545+from maasserver.clusterrpc.dhcp import (
546+ remove_host_maps,
547+ update_host_maps,
548+ )
549+from maasserver.enum import (
550+ IPADDRESS_TYPE,
551+ NODEGROUP_STATUS,
552+ )
553+from maasserver.exceptions import (
554+ MAASAPIBadRequest,
555+ StaticIPAlreadyExistsForMACAddress,
556+ )
557+from maasserver.models import (
558+ NodeGroupInterface,
559+ StaticIPAddress,
560+ )
561+from maasserver.models.macaddress import MACAddress
562+import netaddr
563+>>>>>>> MERGE-SOURCE
564 from provisioningserver.logger import get_maas_logger
565
566
567@@ -67,15 +90,20 @@
568 def resource_uri(cls, *args, **kwargs):
569 return ('ipaddresses_handler', [])
570
571+<<<<<<< TREE
572 @transactional
573 def claim_ip(
574 self, user, interface, requested_address, mac=None,
575 hostname=None):
576+=======
577+ def claim_ip(self, user, interface, requested_address, mac=None):
578+>>>>>>> MERGE-SOURCE
579 """Attempt to get a USER_RESERVED StaticIPAddress for `user` on
580 `interface`.
581
582 :raises StaticIPAddressExhaustion: If no IPs available.
583 """
584+<<<<<<< TREE
585 if mac is None:
586 sip = StaticIPAddress.objects.allocate_new(
587 network=interface.network,
588@@ -134,6 +162,60 @@
589 maaslog.info(
590 "User %s was allocated IP %s for MAC address %s",
591 user.username, sip.ip, mac_address.mac_address)
592+=======
593+ if mac is None:
594+ sip = StaticIPAddress.objects.allocate_new(
595+ range_low=interface.static_ip_range_low,
596+ range_high=interface.static_ip_range_high,
597+ alloc_type=IPADDRESS_TYPE.USER_RESERVED,
598+ requested_address=requested_address,
599+ user=user)
600+ transaction.commit()
601+ maaslog.info("User %s was allocated IP %s", user.username, sip.ip)
602+ else:
603+ # The user has requested a static IP linked to a MAC
604+ # address, so we set that up via the MACAddress model.
605+ mac_address, _ = MACAddress.objects.get_or_create(
606+ mac_address=mac, cluster_interface=interface)
607+ ips_on_interface = (
608+ addr.ip for addr in mac_address.ip_addresses.all()
609+ if netaddr.IPAddress(addr.ip) in interface.network)
610+ if any(ips_on_interface):
611+ # If this MAC already has static IPs on the interface in
612+ # question we raise an error, since we can't sanely
613+ # allocate more addresses for the MAC here.
614+ raise StaticIPAlreadyExistsForMACAddress(
615+ "MAC address %s already has the IP address(es) %s." %
616+ (mac, ips_on_interface))
617+
618+ [sip] = mac_address.claim_static_ips(
619+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=user,
620+ requested_address=requested_address)
621+ # Update the DHCP host maps for the cluster so that this MAC
622+ # gets an entry with this static IP.
623+ host_map_updates = {
624+ interface.nodegroup: {
625+ sip.ip: mac_address.mac_address,
626+ }
627+ }
628+ # Commit the DB changes before we do RPC calls.
629+ transaction.commit()
630+ update_host_maps_failures = list(
631+ update_host_maps(host_map_updates))
632+ if len(update_host_maps_failures) > 0:
633+ # Deallocate the static IPs and delete the MAC address
634+ # if it doesn't have a Node attached.
635+ if mac_address.node is None:
636+ mac_address.delete()
637+ sip.deallocate()
638+ transaction.commit()
639+
640+ # There will only ever be one error, so raise that.
641+ raise update_host_maps_failures[0].value
642+ maaslog.info(
643+ "User %s was allocated IP %s for MAC address %s",
644+ user.username, sip.ip, mac_address.mac_address)
645+>>>>>>> MERGE-SOURCE
646 return sip
647
648 @operation(idempotent=False)
649@@ -154,15 +236,22 @@
650 a static IP address range managed by MAAS.
651 :param hostname: the hostname to use for the specified IP address
652 :type network: unicode
653+<<<<<<< TREE
654
655 Returns 400 if there is no network in MAAS matching the provided one,
656 or a requested_address is supplied, but a corresponding network
657 could not be found.
658 Returns 503 if there are no more IP addresses available.
659+=======
660+
661+ Returns 400 if there is no network in MAAS matching the provided one.
662+ Returns 503 if there are no more IP addresses available.
663+>>>>>>> MERGE-SOURCE
664 """
665 network = get_optional_param(request.POST, "network")
666 requested_address = get_optional_param(
667 request.POST, "requested_address")
668+<<<<<<< TREE
669 hostname = get_optional_param(request.POST, "hostname")
670 mac_address = get_optional_param(request.POST, "mac")
671
672@@ -203,6 +292,30 @@
673 hostname=hostname)
674
675 return sip
676+=======
677+ mac_address = get_optional_param(request.POST, "mac")
678+ # Validate the passed network.
679+ try:
680+ valid_network = netaddr.IPNetwork(network)
681+ except netaddr.core.AddrFormatError:
682+ raise MAASAPIBadRequest("Invalid network parameter %s" % network)
683+
684+ # Match the network to a nodegroupinterface.
685+ interfaces = (
686+ NodeGroupInterface.objects.filter(
687+ nodegroup__status=NODEGROUP_STATUS.ACCEPTED)
688+ .exclude(static_ip_range_low__isnull=True)
689+ .exclude(static_ip_range_high__isnull=True)
690+ )
691+ for interface in interfaces:
692+ if valid_network == interface.network:
693+ # Winner winner chicken dinner.
694+ return self.claim_ip(
695+ request.user, interface, requested_address, mac_address)
696+ raise MAASAPIBadRequest(
697+ "No network found matching %s; you may be requesting an IP "
698+ "on a network with no static IP range defined." % network)
699+>>>>>>> MERGE-SOURCE
700
701 @operation(idempotent=False)
702 def release(self, request):
703@@ -221,6 +334,7 @@
704
705 staticaddress = get_object_or_404(
706 StaticIPAddress, ip=ip, user=request.user)
707+<<<<<<< TREE
708
709 linked_mac_addresses = staticaddress.macaddress_set
710 linked_mac_address_interfaces = set(
711@@ -243,6 +357,30 @@
712 # at SERIALIZABLE there will be no race here, and it's better to
713 # keep cruft out of the DB.
714 linked_mac_addresses.filter(node=None).delete()
715+=======
716+
717+ linked_mac_addresses = staticaddress.macaddress_set
718+ linked_mac_address_interfaces = set(
719+ mac_address.cluster_interface
720+ for mac_address in linked_mac_addresses.all())
721+
722+ # Remove any hostmaps for this IP.
723+ host_maps_to_remove = {
724+ interface.nodegroup: [staticaddress.ip]
725+ for interface in linked_mac_address_interfaces
726+ }
727+ remove_host_maps_failures = list(
728+ remove_host_maps(host_maps_to_remove))
729+ if len(remove_host_maps_failures) > 0:
730+ # There's only going to be one failure, so raise that.
731+ raise remove_host_maps_failures[0].value
732+
733+ # Delete any MACAddress entries that are attached to this static
734+ # IP but that *aren't* attached to a Node. With the DB isolation
735+ # at SERIALIZABLE there will be no race here, and it's better to
736+ # keep cruft out of the DB.
737+ linked_mac_addresses.filter(node=None).delete()
738+>>>>>>> MERGE-SOURCE
739 staticaddress.deallocate()
740
741 maaslog.info("User %s released IP %s", request.user.username, ip)
742
743=== modified file 'src/maasserver/api/node_group_interfaces.py'
744=== modified file 'src/maasserver/api/node_macs.py'
745=== modified file 'src/maasserver/api/nodegroups.py'
746--- src/maasserver/api/nodegroups.py 2015-06-15 18:30:41 +0000
747+++ src/maasserver/api/nodegroups.py 2015-07-08 00:07:16 +0000
748@@ -415,6 +415,7 @@
749 :param power_pass: The password to use, when qemu+ssh is given as a
750 connection string and ssh key authentication is not being used.
751 :type power_pass: unicode
752+<<<<<<< TREE
753 :param prefix_filter: Filter nodes with supplied prefix.
754 :type prefix_filter: unicode
755
756@@ -469,6 +470,14 @@
757 Returns 404 if the nodegroup (cluster) is not found.
758 Returns 403 if the user does not have access to the nodegroup.
759 Returns 400 if the required parameters were not passed.
760+=======
761+ :param prefix_filter: Only import nodes based on supplied prefix.
762+ :type prefix_filter: unicode
763+
764+ Returns 404 if the nodegroup (cluster) is not found.
765+ Returns 403 if the user does not have access to the nodegroup.
766+ Returns 400 if the required parameters were not passed.
767+>>>>>>> MERGE-SOURCE
768 """
769 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
770
771@@ -491,6 +500,7 @@
772 poweraddr = get_mandatory_param(request.data, 'power_address')
773 password = get_optional_param(
774 request.data, 'power_pass', default=None)
775+<<<<<<< TREE
776 prefix_filter = get_optional_param(
777 request.data, 'prefix_filter', default=None)
778
779@@ -518,6 +528,13 @@
780 user, host, username, password, port=port,
781 protocol=protocol, prefix_filter=prefix_filter,
782 accept_all=accept_all)
783+=======
784+ prefix_filter = get_optional_param(
785+ request.data, 'prefix_filter', default=None)
786+
787+ nodegroup.add_virsh(
788+ poweraddr, password=password, prefix_filter=prefix_filter)
789+>>>>>>> MERGE-SOURCE
790 else:
791 return HttpResponse(status=httplib.BAD_REQUEST)
792
793
794=== modified file 'src/maasserver/api/nodes.py'
795--- src/maasserver/api/nodes.py 2015-07-07 10:12:26 +0000
796+++ src/maasserver/api/nodes.py 2015-07-08 00:07:16 +0000
797@@ -40,6 +40,7 @@
798 get_optional_param,
799 )
800 from maasserver.clusterrpc.power_parameters import get_power_types
801+from maasserver.dns.config import change_dns_zones
802 from maasserver.enum import (
803 IPADDRESS_TYPE,
804 NODE_PERMISSION,
805@@ -260,11 +261,19 @@
806 return node.owner.username
807
808 def read(self, request, system_id):
809+<<<<<<< TREE
810 """Read a specific Node.
811
812 Returns 404 if the node is not found.
813 """
814 return Node.nodes.get_node_or_404(
815+=======
816+ """Read a specific Node.
817+
818+ Returns 404 if the node is not found.
819+ """
820+ return Node.objects.get_node_or_404(
821+>>>>>>> MERGE-SOURCE
822 system_id=system_id, user=request.user, perm=NODE_PERMISSION.VIEW)
823
824 def update(self, request, system_id):
825@@ -297,6 +306,7 @@
826 :type power_parameters_skip_check: unicode
827 :param zone: Name of a valid physical zone in which to place this node
828 :type zone: unicode
829+<<<<<<< TREE
830 :param swap_size: Specifies the size of the swap file, in bytes. Field
831 accept K, M, G and T suffixes for values expressed respectively in
832 kilobytes, megabytes, gigabytes and terabytes.
833@@ -309,18 +319,28 @@
834
835 Returns 404 if the node is node found.
836 Returns 403 if the user does not have permission to update the node.
837+=======
838+ :param boot_type: The installation type of the node. 'fastpath': use
839+ the default installer. 'di' use the debian installer.
840+ Note that using 'di' is now deprecated and will be removed in favor
841+ of the default installer in MAAS 1.9.
842+ :type boot_type: unicode
843+
844+ Returns 404 if the node is node found.
845+ Returns 403 if the user does not have permission to update the node.
846+>>>>>>> MERGE-SOURCE
847 """
848 node = Node.nodes.get_node_or_404(
849 system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
850 Form = get_node_edit_form(request.user)
851 form = Form(data=request.data, instance=node)
852-
853 if form.is_valid():
854 return form.save()
855 else:
856 raise MAASAPIValidationError(form.errors)
857
858 def delete(self, request, system_id):
859+<<<<<<< TREE
860 """Delete a specific Node.
861
862 Returns 404 if the node is not found.
863@@ -328,6 +348,15 @@
864 Returns 204 if the node is successfully deleted.
865 """
866 node = Node.nodes.get_node_or_404(
867+=======
868+ """Delete a specific Node.
869+
870+ Returns 404 if the node is not found.
871+ Returns 403 if the user does not have permission to delete the node.
872+ Returns 204 if the node is successfully deleted.
873+ """
874+ node = Node.objects.get_node_or_404(
875+>>>>>>> MERGE-SOURCE
876 system_id=system_id, user=request.user,
877 perm=NODE_PERMISSION.ADMIN)
878 node.delete()
879@@ -430,6 +459,7 @@
880
881 @operation(idempotent=False)
882 def release(self, request, system_id):
883+<<<<<<< TREE
884 """Release a node. Opposite of `NodesHandler.acquire`.
885
886 Returns 404 if the node is not found.
887@@ -437,6 +467,15 @@
888 Returns 409 if the node is in a state where it may not be released.
889 """
890 node = Node.nodes.get_node_or_404(
891+=======
892+ """Release a node. Opposite of `NodesHandler.acquire`.
893+
894+ Returns 404 if the node is not found.
895+ Returns 403 if the user does not have permission to release the node.
896+ Returns 409 if the node is in a state where it may not be released.
897+ """
898+ node = Node.objects.get_node_or_404(
899+>>>>>>> MERGE-SOURCE
900 system_id=system_id, user=request.user, perm=NODE_PERMISSION.EDIT)
901 if node.status == NODE_STATUS.RELEASING or \
902 node.status == NODE_STATUS.READY:
903@@ -549,7 +588,16 @@
904
905 sticky_ips = mac_address.claim_static_ips(
906 alloc_type=IPADDRESS_TYPE.STICKY,
907+<<<<<<< TREE
908 requested_address=requested_address, update_host_maps=True)
909+=======
910+ requested_address=requested_address)
911+ claims = [
912+ (static_ip.ip, mac_address.mac_address.get_raw())
913+ for static_ip in sticky_ips]
914+ node.update_host_maps(claims)
915+ change_dns_zones(node.nodegroup)
916+>>>>>>> MERGE-SOURCE
917 maaslog.info(
918 "%s: Sticky IP address(es) allocated: %s", node.hostname,
919 ', '.join(allocation.ip for allocation in sticky_ips))
920@@ -613,6 +661,7 @@
921
922 @operation(idempotent=False)
923 def mark_fixed(self, request, system_id):
924+<<<<<<< TREE
925 """Mark a broken node as fixed and set its status as 'ready'.
926
927 Returns 404 if the node is not found.
928@@ -620,6 +669,15 @@
929 broken.
930 """
931 node = Node.nodes.get_node_or_404(
932+=======
933+ """Mark a broken node as fixed and set its status as 'ready'.
934+
935+ Returns 404 if the node is not found.
936+ Returns 403 if the user does not have permission to mark the node
937+ broken.
938+ """
939+ node = Node.objects.get_node_or_404(
940+>>>>>>> MERGE-SOURCE
941 user=request.user, system_id=system_id, perm=NODE_PERMISSION.ADMIN)
942 node.mark_fixed()
943 maaslog.info(
944@@ -657,11 +715,18 @@
945 :param system_id: The node to query.
946 :return: a dict whose key is "state" with a value of one of
947 'on' or 'off'.
948+<<<<<<< TREE
949
950 Returns 400 if the node is not installable.
951 Returns 404 if the node is not found.
952 Returns 503 (with explanatory text) if the power state could not
953 be queried.
954+=======
955+
956+ Returns 404 if the node is not found.
957+ Returns 503 (with explanatory text) if the power state could not
958+ be queried.
959+>>>>>>> MERGE-SOURCE
960 """
961 node = get_object_or_404(Node, system_id=system_id)
962 if not node.installable:
963@@ -801,6 +866,7 @@
964 """Create a new Node.
965
966 Adding a server to a MAAS puts it on a path that will wipe its disks
967+<<<<<<< TREE
968 and re-install its operating system, in the event that it PXE boots.
969 In anonymous enlistment (and when the enlistment is done by a
970 non-admin), the node is held in the "New" state for approval by a MAAS
971@@ -835,6 +901,16 @@
972 Note that using 'di' is now deprecated and will be removed in favor
973 of the default installer in MAAS 1.9.
974 :type boot_type: unicode
975+=======
976+ and re-install its operating system. In anonymous enlistment and when
977+ the enlistment is done by a non-admin, the node is held in the
978+ "New" state for approval by a MAAS admin.
979+ :param boot_type: The installation type of the node. 'fastpath': use
980+ the default installer. 'di' use the debian installer.
981+ Note that using 'di' is now deprecated and will be removed in favor
982+ of the default installer in MAAS 1.9.
983+ :type boot_type: unicode
984+>>>>>>> MERGE-SOURCE
985 """
986 return create_node(request)
987
988
989=== modified file 'src/maasserver/api/pxeconfig.py'
990--- src/maasserver/api/pxeconfig.py 2015-05-13 19:20:54 +0000
991+++ src/maasserver/api/pxeconfig.py 2015-07-08 00:07:16 +0000
992@@ -33,8 +33,13 @@
993 Event,
994 MACAddress,
995 NodeGroup,
996+<<<<<<< TREE
997 )
998 from maasserver.models.macaddress import update_mac_cluster_interfaces
999+=======
1000+ )
1001+from maasserver.models.macaddress import update_mac_cluster_interfaces
1002+>>>>>>> MERGE-SOURCE
1003 from maasserver.preseed import (
1004 compose_enlistment_preseed_url,
1005 compose_preseed_url,
1006@@ -171,6 +176,7 @@
1007 node = get_node_from_mac_string(request_mac)
1008
1009 if node is not None:
1010+<<<<<<< TREE
1011 # Only update the PXE booting interface for the node if it has
1012 # changed.
1013 if (node.pxe_mac is None or
1014@@ -183,6 +189,19 @@
1015 update_mac_cluster_interfaces(request_ip, request_mac, node.nodegroup)
1016
1017 if node is None or node.get_boot_purpose() == "commissioning":
1018+=======
1019+ # Only update the PXE booting interface for the node if it has
1020+ # changed.
1021+ if (node.pxe_mac is None or
1022+ node.pxe_mac.mac_address != request_mac):
1023+ node.pxe_mac = MACAddress.objects.get(mac_address=request_mac)
1024+ node.save()
1025+ # Update the record of which MAC address and cluster interface
1026+ # this node uses to PXE boot.
1027+ update_mac_cluster_interfaces(request_ip, request_mac, node.nodegroup)
1028+
1029+ if node is None or node.get_boot_purpose() == "commissioning":
1030+>>>>>>> MERGE-SOURCE
1031 osystem = Config.objects.get_config('commissioning_osystem')
1032 series = Config.objects.get_config('commissioning_distro_series')
1033 else:
1034
1035=== modified file 'src/maasserver/api/ssh_keys.py'
1036=== modified file 'src/maasserver/api/ssl_keys.py'
1037=== modified file 'src/maasserver/api/support.py'
1038--- src/maasserver/api/support.py 2015-04-21 22:51:42 +0000
1039+++ src/maasserver/api/support.py 2015-07-08 00:07:16 +0000
1040@@ -47,12 +47,22 @@
1041 crudmap = Resource.callmap
1042 callmap = dict.fromkeys(crudmap, "dispatch")
1043
1044- def __call__(self, request, *args, **kwargs):
1045- upcall = super(OperationsResource, self).__call__
1046- response = upcall(request, *args, **kwargs)
1047- response["X-MAAS-API-Hash"] = get_api_description_hash()
1048- return response
1049-
1050+<<<<<<< TREE
1051+ def __call__(self, request, *args, **kwargs):
1052+ upcall = super(OperationsResource, self).__call__
1053+ response = upcall(request, *args, **kwargs)
1054+ response["X-MAAS-API-Hash"] = get_api_description_hash()
1055+ return response
1056+
1057+=======
1058+ def __call__(self, request, *args, **kwargs):
1059+ upcall = super(OperationsResource, self).__call__
1060+ response = upcall(request, *args, **kwargs)
1061+ from maasserver.api.doc import get_api_description_hash
1062+ response["X-MAAS-API-Hash"] = get_api_description_hash()
1063+ return response
1064+
1065+>>>>>>> MERGE-SOURCE
1066 def error_handler(self, e, request, meth, em_format):
1067 """
1068 Override piston's error_handler to fix bug #1228205 and generally
1069
1070=== modified file 'src/maasserver/api/tags.py'
1071=== modified file 'src/maasserver/api/tests/test_describe.py'
1072=== modified file 'src/maasserver/api/tests/test_doc.py'
1073--- src/maasserver/api/tests/test_doc.py 2015-06-24 17:51:20 +0000
1074+++ src/maasserver/api/tests/test_doc.py 2015-07-08 00:07:16 +0000
1075@@ -33,10 +33,16 @@
1076 find_api_resources,
1077 generate_api_docs,
1078 generate_power_types_doc,
1079+<<<<<<< TREE
1080 get_api_description_hash,
1081 hash_canonical,
1082 )
1083 from maasserver.api.doc_handler import render_api_docs
1084+=======
1085+ get_api_description_hash,
1086+ hash_canonical,
1087+ )
1088+>>>>>>> MERGE-SOURCE
1089 from maasserver.api.support import (
1090 operation,
1091 OperationsHandler,
1092@@ -45,16 +51,25 @@
1093 from maasserver.testing.config import RegionConfigurationFixture
1094 from maasserver.testing.factory import factory
1095 from maasserver.testing.testcase import MAASServerTestCase
1096+<<<<<<< TREE
1097 from maastesting.matchers import (
1098 IsCallable,
1099 MockCalledOnceWith,
1100 )
1101 from maastesting.testcase import MAASTestCase
1102+=======
1103+from maastesting.matchers import (
1104+ IsCallable,
1105+ MockCalledOnceWith,
1106+ )
1107+from maastesting.testcase import MAASTestCase
1108+>>>>>>> MERGE-SOURCE
1109 from mock import sentinel
1110 from piston.doc import HandlerDocumentation
1111 from piston.handler import BaseHandler
1112 from piston.resource import Resource
1113 from provisioningserver.power_schema import make_json_field
1114+<<<<<<< TREE
1115 from testtools.matchers import (
1116 AfterPreprocessing,
1117 AllMatch,
1118@@ -69,6 +84,22 @@
1119 MatchesStructure,
1120 Not,
1121 )
1122+=======
1123+from testtools.matchers import (
1124+ AfterPreprocessing,
1125+ AllMatch,
1126+ ContainsAll,
1127+ Equals,
1128+ HasLength,
1129+ Is,
1130+ IsInstance,
1131+ MatchesAll,
1132+ MatchesAny,
1133+ MatchesDict,
1134+ MatchesStructure,
1135+ Not,
1136+ )
1137+>>>>>>> MERGE-SOURCE
1138
1139
1140 class TestFindingResources(MAASServerTestCase):
1141@@ -371,6 +402,7 @@
1142 }
1143 self.assertEqual(expected, describe_resource(resource))
1144
1145+<<<<<<< TREE
1146 def test_describe_api_returns_description_document(self):
1147 is_list = IsInstance(list)
1148 is_tuple = IsInstance(tuple)
1149@@ -416,6 +448,53 @@
1150 "handlers": is_legacy_handler_list,
1151 }))
1152
1153+=======
1154+ def test_describe_api_returns_description_document(self):
1155+ is_list = IsInstance(list)
1156+ is_tuple = IsInstance(tuple)
1157+ is_text = MatchesAll(IsInstance((unicode, bytes), Not(HasLength(0))))
1158+ is_bool = IsInstance(bool)
1159+
1160+ is_operation = MatchesAny(Is(None), is_text)
1161+
1162+ is_http_method = MatchesAny(
1163+ Equals("GET"), Equals("POST"),
1164+ Equals("PUT"), Equals("DELETE"),
1165+ )
1166+
1167+ is_action = MatchesDict({
1168+ "doc": is_text,
1169+ "method": is_http_method,
1170+ "name": is_text,
1171+ "op": is_operation,
1172+ "restful": is_bool,
1173+ })
1174+
1175+ is_handler = MatchesDict({
1176+ "actions": MatchesAll(is_list, AllMatch(is_action)),
1177+ "doc": is_text,
1178+ "name": is_text,
1179+ "params": is_tuple,
1180+ "path": is_text,
1181+ })
1182+
1183+ is_resource = MatchesDict({
1184+ "anon": MatchesAny(Is(None), is_handler),
1185+ "auth": is_handler,
1186+ "name": is_text,
1187+ })
1188+
1189+ is_resource_list = MatchesAll(is_list, AllMatch(is_resource))
1190+ is_legacy_handler_list = MatchesAll(is_list, AllMatch(is_handler))
1191+
1192+ self.assertThat(
1193+ describe_api(), MatchesDict({
1194+ "doc": Equals("MAAS API"),
1195+ "resources": is_resource_list,
1196+ "handlers": is_legacy_handler_list,
1197+ }))
1198+
1199+>>>>>>> MERGE-SOURCE
1200
1201 class TestGeneratePowerTypesDoc(MAASServerTestCase):
1202 """Tests for `generate_power_types_doc`."""
1203
1204=== modified file 'src/maasserver/api/tests/test_enlistment.py'
1205=== modified file 'src/maasserver/api/tests/test_ipaddresses.py'
1206--- src/maasserver/api/tests/test_ipaddresses.py 2015-06-13 06:16:08 +0000
1207+++ src/maasserver/api/tests/test_ipaddresses.py 2015-07-08 00:07:16 +0000
1208@@ -18,8 +18,13 @@
1209 import json
1210
1211 from django.core.urlresolvers import reverse
1212+<<<<<<< TREE
1213 from maasserver.clusterrpc import dhcp as dhcp_module
1214 from maasserver.dns import config as dns_config_module
1215+=======
1216+from django.db import transaction
1217+from maasserver.api import ip_addresses as ip_addresses_module
1218+>>>>>>> MERGE-SOURCE
1219 from maasserver.enum import (
1220 IPADDRESS_TYPE,
1221 NODEGROUP_STATUS,
1222@@ -28,9 +33,16 @@
1223 from maasserver.models.macaddress import MACAddress
1224 from maasserver.testing.api import APITestCase
1225 from maasserver.testing.factory import factory
1226+from maasserver.testing.oauthclient import OAuthAuthenticatedClient
1227 from maasserver.testing.orm import reload_object
1228-from maastesting.matchers import MockCalledOnceWith
1229+<<<<<<< TREE
1230+from maastesting.matchers import MockCalledOnceWith
1231+=======
1232+from maastesting.djangotestcase import TransactionTestCase
1233+from maastesting.matchers import MockCalledOnceWith
1234+>>>>>>> MERGE-SOURCE
1235 from netaddr import IPAddress
1236+<<<<<<< TREE
1237 from provisioningserver.rpc.exceptions import NoConnectionsAvailable
1238 from testtools.matchers import (
1239 Equals,
1240@@ -39,6 +51,46 @@
1241 Not,
1242 )
1243 from twisted.python.failure import Failure
1244+=======
1245+from provisioningserver.rpc.exceptions import NoConnectionsAvailable
1246+from testtools.matchers import (
1247+ Contains,
1248+ Equals,
1249+ HasLength,
1250+ Is,
1251+ Not,
1252+ )
1253+from twisted.python.failure import Failure
1254+
1255+
1256+class TestNetworksAPITransaction(TransactionTestCase):
1257+
1258+ def test_transactional_reserve_creates_ipaddress(self):
1259+ # Reserving an address through the API method 'reserve' works
1260+ # with transaction management enabled (see bug 1409852 for details).
1261+ with transaction.atomic():
1262+ self.logged_in_user = factory.make_User(
1263+ username='test', password='test')
1264+ self.logged_in_user.is_superuser = True
1265+ self.logged_in_user.save()
1266+ self.client = OAuthAuthenticatedClient(self.logged_in_user)
1267+
1268+ interface = factory.make_NodeGroupInterface(
1269+ factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED))
1270+ net = interface.network
1271+ params = {
1272+ 'op': 'reserve',
1273+ 'network': unicode(net),
1274+ }
1275+ response = self.client.post(reverse('ipaddresses_handler'), params)
1276+
1277+ self.assertEqual(httplib.OK, response.status_code, response.content)
1278+ [staticipaddress] = StaticIPAddress.objects.all()
1279+ self.assertThat(net, Contains(IPAddress(staticipaddress.ip)))
1280+ self.assertEqual(
1281+ IPADDRESS_TYPE.USER_RESERVED, staticipaddress.alloc_type)
1282+ self.assertEqual(self.logged_in_user, staticipaddress.user)
1283+>>>>>>> MERGE-SOURCE
1284
1285
1286 class TestNetworksAPI(APITestCase):
1287@@ -47,25 +99,35 @@
1288 cluster = factory.make_NodeGroup(status=status, **kwargs)
1289 return factory.make_NodeGroupInterface(cluster)
1290
1291+<<<<<<< TREE
1292 def post_reservation_request(
1293 self, net=None, requested_address=None, mac=None, hostname=None):
1294+=======
1295+ def post_reservation_request(self, net, requested_address=None, mac=None):
1296+>>>>>>> MERGE-SOURCE
1297 params = {
1298 'op': 'reserve',
1299 }
1300 if requested_address is not None:
1301 params["requested_address"] = requested_address
1302+<<<<<<< TREE
1303 if net is not None:
1304 params["network"] = unicode(net)
1305 if mac is not None:
1306 params["mac"] = mac
1307 if hostname is not None:
1308 params["hostname"] = hostname
1309+=======
1310+ if mac is not None:
1311+ params["mac"] = mac
1312+>>>>>>> MERGE-SOURCE
1313 return self.client.post(reverse('ipaddresses_handler'), params)
1314
1315 def post_release_request(self, ip, mac=None):
1316 params = {
1317 'op': 'release',
1318 'ip': ip,
1319+ 'mac': mac,
1320 }
1321 if mac is not None:
1322 params["mac"] = mac
1323@@ -103,6 +165,7 @@
1324 IPADDRESS_TYPE.USER_RESERVED, staticipaddress.alloc_type)
1325 self.assertEqual(self.logged_in_user, staticipaddress.user)
1326
1327+<<<<<<< TREE
1328 def test_POST_reserve_with_MAC_links_MAC_to_ip_address(self):
1329 update_host_maps = self.patch(dhcp_module, 'update_host_maps')
1330 interface = self.make_interface()
1331@@ -174,6 +237,79 @@
1332 staticipaddress.macaddress_set.first().mac_address,
1333 mac.mac_address)
1334
1335+=======
1336+ def test_POST_reserve_with_MAC_links_MAC_to_ip_address(self):
1337+ update_host_maps = self.patch(ip_addresses_module, 'update_host_maps')
1338+ interface = self.make_interface()
1339+ net = interface.network
1340+ mac = factory.make_mac_address()
1341+
1342+ response = self.post_reservation_request(net, mac=mac)
1343+ self.assertEqual(httplib.OK, response.status_code)
1344+ returned_address = json.loads(response.content)
1345+ [staticipaddress] = StaticIPAddress.objects.all()
1346+ self.expectThat(
1347+ staticipaddress.macaddress_set.first().mac_address,
1348+ Equals(mac))
1349+
1350+ # DHCP Host maps have been updated.
1351+ self.expectThat(
1352+ update_host_maps,
1353+ MockCalledOnceWith(
1354+ {interface.nodegroup: {returned_address['ip']: mac}}))
1355+
1356+ def test_POST_reserve_with_MAC_returns_503_if_hostmap_update_fails(self):
1357+ update_host_maps = self.patch(ip_addresses_module, 'update_host_maps')
1358+ # We a specific exception here because update_host_maps() will
1359+ # fail with RPC-specific errors.
1360+ update_host_maps.return_value = [
1361+ Failure(
1362+ NoConnectionsAvailable(
1363+ "Are you sure you're not Elvis?"))
1364+ ]
1365+ interface = self.make_interface()
1366+ net = interface.network
1367+ mac = factory.make_mac_address()
1368+
1369+ response = self.post_reservation_request(net, mac=mac)
1370+ self.expectThat(
1371+ response.status_code, Equals(httplib.SERVICE_UNAVAILABLE))
1372+ # No static IP has been created.
1373+ self.expectThat(
1374+ StaticIPAddress.objects.all(), HasLength(0))
1375+ # No MAC address has been created, either.
1376+ self.expectThat(
1377+ MACAddress.objects.all(), HasLength(0))
1378+
1379+ def test_POST_returns_CONFLICT_when_static_ip_for_MAC_already_exists(self):
1380+ interface = self.make_interface()
1381+ mac = factory.make_MACAddress(cluster_interface=interface)
1382+ mac.claim_static_ips()
1383+ net = interface.network
1384+
1385+ response = self.post_reservation_request(net, mac=mac.mac_address)
1386+ self.expectThat(
1387+ response.status_code, Equals(httplib.CONFLICT))
1388+ # No new static IP has been created.
1389+ self.expectThat(
1390+ StaticIPAddress.objects.all().count(),
1391+ Equals(1))
1392+
1393+ def test_POST_allows_claiming_of_new_static_ips_for_existing_MAC(self):
1394+ self.patch(ip_addresses_module, 'update_host_maps')
1395+
1396+ interface = self.make_interface()
1397+ net = interface.network
1398+ mac = factory.make_MACAddress(cluster_interface=interface)
1399+
1400+ response = self.post_reservation_request(net, mac=mac.mac_address)
1401+ self.expectThat(response.status_code, Equals(httplib.OK))
1402+ [staticipaddress] = StaticIPAddress.objects.all()
1403+ self.assertEqual(
1404+ staticipaddress.macaddress_set.first().mac_address,
1405+ mac.mac_address)
1406+
1407+>>>>>>> MERGE-SOURCE
1408 def test_POST_reserve_errors_for_no_matching_interface(self):
1409 interface = self.make_interface()
1410 net = factory.make_ipv4_network(but_not=[interface.network])
1411@@ -362,76 +498,143 @@
1412 self.assertEqual(httplib.OK, response.status_code, response.content)
1413 self.assertIsNone(reload_object(ipaddress))
1414
1415- def test_POST_release_deletes_floating_MAC_address(self):
1416- self.patch(dhcp_module, dhcp_module.remove_host_maps.__name__)
1417-
1418- interface = self.make_interface()
1419- floating_mac = factory.make_MACAddress(cluster_interface=interface)
1420- [ipaddress] = floating_mac.claim_static_ips(
1421- alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
1422- update_host_maps=False)
1423-
1424- self.post_release_request(ipaddress.ip)
1425- self.assertIsNone(reload_object(floating_mac))
1426-
1427- def test_POST_release_does_not_delete_MACs_linked_to_nodes(self):
1428- self.patch(dhcp_module, dhcp_module.remove_host_maps.__name__)
1429-
1430- interface = self.make_interface()
1431- node = factory.make_Node(nodegroup=interface.nodegroup)
1432- attached_mac = factory.make_MACAddress(
1433- node=node, cluster_interface=interface)
1434- [ipaddress] = attached_mac.claim_static_ips(
1435- alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
1436- update_host_maps=False)
1437-
1438- self.post_release_request(ipaddress.ip)
1439- self.assertEqual(attached_mac, reload_object(attached_mac))
1440-
1441- def test_POST_release_updates_DNS_and_DHCP(self):
1442- remove_host_maps = self.patch(
1443- dhcp_module, dhcp_module.remove_host_maps.__name__)
1444-
1445- interface = self.make_interface()
1446- floating_mac = factory.make_MACAddress(cluster_interface=interface)
1447- [ipaddress] = floating_mac.claim_static_ips(
1448- alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
1449- update_host_maps=False)
1450-
1451- self.post_release_request(ipaddress.ip)
1452- self.expectThat(
1453- remove_host_maps, MockCalledOnceWith(
1454- {interface.nodegroup: [ipaddress.ip]}))
1455-
1456- def test_POST_release_raises_503_if_removing_host_maps_errors(self):
1457- remove_host_maps = self.patch(
1458- dhcp_module, dhcp_module.remove_host_maps.__name__)
1459- # Failures in remove_host_maps() will be RPC-related exceptions,
1460- # so we use one of those explicitly.
1461- remove_host_maps.return_value = [
1462- Failure(
1463- NoConnectionsAvailable(
1464- "The wizard's staff has a knob on the end."))
1465- ]
1466-
1467- interface = self.make_interface()
1468- floating_mac = factory.make_MACAddress(cluster_interface=interface)
1469- [ipaddress] = floating_mac.claim_static_ips(
1470- alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
1471- update_host_maps=False)
1472-
1473- response = self.post_release_request(ipaddress.ip)
1474- self.expectThat(
1475- response.status_code, Equals(httplib.SERVICE_UNAVAILABLE))
1476-
1477- # The static IP hasn't been deleted.
1478- self.expectThat(
1479- reload_object(ipaddress), Not(Is(None)))
1480-
1481- # Neither has the DHCPHost.
1482- self.expectThat(
1483- reload_object(floating_mac), Not(Is(None)))
1484-
1485+<<<<<<< TREE
1486+ def test_POST_release_deletes_floating_MAC_address(self):
1487+ self.patch(dhcp_module, dhcp_module.remove_host_maps.__name__)
1488+
1489+ interface = self.make_interface()
1490+ floating_mac = factory.make_MACAddress(cluster_interface=interface)
1491+ [ipaddress] = floating_mac.claim_static_ips(
1492+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
1493+ update_host_maps=False)
1494+
1495+ self.post_release_request(ipaddress.ip)
1496+ self.assertIsNone(reload_object(floating_mac))
1497+
1498+ def test_POST_release_does_not_delete_MACs_linked_to_nodes(self):
1499+ self.patch(dhcp_module, dhcp_module.remove_host_maps.__name__)
1500+
1501+ interface = self.make_interface()
1502+ node = factory.make_Node(nodegroup=interface.nodegroup)
1503+ attached_mac = factory.make_MACAddress(
1504+ node=node, cluster_interface=interface)
1505+ [ipaddress] = attached_mac.claim_static_ips(
1506+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
1507+ update_host_maps=False)
1508+
1509+ self.post_release_request(ipaddress.ip)
1510+ self.assertEqual(attached_mac, reload_object(attached_mac))
1511+
1512+ def test_POST_release_updates_DNS_and_DHCP(self):
1513+ remove_host_maps = self.patch(
1514+ dhcp_module, dhcp_module.remove_host_maps.__name__)
1515+
1516+ interface = self.make_interface()
1517+ floating_mac = factory.make_MACAddress(cluster_interface=interface)
1518+ [ipaddress] = floating_mac.claim_static_ips(
1519+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
1520+ update_host_maps=False)
1521+
1522+ self.post_release_request(ipaddress.ip)
1523+ self.expectThat(
1524+ remove_host_maps, MockCalledOnceWith(
1525+ {interface.nodegroup: [ipaddress.ip]}))
1526+
1527+ def test_POST_release_raises_503_if_removing_host_maps_errors(self):
1528+ remove_host_maps = self.patch(
1529+ dhcp_module, dhcp_module.remove_host_maps.__name__)
1530+ # Failures in remove_host_maps() will be RPC-related exceptions,
1531+ # so we use one of those explicitly.
1532+ remove_host_maps.return_value = [
1533+ Failure(
1534+ NoConnectionsAvailable(
1535+ "The wizard's staff has a knob on the end."))
1536+ ]
1537+
1538+ interface = self.make_interface()
1539+ floating_mac = factory.make_MACAddress(cluster_interface=interface)
1540+ [ipaddress] = floating_mac.claim_static_ips(
1541+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user,
1542+ update_host_maps=False)
1543+
1544+ response = self.post_release_request(ipaddress.ip)
1545+ self.expectThat(
1546+ response.status_code, Equals(httplib.SERVICE_UNAVAILABLE))
1547+
1548+ # The static IP hasn't been deleted.
1549+ self.expectThat(
1550+ reload_object(ipaddress), Not(Is(None)))
1551+
1552+ # Neither has the DHCPHost.
1553+ self.expectThat(
1554+ reload_object(floating_mac), Not(Is(None)))
1555+
1556+=======
1557+ def test_POST_release_deletes_floating_MAC_address(self):
1558+ self.patch(ip_addresses_module, 'remove_host_maps')
1559+
1560+ interface = self.make_interface()
1561+ floating_mac = factory.make_MACAddress(cluster_interface=interface)
1562+ [ipaddress] = floating_mac.claim_static_ips(
1563+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)
1564+
1565+ self.post_release_request(ipaddress.ip)
1566+ self.assertIsNone(reload_object(floating_mac))
1567+
1568+ def test_POST_release_does_not_delete_MACs_linked_to_nodes(self):
1569+ self.patch(ip_addresses_module, 'remove_host_maps')
1570+
1571+ interface = self.make_interface()
1572+ node = factory.make_Node(nodegroup=interface.nodegroup)
1573+ attached_mac = factory.make_MACAddress(
1574+ node=node, cluster_interface=interface)
1575+ [ipaddress] = attached_mac.claim_static_ips(
1576+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)
1577+
1578+ self.post_release_request(ipaddress.ip)
1579+ self.assertEqual(attached_mac, reload_object(attached_mac))
1580+
1581+ def test_POST_release_updates_DNS_and_DHCP(self):
1582+ remove_host_maps = self.patch(ip_addresses_module, 'remove_host_maps')
1583+
1584+ interface = self.make_interface()
1585+ floating_mac = factory.make_MACAddress(cluster_interface=interface)
1586+ [ipaddress] = floating_mac.claim_static_ips(
1587+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)
1588+
1589+ self.post_release_request(ipaddress.ip)
1590+ self.expectThat(
1591+ remove_host_maps, MockCalledOnceWith(
1592+ {interface.nodegroup: [ipaddress.ip]}))
1593+
1594+ def test_POST_release_raises_503_if_removing_host_maps_errors(self):
1595+ remove_host_maps = self.patch(ip_addresses_module, 'remove_host_maps')
1596+ # Failures in remove_host_maps() will be RPC-related exceptions,
1597+ # so we use one of those explicitly.
1598+ remove_host_maps.return_value = [
1599+ Failure(
1600+ NoConnectionsAvailable(
1601+ "The wizard's staff has a knob on the end."))
1602+ ]
1603+
1604+ interface = self.make_interface()
1605+ floating_mac = factory.make_MACAddress(cluster_interface=interface)
1606+ [ipaddress] = floating_mac.claim_static_ips(
1607+ alloc_type=IPADDRESS_TYPE.USER_RESERVED, user=self.logged_in_user)
1608+
1609+ response = self.post_release_request(ipaddress.ip)
1610+ self.expectThat(
1611+ response.status_code, Equals(httplib.SERVICE_UNAVAILABLE))
1612+
1613+ # The static IP hasn't been deleted.
1614+ self.expectThat(
1615+ reload_object(ipaddress), Not(Is(None)))
1616+
1617+ # Neither has the DHCPHost.
1618+ self.expectThat(
1619+ reload_object(floating_mac), Not(Is(None)))
1620+
1621+>>>>>>> MERGE-SOURCE
1622 def test_POST_release_does_not_delete_IP_that_I_dont_own(self):
1623 ipaddress = factory.make_StaticIPAddress(user=factory.make_User())
1624 response = self.post_release_request(ipaddress.ip)
1625
1626=== modified file 'src/maasserver/api/tests/test_network.py'
1627=== modified file 'src/maasserver/api/tests/test_node.py'
1628--- src/maasserver/api/tests/test_node.py 2015-06-16 21:10:26 +0000
1629+++ src/maasserver/api/tests/test_node.py 2015-07-08 00:07:16 +0000
1630@@ -24,7 +24,11 @@
1631 from django.core.urlresolvers import reverse
1632 from django.db import transaction
1633 from maasserver import forms
1634+<<<<<<< TREE
1635 from maasserver.dns import config as dns_config
1636+=======
1637+from maasserver.api import nodes as api_nodes
1638+>>>>>>> MERGE-SOURCE
1639 from maasserver.enum import (
1640 IPADDRESS_TYPE,
1641 NODE_BOOT,
1642@@ -203,6 +207,7 @@
1643 parsed_result['zone']['name'],
1644 parsed_result['zone']['description']])
1645
1646+<<<<<<< TREE
1647 def test_GET_returns_boot_type(self):
1648 node = factory.make_Node()
1649 response = self.client.get(self.get_node_uri(node))
1650@@ -235,6 +240,16 @@
1651 parsed_result = json.loads(response.content)
1652 self.assertIsNone(parsed_result['pxe_mac'])
1653
1654+=======
1655+ def test_GET_returns_boot_type(self):
1656+ node = factory.make_Node()
1657+ response = self.client.get(self.get_node_uri(node))
1658+ self.assertEqual(httplib.OK, response.status_code)
1659+ parsed_result = json.loads(response.content)
1660+ self.assertEqual(
1661+ node.boot_type, parsed_result['boot_type'])
1662+
1663+>>>>>>> MERGE-SOURCE
1664 def test_GET_refuses_to_access_nonexistent_node(self):
1665 # When fetching a Node, the api returns a 'Not Found' (404) error
1666 # if no node is found.
1667@@ -1043,6 +1058,7 @@
1668 node = reload_object(node)
1669 self.assertEqual(original_setting, node.disable_ipv4)
1670
1671+<<<<<<< TREE
1672 def test_PUT_updates_boot_type(self):
1673 node = factory.make_Node(
1674 owner=self.logged_in_user,
1675@@ -1112,6 +1128,22 @@
1676 self.assertEqual('Invalid size for swap: 5E',
1677 parsed_result['swap_size'][0])
1678
1679+=======
1680+ def test_PUT_updates_boot_type(self):
1681+ node = factory.make_Node(
1682+ owner=self.logged_in_user,
1683+ architecture=make_usable_architecture(self),
1684+ boot_type=NODE_BOOT.FASTPATH,
1685+ )
1686+ response = self.client_put(
1687+ self.get_node_uri(node), {'boot_type': NODE_BOOT.DEBIAN})
1688+ parsed_result = json.loads(response.content)
1689+ self.assertEqual(httplib.OK, response.status_code, response.content)
1690+ node = reload_object(node)
1691+ self.assertEqual(node.boot_type, parsed_result['boot_type'])
1692+ self.assertEqual(node.boot_type, NODE_BOOT.DEBIAN)
1693+
1694+>>>>>>> MERGE-SOURCE
1695 def test_DELETE_deletes_node(self):
1696 # The api allows to delete a Node.
1697 self.become_admin()
1698@@ -1193,9 +1225,15 @@
1699
1700 def test_claim_sticky_ip_address_returns_existing_if_already_exists(self):
1701 self.become_admin()
1702+<<<<<<< TREE
1703 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
1704 # Silence 'update_host_maps'.
1705 self.patch(Node.update_host_maps)
1706+=======
1707+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
1708+ # Silence 'update_host_maps'.
1709+ self.patch(node_module, "update_host_maps")
1710+>>>>>>> MERGE-SOURCE
1711 [existing_ip] = node.get_primary_mac().claim_static_ips(
1712 alloc_type=IPADDRESS_TYPE.STICKY, update_host_maps=False)
1713 response = self.client.post(
1714@@ -1249,9 +1287,15 @@
1715
1716 def test_claim_sticky_ip_address_claims_sticky_ip_address(self):
1717 self.become_admin()
1718+<<<<<<< TREE
1719 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
1720 # Silence 'update_host_maps'.
1721 self.patch(node_module, "update_host_maps")
1722+=======
1723+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
1724+ # Silence 'update_host_maps'.
1725+ self.patch(node_module, "update_host_maps")
1726+>>>>>>> MERGE-SOURCE
1727 response = self.client.post(
1728 self.get_node_uri(node), {'op': 'claim_sticky_ip_address'})
1729 self.assertEqual(httplib.OK, response.status_code, response.content)
1730@@ -1289,6 +1333,32 @@
1731 self.assertThat(
1732 dns_update_zones, MockCalledOnceWith([node.nodegroup]))
1733
1734+ def test_claim_ip_address_creates_host_DHCP_and_DNS_mappings(self):
1735+ self.become_admin()
1736+ node = factory.make_node_with_mac_attached_to_nodegroupinterface()
1737+ change_dns_zones = self.patch(api_nodes, 'change_dns_zones')
1738+ update_host_maps = self.patch(node_module, "update_host_maps")
1739+ update_host_maps.return_value = [] # No failures.
1740+ response = self.client.post(
1741+ self.get_node_uri(node), {'op': 'claim_sticky_ip_address'})
1742+ self.assertEqual(httplib.OK, response.status_code, response.content)
1743+
1744+ self.assertItemsEqual(
1745+ [node.get_primary_mac()],
1746+ node.mac_addresses_on_managed_interfaces())
1747+ # Host maps are updated.
1748+ self.assertThat(
1749+ update_host_maps, MockCalledOnceWith({
1750+ node.nodegroup: {
1751+ ip_address.ip: mac.mac_address
1752+ for ip_address in mac.ip_addresses.all()
1753+ }
1754+ for mac in node.mac_addresses_on_managed_interfaces()
1755+ }))
1756+ # DNS has been updated.
1757+ self.assertThat(
1758+ change_dns_zones, MockCalledOnceWith(node.nodegroup))
1759+
1760 def test_claim_sticky_ip_address_allows_macaddress_parameter(self):
1761 self.become_admin()
1762 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
1763
1764=== modified file 'src/maasserver/api/tests/test_nodegroup.py'
1765--- src/maasserver/api/tests/test_nodegroup.py 2015-06-16 21:10:26 +0000
1766+++ src/maasserver/api/tests/test_nodegroup.py 2015-07-08 00:07:16 +0000
1767@@ -57,10 +57,15 @@
1768 )
1769 from mock import Mock
1770 from provisioningserver.rpc.cluster import (
1771+<<<<<<< TREE
1772 AddSeaMicro15k,
1773 AddVirsh,
1774 AddVMware,
1775 EnlistNodesFromMicrosoftOCS,
1776+=======
1777+ AddSeaMicro15k,
1778+ AddVirsh,
1779+>>>>>>> MERGE-SOURCE
1780 EnlistNodesFromMSCM,
1781 EnlistNodesFromUCSM,
1782 ImportBootImages,
1783@@ -332,6 +337,7 @@
1784 httplib.BAD_REQUEST, response.status_code,
1785 explain_unexpected_response(httplib.BAD_REQUEST, response))
1786
1787+<<<<<<< TREE
1788 def test_probe_and_enlist_hardware_adds_seamicro(self):
1789 self.become_admin()
1790 user = self.logged_in_user
1791@@ -492,6 +498,75 @@
1792 ('unified', {'endpoint': 'probe_and_enlist_hardware'}),
1793 ]
1794
1795+=======
1796+ def test_probe_and_enlist_hardware_adds_seamicro(self):
1797+ nodegroup = factory.make_NodeGroup()
1798+ model = 'seamicro15k'
1799+ mac = factory.make_mac_address()
1800+ username = factory.make_name('user')
1801+ password = factory.make_name('password')
1802+ power_control = random.choice(
1803+ ['ipmi', 'restapi', 'restapi2'])
1804+ self.become_admin()
1805+
1806+ getClientFor = self.patch(nodegroup_module, 'getClientFor')
1807+ client = getClientFor.return_value
1808+ nodegroup = factory.make_NodeGroup()
1809+
1810+ response = self.client.post(
1811+ reverse('nodegroup_handler', args=[nodegroup.uuid]),
1812+ {
1813+ 'op': 'probe_and_enlist_hardware',
1814+ 'model': model,
1815+ 'mac': mac,
1816+ 'username': username,
1817+ 'password': password,
1818+ 'power_control': power_control,
1819+ })
1820+
1821+ self.assertEqual(
1822+ httplib.OK, response.status_code,
1823+ explain_unexpected_response(httplib.OK, response))
1824+
1825+ self.expectThat(
1826+ client,
1827+ MockCalledOnceWith(
1828+ AddSeaMicro15k, mac=mac, username=username,
1829+ password=password, power_control=power_control))
1830+
1831+ def test_probe_and_enlist_hardware_adds_virsh(self):
1832+ nodegroup = factory.make_NodeGroup()
1833+ model = 'virsh'
1834+ poweraddr = factory.make_ipv4_address()
1835+ password = factory.make_name('password')
1836+ prefix_filter = factory.make_name('filter')
1837+ self.become_admin()
1838+
1839+ getClientFor = self.patch(nodegroup_module, 'getClientFor')
1840+ client = getClientFor.return_value
1841+ nodegroup = factory.make_NodeGroup()
1842+
1843+ response = self.client.post(
1844+ reverse('nodegroup_handler', args=[nodegroup.uuid]),
1845+ {
1846+ 'op': 'probe_and_enlist_hardware',
1847+ 'model': model,
1848+ 'power_address': poweraddr,
1849+ 'power_pass': password,
1850+ 'prefix_filter': prefix_filter,
1851+ })
1852+
1853+ self.assertEqual(
1854+ httplib.OK, response.status_code,
1855+ explain_unexpected_response(httplib.OK, response))
1856+
1857+ self.expectThat(
1858+ client,
1859+ MockCalledOnceWith(
1860+ AddVirsh, poweraddr=poweraddr,
1861+ password=password, prefix_filter=prefix_filter))
1862+
1863+>>>>>>> MERGE-SOURCE
1864 def test_probe_and_enlist_ucsm_adds_ucsm(self):
1865 self.become_admin()
1866 user = self.logged_in_user
1867
1868=== modified file 'src/maasserver/api/tests/test_nodes.py'
1869=== modified file 'src/maasserver/api/tests/test_pxeconfig.py'
1870--- src/maasserver/api/tests/test_pxeconfig.py 2015-06-10 10:08:45 +0000
1871+++ src/maasserver/api/tests/test_pxeconfig.py 2015-07-08 00:07:16 +0000
1872@@ -40,8 +40,13 @@
1873 Config,
1874 Event,
1875 MACAddress,
1876+<<<<<<< TREE
1877 Node,
1878 )
1879+=======
1880+ Node,
1881+ )
1882+>>>>>>> MERGE-SOURCE
1883 from maasserver.preseed import (
1884 compose_enlistment_preseed_url,
1885 compose_preseed_url,
1886@@ -431,8 +436,14 @@
1887 pxe_config = self.get_pxeconfig(params)
1888 self.assertEqual(None, pxe_config['extra_opts'])
1889
1890+<<<<<<< TREE
1891 def test_pxeconfig_returns_commissioning_for_insane_state(self):
1892 mac = factory.make_MACAddress_with_Node()
1893+=======
1894+ def test_pxeconfig_returns_commissioning_for_insane_state(self):
1895+ node = factory.make_Node()
1896+ mac = factory.make_MACAddress(node=node)
1897+>>>>>>> MERGE-SOURCE
1898 params = self.get_default_params()
1899 params['mac'] = mac.mac_address
1900 pxe_config = self.get_pxeconfig(params)
1901@@ -443,8 +454,14 @@
1902 # the machine down.
1903 self.assertEqual('commissioning', pxe_config['purpose'])
1904
1905+<<<<<<< TREE
1906 def test_pxeconfig_returns_commissioning_for_ready_node(self):
1907 mac = factory.make_MACAddress_with_Node()
1908+=======
1909+ def test_pxeconfig_returns_commissioning_for_ready_node(self):
1910+ node = factory.make_Node()
1911+ mac = factory.make_MACAddress(node=node)
1912+>>>>>>> MERGE-SOURCE
1913 mac.node.status = NODE_STATUS.READY
1914 mac.node.save()
1915 params = self.get_default_params()
1916
1917=== modified file 'src/maasserver/api/tests/test_support.py'
1918--- src/maasserver/api/tests/test_support.py 2015-04-21 22:51:42 +0000
1919+++ src/maasserver/api/tests/test_support.py 2015-07-08 00:07:16 +0000
1920@@ -20,11 +20,16 @@
1921
1922 from django.core.exceptions import PermissionDenied
1923 from django.core.urlresolvers import reverse
1924+<<<<<<< TREE
1925 from maasserver.api.doc import get_api_description_hash
1926 from maasserver.api.support import (
1927 admin_method,
1928 OperationsHandlerMixin,
1929 )
1930+=======
1931+from maasserver.api.doc import get_api_description_hash
1932+from maasserver.api.support import admin_method
1933+>>>>>>> MERGE-SOURCE
1934 from maasserver.models.config import (
1935 Config,
1936 ConfigManager,
1937@@ -35,9 +40,14 @@
1938 from mock import (
1939 call,
1940 Mock,
1941+<<<<<<< TREE
1942 sentinel,
1943 )
1944 from testtools.matchers import Equals
1945+=======
1946+ )
1947+from testtools.matchers import Equals
1948+>>>>>>> MERGE-SOURCE
1949
1950
1951 class TestOperationsResource(APITestCase):
1952
1953=== modified file 'src/maasserver/api/tests/test_version.py'
1954--- src/maasserver/api/tests/test_version.py 2015-06-24 14:20:57 +0000
1955+++ src/maasserver/api/tests/test_version.py 2015-07-08 00:07:16 +0000
1956@@ -32,11 +32,19 @@
1957 '/api/1.0/version/', reverse('version_handler'))
1958
1959 def test_GET_returns_details(self):
1960+<<<<<<< TREE
1961 mock_apt = self.patch(version_module, "get_version_from_apt")
1962 mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
1963 self.patch(version_module, "_cache", {})
1964
1965 response = self.client.get(reverse('version_handler'))
1966+=======
1967+ mock_apt = self.patch(version_module, "get_version_from_apt")
1968+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
1969+ self.patch(version_module, "_cache", {})
1970+
1971+ response = self.client.get(reverse('version'))
1972+>>>>>>> MERGE-SOURCE
1973 self.assertEqual(httplib.OK, response.status_code)
1974
1975 parsed_result = json.loads(response.content)
1976
1977=== modified file 'src/maasserver/api/users.py'
1978=== modified file 'src/maasserver/api/version.py'
1979--- src/maasserver/api/version.py 2015-06-24 17:35:42 +0000
1980+++ src/maasserver/api/version.py 2015-07-08 00:07:16 +0000
1981@@ -40,6 +40,7 @@
1982 """Information about this MAAS instance.
1983
1984 This returns a JSON dictionary with information about this
1985+<<<<<<< TREE
1986 MAAS instance::
1987
1988 {
1989@@ -47,13 +48,25 @@
1990 'subversion': 'alpha10+bzr3750',
1991 'capabilities': ['capability1', 'capability2', ...]
1992 }
1993+=======
1994+ MAAS instance.
1995+ {
1996+ 'version': '1.8.0',
1997+ 'subversion': 'alpha10+bzr3750',
1998+ 'capabilities': ['capability1', 'capability2', ...]
1999+ }
2000+>>>>>>> MERGE-SOURCE
2001 """
2002 api_doc_section_name = "MAAS version"
2003 create = update = delete = None
2004
2005 def read(self, request):
2006+<<<<<<< TREE
2007 """Version and capabilities of this MAAS instance."""
2008 version, subversion = get_maas_version_subversion()
2009+=======
2010+ version, subversion = get_maas_version_subversion()
2011+>>>>>>> MERGE-SOURCE
2012 version_info = {
2013 'capabilities': API_CAPABILITIES_LIST,
2014 'version': version,
2015
2016=== modified file 'src/maasserver/api/zones.py'
2017=== modified file 'src/maasserver/clusterrpc/tests/test_boot_images.py'
2018--- src/maasserver/clusterrpc/tests/test_boot_images.py 2015-06-25 11:55:17 +0000
2019+++ src/maasserver/clusterrpc/tests/test_boot_images.py 2015-07-08 00:07:16 +0000
2020@@ -151,7 +151,15 @@
2021
2022 def setUp(self):
2023 super(TestGetBootImages, self).setUp()
2024+<<<<<<< TREE
2025 prepare_tftp_root(self) # Sets self.tftp_root.
2026+=======
2027+ resource_dir = self.make_dir()
2028+ self.tftproot = os.path.join(resource_dir, 'current')
2029+ os.mkdir(self.tftproot)
2030+ self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
2031+ self.patch(boot_images, 'BOOT_RESOURCES_STORAGE', resource_dir)
2032+>>>>>>> MERGE-SOURCE
2033
2034 def test_returns_boot_images(self):
2035 nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ENABLED)
2036@@ -176,7 +184,15 @@
2037
2038 def setUp(self):
2039 super(TestGetAvailableBootImages, self).setUp()
2040+<<<<<<< TREE
2041 prepare_tftp_root(self) # Sets self.tftp_root.
2042+=======
2043+ resource_dir = self.make_dir()
2044+ self.tftproot = os.path.join(resource_dir, 'current')
2045+ os.mkdir(self.tftproot)
2046+ self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
2047+ self.patch(boot_images, 'BOOT_RESOURCES_STORAGE', resource_dir)
2048+>>>>>>> MERGE-SOURCE
2049
2050 def test_returns_boot_images_for_one_cluster(self):
2051 factory.make_NodeGroup().accept()
2052@@ -262,7 +278,15 @@
2053
2054 def setUp(self):
2055 super(TestGetBootImagesFor, self).setUp()
2056+<<<<<<< TREE
2057 prepare_tftp_root(self) # Sets self.tftp_root.
2058+=======
2059+ resource_dir = self.make_dir()
2060+ self.tftproot = os.path.join(resource_dir, 'current')
2061+ os.mkdir(self.tftproot)
2062+ self.patch(boot_images, 'CACHED_BOOT_IMAGES', None)
2063+ self.patch(boot_images, 'BOOT_RESOURCES_STORAGE', resource_dir)
2064+>>>>>>> MERGE-SOURCE
2065
2066 def make_boot_images(self):
2067 purposes = ['install', 'commissioning', 'xinstall']
2068
2069=== modified file 'src/maasserver/dhcp.py'
2070=== modified file 'src/maasserver/dns/config.py'
2071--- src/maasserver/dns/config.py 2015-07-01 18:58:34 +0000
2072+++ src/maasserver/dns/config.py 2015-07-08 00:07:16 +0000
2073@@ -189,6 +189,7 @@
2074
2075 clusters = NodeGroup.objects.all()
2076 zones = ZoneGenerator(
2077+<<<<<<< TREE
2078 clusters, serial_generator=next_zone_serial).as_list()
2079 bind_write_zones(zones)
2080
2081@@ -365,6 +366,17 @@
2082 :return: "on", "off", or "auto"
2083 """
2084 return Config.objects.get_config("dnssec_validation")
2085+=======
2086+ NodeGroup.objects.all(), serial_generator=next_zone_serial
2087+ ).as_list()
2088+ upstream_dns = get_upstream_dns()
2089+ tasks.write_full_dns_config.delay(
2090+ zones=zones,
2091+ callback=tasks.rndc_command.subtask(
2092+ args=[['reload'], reload_retry]),
2093+ upstream_dns=upstream_dns,
2094+ trusted_networks=get_trusted_networks())
2095+>>>>>>> MERGE-SOURCE
2096
2097
2098 def get_trusted_networks():
2099@@ -372,7 +384,23 @@
2100
2101 :return: A list of CIDR-format network specifications.
2102 """
2103+<<<<<<< TREE
2104 return [
2105 unicode(net.get_network().cidr)
2106 for net in Network.objects.all()
2107 ]
2108+=======
2109+ networks = " ".join(
2110+ "%s;" % net.get_network().cidr
2111+ for net in Network.objects.all())
2112+ return networks
2113+
2114+
2115+def get_upstream_dns():
2116+ """Return the IP addresses of configured upstream DNS servers.
2117+
2118+ :return: A list of IP addresses.
2119+ """
2120+ upstream_dns = Config.objects.get_config("upstream_dns")
2121+ return [] if upstream_dns is None else upstream_dns.split()
2122+>>>>>>> MERGE-SOURCE
2123
2124=== modified file 'src/maasserver/dns/tests/test_config.py'
2125--- src/maasserver/dns/tests/test_config.py 2015-07-01 19:02:36 +0000
2126+++ src/maasserver/dns/tests/test_config.py 2015-07-08 00:07:16 +0000
2127@@ -504,6 +504,7 @@
2128 def test_dns_update_all_zones_now_passes_upstream_dns_parameter(self):
2129 self.patch(settings, 'DNS_CONNECT', True)
2130 self.create_managed_nodegroup()
2131+<<<<<<< TREE
2132 random_ip = factory.make_ipv4_address()
2133 Config.objects.set_config("upstream_dns", random_ip)
2134 bind_write_options = self.patch_autospec(
2135@@ -513,6 +514,16 @@
2136 bind_write_options,
2137 MockCalledOnceWith(
2138 dnssec_validation='auto', upstream_dns=[random_ip]))
2139+=======
2140+ ips = [factory.make_ipv4_address() for _ in range(3)]
2141+ input_ips = " ".join(ips)
2142+ Config.objects.set_config("upstream_dns", input_ips)
2143+ patched_task = self.patch(dns_tasks.write_full_dns_config, "delay")
2144+ write_full_dns_config()
2145+ self.assertThat(patched_task, MockCalledOnceWith(
2146+ zones=ANY, callback=ANY, trusted_networks=ANY,
2147+ upstream_dns=ips))
2148+>>>>>>> MERGE-SOURCE
2149
2150 def test_dns_update_all_zones_now_writes_trusted_networks_parameter(self):
2151 self.patch(settings, 'DNS_CONNECT', True)
2152
2153=== modified file 'src/maasserver/exceptions.py'
2154--- src/maasserver/exceptions.py 2015-05-07 18:14:38 +0000
2155+++ src/maasserver/exceptions.py 2015-07-08 00:07:16 +0000
2156@@ -162,6 +162,7 @@
2157 api_error = httplib.CONFLICT
2158
2159
2160+<<<<<<< TREE
2161 class StaticIPAlreadyExistsForMACAddress(MAASAPIException):
2162 """Raised when trying to allocate a static IP for a non-node MAC
2163 where a node with that MAC already exists."""
2164@@ -180,6 +181,14 @@
2165 api_error = httplib.CONFLICT
2166
2167
2168+=======
2169+class StaticIPAlreadyExistsForMACAddress(MAASAPIException):
2170+ """Raised when trying to allocate a static IP for a non-node MAC
2171+ where a node with that MAC already exists."""
2172+ api_error = httplib.CONFLICT
2173+
2174+
2175+>>>>>>> MERGE-SOURCE
2176 class NodeActionError(MAASException):
2177 """Raised when there is an error performing a NodeAction."""
2178
2179
2180=== modified file 'src/maasserver/fields.py'
2181--- src/maasserver/fields.py 2015-07-06 08:52:20 +0000
2182+++ src/maasserver/fields.py 2015-07-08 00:07:16 +0000
2183@@ -539,6 +539,7 @@
2184 raise AssertionError(
2185 "Invalid LargeObjectField value (expected integer): '%s'"
2186 % repr(value))
2187+<<<<<<< TREE
2188
2189
2190 def parse_cidr(value):
2191@@ -600,3 +601,28 @@
2192 "Invalid IP address: %s; provide a list of "
2193 "space-separated IP addresses" % ip)
2194 return ' '.join(ips)
2195+=======
2196+
2197+
2198+class IPListFormField(CharField):
2199+ """Accepts a space/comma separated list of IP addresses.
2200+
2201+ This field normalizes the list to a space-separated list.
2202+ """
2203+ separators = re.compile('[,\s]+')
2204+
2205+ def clean(self, value):
2206+ if value is None:
2207+ return None
2208+ else:
2209+ ips = re.split(self.separators, value)
2210+ ips = [ip.strip() for ip in ips if ip != '']
2211+ for ip in ips:
2212+ try:
2213+ GenericIPAddressField().clean(ip, model_instance=None)
2214+ except ValidationError:
2215+ raise ValidationError(
2216+ "Invalid IP address: %s; provide a list of "
2217+ "space-separated IP addresses" % ip)
2218+ return ' '.join(ips)
2219+>>>>>>> MERGE-SOURCE
2220
2221=== modified file 'src/maasserver/forms.py'
2222--- src/maasserver/forms.py 2015-07-03 17:04:29 +0000
2223+++ src/maasserver/forms.py 2015-07-08 00:07:16 +0000
2224@@ -92,9 +92,14 @@
2225 from maasserver.enum import (
2226 BOOT_RESOURCE_FILE_TYPE,
2227 BOOT_RESOURCE_TYPE,
2228+<<<<<<< TREE
2229 FILESYSTEM_TYPE_CHOICES,
2230 NODE_BOOT,
2231 NODE_BOOT_CHOICES,
2232+=======
2233+ NODE_BOOT,
2234+ NODE_BOOT_CHOICES,
2235+>>>>>>> MERGE-SOURCE
2236 NODE_STATUS,
2237 NODEGROUPINTERFACE_MANAGEMENT,
2238 NODEGROUPINTERFACE_MANAGEMENT_CHOICES,
2239@@ -533,6 +538,7 @@
2240 self.cleaned_data['disable_ipv4'] = False
2241 return self.cleaned_data['disable_ipv4']
2242
2243+<<<<<<< TREE
2244 def clean_swap_size(self):
2245 """Validates the swap size field and parses integers suffixed with K,
2246 M, G and T
2247@@ -560,6 +566,15 @@
2248 else:
2249 return boot_type
2250
2251+=======
2252+ def clean_boot_type(self):
2253+ boot_type = self.cleaned_data.get('boot_type')
2254+ if not boot_type:
2255+ return NODE_BOOT.FASTPATH
2256+ else:
2257+ return boot_type
2258+
2259+>>>>>>> MERGE-SOURCE
2260 def clean(self):
2261 cleaned_data = super(NodeForm, self).clean()
2262 if self.new_node and self.data.get('disable_ipv4') is None:
2263@@ -659,6 +674,7 @@
2264 "does not manage DNS, then the host name as entered will be the "
2265 "FQDN."))
2266
2267+<<<<<<< TREE
2268 swap_size = forms.CharField(
2269 label="Swap size", required=False, help_text=(
2270 "The size of the swap file in bytes. The field also accepts K, M, "
2271@@ -667,6 +683,11 @@
2272 boot_type = forms.ChoiceField(
2273 choices=NODE_BOOT_CHOICES, initial=NODE_BOOT.FASTPATH, required=False)
2274
2275+=======
2276+ boot_type = forms.ChoiceField(
2277+ choices=NODE_BOOT_CHOICES, initial=NODE_BOOT.FASTPATH, required=False)
2278+
2279+>>>>>>> MERGE-SOURCE
2280 class Meta:
2281 model = Node
2282
2283@@ -681,8 +702,12 @@
2284 'distro_series',
2285 'license_key',
2286 'disable_ipv4',
2287+<<<<<<< TREE
2288 'swap_size',
2289 'boot_type',
2290+=======
2291+ 'boot_type',
2292+>>>>>>> MERGE-SOURCE
2293 )
2294
2295
2296@@ -750,7 +775,13 @@
2297 cpu_count = forms.IntegerField(
2298 required=False, initial=0, label="CPU Count")
2299 memory = forms.IntegerField(
2300- required=False, initial=0, label="Memory (MiB)")
2301+<<<<<<< TREE
2302+ required=False, initial=0, label="Memory (MiB)")
2303+=======
2304+ required=False, initial=0, label="Memory (MiB)")
2305+ storage = forms.IntegerField(
2306+ required=False, initial=0, label="Disk space (MB)")
2307+>>>>>>> MERGE-SOURCE
2308
2309 class Meta:
2310 model = Node
2311
2312=== modified file 'src/maasserver/forms_settings.py'
2313--- src/maasserver/forms_settings.py 2015-07-06 08:52:20 +0000
2314+++ src/maasserver/forms_settings.py 2015-07-08 00:07:16 +0000
2315@@ -25,8 +25,12 @@
2316
2317 from django import forms
2318 from django.core.exceptions import ValidationError
2319+<<<<<<< TREE
2320 from maasserver.bootresources import IMPORT_RESOURCES_SERVICE_PERIOD
2321 from maasserver.fields import IPListFormField
2322+=======
2323+from maasserver.fields import IPListFormField
2324+>>>>>>> MERGE-SOURCE
2325 from maasserver.models.config import (
2326 Config,
2327 DEFAULT_OS,
2328@@ -275,6 +279,7 @@
2329 "Erase nodes' disks prior to releasing.")
2330 }
2331 },
2332+<<<<<<< TREE
2333 'boot_images_auto_import': {
2334 'default': True,
2335 'form': forms.BooleanField,
2336@@ -286,6 +291,18 @@
2337 (IMPORT_RESOURCES_SERVICE_PERIOD.total_seconds() / 60.0))
2338 }
2339 },
2340+=======
2341+ 'enable_dhcp_discovery_on_unconfigured_interfaces': {
2342+ 'default': False,
2343+ 'form': forms.BooleanField,
2344+ 'form_kwargs': {
2345+ 'required': False,
2346+ 'label': (
2347+ "Perform DHCP discovery on unconfigured network "
2348+ "interfaces of commissioning nodes."),
2349+ }
2350+ },
2351+>>>>>>> MERGE-SOURCE
2352 }
2353
2354
2355
2356=== modified file 'src/maasserver/management/commands/edit_named_options.py'
2357--- src/maasserver/management/commands/edit_named_options.py 2015-07-07 20:11:36 +0000
2358+++ src/maasserver/management/commands/edit_named_options.py 2015-07-08 00:07:16 +0000
2359@@ -31,6 +31,7 @@
2360 from django.core.management.base import (
2361 BaseCommand,
2362 CommandError,
2363+<<<<<<< TREE
2364 )
2365 from maasserver.models import Config
2366 from maasserver.utils.isc import (
2367@@ -38,6 +39,13 @@
2368 make_isc_string,
2369 parse_isc_string,
2370 )
2371+=======
2372+ )
2373+from maasserver.utils.isc import (
2374+ make_isc_string,
2375+ parse_isc_string,
2376+ )
2377+>>>>>>> MERGE-SOURCE
2378 from provisioningserver.dns.config import MAAS_NAMED_CONF_OPTIONS_INSIDE_NAME
2379
2380
2381@@ -90,8 +98,14 @@
2382 Then insert the include statement that we need.
2383 """
2384 try:
2385+<<<<<<< TREE
2386 config_dict = parse_isc_string(options_file)
2387 except ISCParseException as e:
2388+=======
2389+ config_dict = parse_isc_string(options_file)
2390+ except Exception as e:
2391+ # Yes, it throws bare exceptions :(
2392+>>>>>>> MERGE-SOURCE
2393 raise CommandError("Failed to parse %s: %s" % (
2394 config_path, e.message))
2395 options_block = config_dict.get("options", None)
2396@@ -217,6 +231,7 @@
2397
2398 # Modify the configuration (if necessary).
2399 self.set_up_include_statement(options_block, config_path)
2400+<<<<<<< TREE
2401
2402 if migrate_conflicting_options:
2403 self.migrate_forwarders(options_block, dry_run, stdout)
2404@@ -236,3 +251,12 @@
2405 with open(config_path, "wb") as fd:
2406 self.write_new_named_conf_options(
2407 fd, backup_filename, new_content)
2408+=======
2409+ self.remove_forwarders(options_block)
2410+ new_content = make_isc_string(config_dict)
2411+
2412+ # Back up and write new file.
2413+ self.back_up_existing_file(config_path)
2414+ with open(config_path, "wb") as fd:
2415+ fd.write(new_content)
2416+>>>>>>> MERGE-SOURCE
2417
2418=== modified file 'src/maasserver/management/commands/runserver.py'
2419=== modified file 'src/maasserver/migrations/0099_convert_cluster_interfaces_to_networks.py'
2420=== modified file 'src/maasserver/models/config.py'
2421--- src/maasserver/models/config.py 2015-06-04 14:31:33 +0000
2422+++ src/maasserver/models/config.py 2015-07-08 00:07:16 +0000
2423@@ -67,7 +67,12 @@
2424 # Third Party
2425 'enable_third_party_drivers': True,
2426 'enable_disk_erasing_on_release': False,
2427+<<<<<<< TREE
2428 # # /settings
2429+=======
2430+ 'enable_dhcp_discovery_on_unconfigured_interfaces': True,
2431+ ## /settings
2432+>>>>>>> MERGE-SOURCE
2433 }
2434
2435
2436
2437=== modified file 'src/maasserver/models/macaddress.py'
2438--- src/maasserver/models/macaddress.py 2015-06-15 08:11:07 +0000
2439+++ src/maasserver/models/macaddress.py 2015-07-08 00:07:16 +0000
2440@@ -24,8 +24,13 @@
2441 from django.db.models import (
2442 ForeignKey,
2443 ManyToManyField,
2444+<<<<<<< TREE
2445 SET_NULL,
2446 )
2447+=======
2448+ SET_NULL,
2449+ )
2450+>>>>>>> MERGE-SOURCE
2451 from maasserver import DefaultMeta
2452 from maasserver.enum import IPADDRESS_TYPE
2453 from maasserver.exceptions import (
2454@@ -74,6 +79,7 @@
2455
2456 def update_mac_cluster_interfaces(ip, mac, cluster):
2457 """Calculate and store which interface a MAC is attached to."""
2458+<<<<<<< TREE
2459 # Create a `leases` dict with only one lease: this is so we can re-use
2460 # update_macs_cluster_interfaces() which is designed to deal with multiple
2461 # leases.
2462@@ -83,6 +89,14 @@
2463
2464 def update_macs_cluster_interfaces(leases, cluster):
2465 """Calculate and store which interface a set of MACs are attached to."""
2466+=======
2467+ try:
2468+ mac_address = MACAddress.objects.get(mac_address=mac)
2469+ except MACAddress.DoesNotExist:
2470+ # Silently ignore MAC addresses that we don't know about.
2471+ return
2472+
2473+>>>>>>> MERGE-SOURCE
2474 interface_ranges = {}
2475 # Only consider configured interfaces.
2476 interfaces = (
2477@@ -101,6 +115,7 @@
2478 else:
2479 static_range = []
2480 interface_ranges[interface] = (ip_range, static_range)
2481+<<<<<<< TREE
2482
2483 for ip, mac in leases.viewitems():
2484 # Look through the interface ranges to see if any match the passed
2485@@ -136,6 +151,32 @@
2486 pass
2487 else:
2488 network.macaddress_set.add(mac_address)
2489+=======
2490+
2491+ # Look through the interface ranges to see if any match the passed
2492+ # IP address.
2493+ for interface, (ip_range, static_range) in interface_ranges.items():
2494+ ipaddress = IPAddress(ip)
2495+ # Set the cluster interface only if it's new/changed.
2496+ # This is only an optimisation to prevent repeated logging.
2497+ changed = mac_address.cluster_interface != interface
2498+ in_range = ipaddress in ip_range or ipaddress in static_range
2499+ if in_range and changed:
2500+ mac_address.cluster_interface = interface
2501+ mac_address.save()
2502+ maaslog.info(
2503+ "%s %s linked to cluster interface %s",
2504+ mac_address.node.hostname, mac_address, interface.name)
2505+
2506+ # Locate the Network to which this MAC belongs and link it.
2507+ ipnetwork = interface.network
2508+ if ipnetwork is not None:
2509+ try:
2510+ network = Network.objects.get(ip=ipnetwork.ip.format())
2511+ network.macaddress_set.add(mac_address)
2512+ except Network.DoesNotExist:
2513+ pass
2514+>>>>>>> MERGE-SOURCE
2515
2516
2517 class MACAddress(CleanSave, TimestampedModel):
2518@@ -242,13 +283,19 @@
2519 cluster_interface.network,
2520 cluster_interface.static_ip_range_low,
2521 cluster_interface.static_ip_range_high,
2522+<<<<<<< TREE
2523 cluster_interface.ip_range_low,
2524 cluster_interface.ip_range_high,
2525 alloc_type, requested_address=requested_address,
2526 user=user)
2527+=======
2528+ alloc_type, requested_address=requested_address,
2529+ user=user)
2530+>>>>>>> MERGE-SOURCE
2531 MACStaticIPAddressLink(mac_address=self, ip_address=new_sip).save()
2532 return new_sip
2533
2534+<<<<<<< TREE
2535 def get_cluster_interface(self):
2536 """Return the cluster interface for this MAC.
2537
2538@@ -291,6 +338,10 @@
2539 def claim_static_ips(
2540 self, alloc_type=IPADDRESS_TYPE.AUTO, requested_address=None,
2541 fabric=None, user=None, update_host_maps=True):
2542+=======
2543+ def claim_static_ips(self, alloc_type=IPADDRESS_TYPE.AUTO,
2544+ requested_address=None, user=None):
2545+>>>>>>> MERGE-SOURCE
2546 """Assign static IP addresses to this MAC.
2547
2548 Allocates one address per managed cluster interface connected to this
2549@@ -306,10 +357,15 @@
2550 the range defined on some cluster interface to which this
2551 MACAddress is related. If given, no allocations will be made on
2552 any other cluster interfaces the MAC may be connected to.
2553+<<<<<<< TREE
2554 :param user: Optional User who will be given ownership of any
2555 `StaticIPAddress`es claimed.
2556 :param update_host_maps: If True, will update any relevant DHCP
2557 mappings in addition to allocating the address.
2558+=======
2559+ :param user: Optional User who will be given ownership of any
2560+ `StaticIPAddress`es claimed.
2561+>>>>>>> MERGE-SOURCE
2562 :return: A list of :class:`StaticIPAddress`. Returns empty if
2563 the cluster_interface is not yet known, or the
2564 static_ip_range_low/high values values are not set on the
2565@@ -337,10 +393,22 @@
2566 # different representations for "none" values in IP addresses.
2567 if self.get_cluster_interface() is None:
2568 # No known cluster interface. Nothing we can do.
2569+<<<<<<< TREE
2570 hostname_string = self._get_hostname_log_prefix()
2571+=======
2572+ if self.node is not None:
2573+ hostname_string = "%s: " % self.node.hostname
2574+ else:
2575+ hostname_string = ""
2576+>>>>>>> MERGE-SOURCE
2577 maaslog.error(
2578+<<<<<<< TREE
2579 "%sTried to allocate an IP to MAC %s, but its cluster "
2580 "interface is not known", hostname_string, self)
2581+=======
2582+ "%sTried to allocate an IP to MAC %s but its cluster "
2583+ "interface is not known", hostname_string, self)
2584+>>>>>>> MERGE-SOURCE
2585 return []
2586 cluster_interfaces = self._get_attached_clusters_with_static_ranges()
2587 if len(cluster_interfaces) == 0:
2588@@ -378,6 +446,7 @@
2589 # MAC does not have any address allocated yet.
2590 for interface in cluster_interfaces:
2591 if allocations[interface] is None:
2592+<<<<<<< TREE
2593 # No IP address yet on this cluster interface. Get one.
2594 static_ip = self._allocate_static_address(
2595 interface, alloc_type, requested_address, user=user)
2596@@ -385,6 +454,11 @@
2597 mac_address = MAC(self.mac_address)
2598 new_allocations.append(
2599 (static_ip.ip, mac_address.get_raw()))
2600+=======
2601+ # No IP address yet on this cluster interface. Get one.
2602+ allocations[interface] = self._allocate_static_address(
2603+ interface, alloc_type, requested_address, user=user)
2604+>>>>>>> MERGE-SOURCE
2605
2606 # Note: the previous behavior of the product (MAAS < 1.8) was to
2607 # update host maps with *every* address, not just changed addresses.
2608
2609=== modified file 'src/maasserver/models/network.py'
2610=== modified file 'src/maasserver/models/node.py'
2611--- src/maasserver/models/node.py 2015-07-07 12:39:20 +0000
2612+++ src/maasserver/models/node.py 2015-07-08 00:07:16 +0000
2613@@ -112,6 +112,7 @@
2614 from maasserver.utils import (
2615 get_db_state,
2616 strip_domain,
2617+<<<<<<< TREE
2618 )
2619 from maasserver.utils.dns import validate_hostname
2620 from maasserver.utils.mac import get_vendor_for_mac
2621@@ -121,6 +122,11 @@
2622 post_commit_do,
2623 transactional,
2624 )
2625+=======
2626+ )
2627+from maasserver.utils.mac import get_vendor_for_mac
2628+from maasserver.utils.orm import get_one
2629+>>>>>>> MERGE-SOURCE
2630 from metadataserver.enum import RESULT_TYPE
2631 from netaddr import IPAddress
2632 from piston.models import Token
2633@@ -520,6 +526,7 @@
2634 # IP reservation for when starting a node.
2635 pxe_mac = ForeignKey(
2636 MACAddress, default=None, blank=True, null=True, editable=False,
2637+<<<<<<< TREE
2638 related_name='+', on_delete=SET_NULL)
2639
2640 # Note that the ordering of the managers is meaningul. More precisely, the
2641@@ -534,6 +541,11 @@
2642
2643 # 'devices' are all the non-installable nodes.
2644 devices = DeviceManager()
2645+=======
2646+ related_name='+', on_delete=SET_NULL)
2647+
2648+ objects = NodeManager()
2649+>>>>>>> MERGE-SOURCE
2650
2651 def __unicode__(self):
2652 if self.hostname:
2653@@ -940,7 +952,12 @@
2654 from metadataserver.models import NodeResult
2655
2656 commissioning_user_data = generate_user_data(node=self)
2657+<<<<<<< TREE
2658 # Clear any existing commissioning results.
2659+=======
2660+ # Avoid circular imports
2661+ from metadataserver.models import NodeResult
2662+>>>>>>> MERGE-SOURCE
2663 NodeResult.objects.clear_results(self)
2664 # We need to mark the node as COMMISSIONING now to avoid a race
2665 # when starting multiple nodes. We hang on to old_status just in
2666@@ -1632,6 +1649,7 @@
2667 """Mark allocated or reserved node as available again and power off.
2668 """
2669 maaslog.info("%s: Releasing node", self.hostname)
2670+<<<<<<< TREE
2671
2672 # Don't perform stop the node if its already off. Doing so will
2673 # place an action in the power registry which is not needed and can
2674@@ -1647,6 +1665,17 @@
2675 raise
2676
2677 deallocate_ip_address = True
2678+=======
2679+ try:
2680+ self.stop(self.owner)
2681+ except Exception as ex:
2682+ maaslog.error(
2683+ "%s: Unable to shut node down: %s", self.hostname,
2684+ unicode(ex))
2685+ raise
2686+
2687+ deallocate_ip_address = True
2688+>>>>>>> MERGE-SOURCE
2689 if self.power_state == POWER_STATE.OFF:
2690 # Node is already off.
2691 self.status = NODE_STATUS.READY
2692@@ -1680,6 +1709,7 @@
2693
2694 # Do these after updating the node to avoid creating deadlocks with
2695 # other node editing operations.
2696+<<<<<<< TREE
2697 if deallocate_ip_address:
2698 self._async_deallocate_static_ip_addresses()
2699
2700@@ -1688,6 +1718,15 @@
2701
2702 # If this node has non-installable children, remove them.
2703 self.children.all().delete()
2704+=======
2705+ if deallocate_ip_address:
2706+ self.deallocate_static_ip_addresses()
2707+
2708+ # We explicitly commit here because during bulk node actions we
2709+ # want to make sure that each successful state transition is
2710+ # recorded in the DB.
2711+ transaction.commit()
2712+>>>>>>> MERGE-SOURCE
2713
2714 def release_or_erase(self):
2715 """Either release the node or erase the node then release it, depending
2716@@ -1791,7 +1830,11 @@
2717 self.status = NODE_STATUS.READY
2718 self.owner = None
2719 self.stop_transition_monitor()
2720+<<<<<<< TREE
2721 self._async_deallocate_static_ip_addresses()
2722+=======
2723+ self.deallocate_static_ip_addresses()
2724+>>>>>>> MERGE-SOURCE
2725 self.save()
2726
2727 def claim_static_ip_addresses(
2728@@ -1830,6 +1873,7 @@
2729 # because it's all-or-nothing (hence the atomic context).
2730 return [(static_ip.ip, unicode(mac)) for static_ip in static_ips]
2731
2732+<<<<<<< TREE
2733 @staticmethod
2734 def update_nodegroup_host_maps(nodegroups, claims):
2735 """Update host maps for the given MAC->IP mappings.
2736@@ -1904,6 +1948,36 @@
2737 reactor, 0, deferToThread,
2738 transactional(self.deallocate_static_ip_addresses))
2739
2740+=======
2741+ def update_host_maps(self, claims):
2742+ """Update host maps for the given MAC->IP mappings."""
2743+ static_mappings = defaultdict(dict)
2744+ static_mappings[self.nodegroup].update(claims)
2745+ update_host_maps_failures = list(
2746+ update_host_maps(static_mappings))
2747+ if len(update_host_maps_failures) != 0:
2748+ # We've hit an error, so release any IPs we've claimed
2749+ # and then raise the error for the call site to
2750+ # handle.
2751+ StaticIPAddress.objects.deallocate_by_node(self)
2752+ # We know there's only one error because we only
2753+ # sent one mapping to update_host_maps(), so we
2754+ # extract the exception from the Failure and raise
2755+ # it.
2756+ raise update_host_maps_failures[0].raiseException()
2757+
2758+ def deallocate_static_ip_addresses(self):
2759+ """Release the `StaticIPAddress` that is assigned to this node and
2760+ remove the host mapping on the cluster.
2761+
2762+ This should only be done when the node is in an unused state.
2763+ """
2764+ deallocated_ips = StaticIPAddress.objects.deallocate_by_node(self)
2765+ self.delete_host_maps(deallocated_ips)
2766+ from maasserver.dns.config import change_dns_zones
2767+ change_dns_zones([self.nodegroup])
2768+
2769+>>>>>>> MERGE-SOURCE
2770 def get_boot_purpose(self):
2771 """
2772 Return a suitable "purpose" for this boot, e.g. "install".
2773@@ -1950,6 +2024,7 @@
2774 if self.pxe_mac is not None:
2775 return self.pxe_mac
2776
2777+<<<<<<< TREE
2778 # Only use "all" and perform the sorting manually to stop extra queries
2779 # when the `macaddress_set` is prefetched.
2780 macs = sorted(self.macaddress_set.all(), key=attrgetter('id'))
2781@@ -1979,6 +2054,25 @@
2782 if mac != pxe_mac
2783 ]
2784 return extra_macs
2785+=======
2786+ return self.macaddress_set.order_by('id').first()
2787+
2788+ def get_pxe_mac_vendor(self):
2789+ """Return the vendor of the MAC address the node pxebooted from."""
2790+ pxe_mac = self.get_pxe_mac()
2791+ if pxe_mac is None:
2792+ return None
2793+ else:
2794+ return get_vendor_for_mac(pxe_mac.mac_address.get_raw())
2795+
2796+ def get_extra_macs(self):
2797+ """Get the MACs other that the one the node PXE booted from."""
2798+ pxe_mac = self.get_pxe_mac()
2799+ extra_macs = self.macaddress_set.all()
2800+ if pxe_mac is not None:
2801+ extra_macs = extra_macs.exclude(mac_address=pxe_mac.mac_address)
2802+ return extra_macs
2803+>>>>>>> MERGE-SOURCE
2804
2805 def is_pxe_mac_on_managed_interface(self):
2806 pxe_mac = self.get_pxe_mac()
2807@@ -2026,6 +2120,7 @@
2808
2809 # Claim static IP addresses for the node if it's ALLOCATED.
2810 if self.status == NODE_STATUS.ALLOCATED:
2811+<<<<<<< TREE
2812
2813 # Don't update host maps if we're not on a managed interface.
2814 if not self.is_pxe_mac_on_managed_interface() and update_host_maps:
2815@@ -2033,6 +2128,13 @@
2816
2817 self.claim_static_ip_addresses(
2818 update_host_maps=update_host_maps)
2819+=======
2820+ claims = self.claim_static_ip_addresses()
2821+ # If the PXE mac is on a managed interface then we can ask
2822+ # the cluster to generate the DHCP host map(s).
2823+ if self.is_pxe_mac_on_managed_interface():
2824+ self.update_host_maps(claims)
2825+>>>>>>> MERGE-SOURCE
2826
2827 if self.status == NODE_STATUS.ALLOCATED:
2828 transition_monitor = (
2829
2830=== modified file 'src/maasserver/models/nodegroup.py'
2831--- src/maasserver/models/nodegroup.py 2015-06-30 09:33:28 +0000
2832+++ src/maasserver/models/nodegroup.py 2015-07-08 00:07:16 +0000
2833@@ -381,10 +381,15 @@
2834 password=password, power_control=power_control,
2835 accept_all=accept_all)
2836
2837+<<<<<<< TREE
2838 def add_virsh(self, user, poweraddr, password=None,
2839 prefix_filter=None, accept_all=False):
2840+=======
2841+ def add_virsh(self, poweraddr, password=None, prefix_filter=None):
2842+>>>>>>> MERGE-SOURCE
2843 """ Add all of the virtual machines inside a virsh controller.
2844
2845+<<<<<<< TREE
2846 :param user: user for the nodes.
2847 :param poweraddr: virsh connection string.
2848 :param password: ssh password.
2849@@ -436,6 +441,28 @@
2850
2851 def enlist_nodes_from_ucsm(self, user, url, username,
2852 password, accept_all=False):
2853+=======
2854+ :param poweraddr: virsh connection string
2855+ :param password: ssh password
2856+ :param prefix_filter: import based on prefix
2857+
2858+ :raises NoConnectionsAvailable: If no connections to the cluster
2859+ are available.
2860+ """
2861+ try:
2862+ client = getClientFor(self.uuid, timeout=1)
2863+ except NoConnectionsAvailable:
2864+ # No connection to the cluster so we can't do anything. We
2865+ # let the caller handle the error, since we don't want to
2866+ # just drop it.
2867+ raise
2868+ else:
2869+ return client(
2870+ AddVirsh, poweraddr=poweraddr,
2871+ password=password, prefix_filter=prefix_filter)
2872+
2873+ def enlist_nodes_from_ucsm(self, url, username, password):
2874+>>>>>>> MERGE-SOURCE
2875 """ Add the servers from a Cicso UCS Manager.
2876
2877 :param user: user for the nodes.
2878
2879=== modified file 'src/maasserver/models/signals/power.py'
2880--- src/maasserver/models/signals/power.py 2015-06-10 11:24:44 +0000
2881+++ src/maasserver/models/signals/power.py 2015-07-08 00:07:16 +0000
2882@@ -21,10 +21,15 @@
2883 from maasserver.models.signals.base import connect_to_field_change
2884 from maasserver.node_status import QUERY_TRANSITIONS
2885 from maasserver.rpc import getClientFor
2886+<<<<<<< TREE
2887 from maasserver.utils.orm import (
2888 post_commit,
2889 transactional,
2890 )
2891+=======
2892+from maasserver.signals import connect_to_field_change
2893+from maasserver.utils.async import transactional
2894+>>>>>>> MERGE-SOURCE
2895 from provisioningserver.logger import get_maas_logger
2896 from provisioningserver.power.poweraction import (
2897 PowerActionFail,
2898@@ -56,6 +61,7 @@
2899 WAIT_TO_QUERY = timedelta(seconds=20)
2900
2901
2902+<<<<<<< TREE
2903 @transactional
2904 def get_node_cluster_and_power_info(system_id):
2905 """Get the node, cluster, and power-info for the specified node.
2906@@ -96,6 +102,10 @@
2907
2908 @asynchronous(timeout=300)
2909 @inlineCallbacks
2910+=======
2911+@synchronous
2912+@transactional
2913+>>>>>>> MERGE-SOURCE
2914 def update_power_state_of_node(system_id):
2915 """Query and update the power state of the given node.
2916
2917
2918=== modified file 'src/maasserver/models/tests/test_bootsource.py'
2919--- src/maasserver/models/tests/test_bootsource.py 2015-05-07 18:14:38 +0000
2920+++ src/maasserver/models/tests/test_bootsource.py 2015-07-08 00:07:16 +0000
2921@@ -99,9 +99,15 @@
2922 [],
2923 boot_source_dict['selections'])
2924
2925+<<<<<<< TREE
2926 # XXX: GavinPanella 2015-03-03 bug=1376317: This test is fragile, possibly
2927 # due to isolation issues.
2928 @skip("Possible isolation issues")
2929+=======
2930+ # XXX: GavinPanella 2014-10-28 bug=1376317: This test is fragile, possibly
2931+ # due to isolation issues.
2932+ @skip("Possible isolation issues")
2933+>>>>>>> MERGE-SOURCE
2934 def test_calls_cache_boot_sources_on_create(self):
2935 mock_callLater = self.patch(reactor, 'callLater')
2936 BootSource.objects.create(
2937
2938=== modified file 'src/maasserver/models/tests/test_dhcplease.py'
2939=== modified file 'src/maasserver/models/tests/test_macaddress.py'
2940--- src/maasserver/models/tests/test_macaddress.py 2015-06-10 08:08:51 +0000
2941+++ src/maasserver/models/tests/test_macaddress.py 2015-07-08 00:07:16 +0000
2942@@ -55,8 +55,13 @@
2943 HasLength,
2944 Is,
2945 MatchesStructure,
2946+<<<<<<< TREE
2947 Not,
2948 )
2949+=======
2950+ Not,
2951+ )
2952+>>>>>>> MERGE-SOURCE
2953
2954
2955 def get_random_ip_from_interface_range(interface, use_static_range=None):
2956@@ -127,6 +132,7 @@
2957 mac_address=bytes_mac, node=factory.make_Node())
2958 self.assertEqual(bytes_mac, mac.__str__())
2959
2960+<<<<<<< TREE
2961 def test_cluster_interface_deletion_does_not_delete_MAC(self):
2962 cluster_interface = factory.make_NodeGroupInterface(
2963 factory.make_NodeGroup())
2964@@ -148,6 +154,16 @@
2965 parent.get_primary_mac().cluster_interface,
2966 mac.get_cluster_interface())
2967
2968+=======
2969+ def test_cluster_interface_deletion_does_not_delete_MAC(self):
2970+ cluster_interface = factory.make_NodeGroupInterface(
2971+ factory.make_NodeGroup())
2972+ mac_address = factory.make_MACAddress(
2973+ cluster_interface=cluster_interface)
2974+ cluster_interface.delete()
2975+ self.expectThat(reload_object(mac_address), Not(Is(None)))
2976+
2977+>>>>>>> MERGE-SOURCE
2978
2979 class TestFindClusterInterfaceResponsibleFor(MAASServerTestCase):
2980 """Tests for `find_cluster_interface_responsible_for_ip`."""
2981@@ -369,8 +385,13 @@
2982
2983 def test__returns_empty_if_no_cluster_interface(self):
2984 # If mac.cluster_interface is None, we can't allocate any IP.
2985+<<<<<<< TREE
2986 mac = factory.make_MACAddress_with_Node()
2987 self.assertEquals([], mac.claim_static_ips(update_host_maps=False))
2988+=======
2989+ mac = factory.make_MACAddress_with_Node()
2990+ self.assertEquals([], mac.claim_static_ips())
2991+>>>>>>> MERGE-SOURCE
2992
2993 def test__reserves_an_ip_address(self):
2994 node = factory.make_Node_with_MACAddress_and_NodeGroupInterface()
2995@@ -686,6 +707,7 @@
2996 [sip] = allocation
2997 self.assertEqual(IPAddress(requested_ip), IPAddress(sip.ip))
2998
2999+<<<<<<< TREE
3000 def test__links_static_ip_to_user_if_passed(self):
3001 cluster = factory.make_NodeGroup()
3002 cluster_interface = factory.make_NodeGroupInterface(cluster)
3003@@ -697,6 +719,18 @@
3004 update_host_maps=False)
3005 self.assertEqual(sip.user, user)
3006
3007+=======
3008+ def test__links_static_ip_to_user_if_passed(self):
3009+ cluster = factory.make_NodeGroup()
3010+ cluster_interface = factory.make_NodeGroupInterface(cluster)
3011+ mac_address = factory.make_MACAddress(
3012+ cluster_interface=cluster_interface)
3013+ user = factory.make_User()
3014+ [sip] = mac_address.claim_static_ips(
3015+ user=user, alloc_type=IPADDRESS_TYPE.USER_RESERVED)
3016+ self.assertEqual(sip.user, user)
3017+
3018+>>>>>>> MERGE-SOURCE
3019
3020 class TestGetClusterInterfaces(MAASServerTestCase):
3021 """Tests for `MACAddress.get_cluster_interfaces`."""
3022@@ -787,26 +821,68 @@
3023 class TestUpdateMacClusterInterfaces(MAASServerTestCase):
3024 """Tests for `update_mac_cluster_interfaces`()."""
3025
3026+<<<<<<< TREE
3027+=======
3028+ def make_cluster_with_macs_and_leases(self, use_static_range=False):
3029+ cluster = factory.make_NodeGroup()
3030+ mac_addresses = {
3031+ factory.make_MACAddress_with_Node():
3032+ factory.make_NodeGroupInterface(nodegroup=cluster)
3033+ for _ in range(4)
3034+ }
3035+ leases = {
3036+ get_random_ip_from_interface_range(interface, use_static_range): (
3037+ mac_address.mac_address)
3038+ for mac_address, interface in mac_addresses.viewitems()
3039+ }
3040+ return cluster, mac_addresses, leases
3041+
3042+ def make_cluster_with_mac_and_node_and_ip(self, use_static_range=False):
3043+ cluster = factory.make_NodeGroup()
3044+ mac_address = factory.make_MACAddress_with_Node()
3045+ interface = factory.make_NodeGroupInterface(nodegroup=cluster)
3046+ ip = get_random_ip_from_interface_range(interface, use_static_range)
3047+ return cluster, interface, mac_address, ip
3048+
3049+>>>>>>> MERGE-SOURCE
3050 def test_updates_mac_cluster_interfaces(self):
3051+<<<<<<< TREE
3052 cluster, interface, mac_address, ip = (
3053 make_cluster_with_mac_and_node_and_ip())
3054 update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
3055 mac_address = reload_object(mac_address)
3056 self.assertEqual(interface, mac_address.cluster_interface)
3057+=======
3058+ cluster, interface, mac_address, ip = (
3059+ self.make_cluster_with_mac_and_node_and_ip())
3060+ update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
3061+ mac_address = reload_object(mac_address)
3062+ self.assertEqual(interface, mac_address.cluster_interface)
3063+>>>>>>> MERGE-SOURCE
3064
3065 def test_considers_static_range_when_updating_interfaces(self):
3066 cluster, mac_addresses, leases = (
3067+<<<<<<< TREE
3068 make_cluster_with_macs_and_leases(use_static_range=True))
3069 cluster, interface, mac_address, ip = (
3070 make_cluster_with_mac_and_node_and_ip(use_static_range=True))
3071 update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
3072 mac_address = reload_object(mac_address)
3073 self.assertEqual(interface, mac_address.cluster_interface)
3074+=======
3075+ self.make_cluster_with_macs_and_leases(use_static_range=True))
3076+ cluster, interface, mac_address, ip = (
3077+ self.make_cluster_with_mac_and_node_and_ip(use_static_range=True))
3078+ update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
3079+ mac_address = reload_object(mac_address)
3080+ self.assertEqual(interface, mac_address.cluster_interface)
3081+>>>>>>> MERGE-SOURCE
3082
3083 def test_updates_network_relations(self):
3084 # update_mac_cluster_interfaces should also associate the mac
3085 # with the network on which it resides.
3086 cluster, mac_addresses, leases = (
3087+<<<<<<< TREE
3088 make_cluster_with_macs_and_leases())
3089 cluster, interface, mac_address, ip = (
3090 make_cluster_with_mac_and_node_and_ip())
3091@@ -819,6 +895,20 @@
3092 default_gateway=interface.router_ip,
3093 netmask=interface.subnet_mask,
3094 ))
3095+=======
3096+ self.make_cluster_with_macs_and_leases())
3097+ cluster, interface, mac_address, ip = (
3098+ self.make_cluster_with_mac_and_node_and_ip())
3099+ net = create_Network_from_NodeGroupInterface(interface)
3100+ update_mac_cluster_interfaces(ip, mac_address.mac_address, cluster)
3101+ [observed_macddress] = net.macaddress_set.all()
3102+ self.expectThat(mac_address, Equals(observed_macddress))
3103+ self.expectThat(
3104+ net, MatchesStructure.byEquality(
3105+ default_gateway=interface.router_ip,
3106+ netmask=interface.subnet_mask,
3107+ ))
3108+>>>>>>> MERGE-SOURCE
3109
3110 def test_does_not_overwrite_network_with_same_name(self):
3111 cluster = factory.make_NodeGroup()
3112
3113=== modified file 'src/maasserver/models/tests/test_network.py'
3114=== modified file 'src/maasserver/models/tests/test_node.py'
3115--- src/maasserver/models/tests/test_node.py 2015-07-01 14:55:23 +0000
3116+++ src/maasserver/models/tests/test_node.py 2015-07-08 00:07:16 +0000
3117@@ -60,11 +60,20 @@
3118 MACAddress,
3119 Node,
3120 node as node_module,
3121+<<<<<<< TREE
3122 )
3123 from maasserver.models.node import PowerInfo
3124 from maasserver.models.signals import power as node_query
3125 from maasserver.models.staticipaddress import StaticIPAddress
3126 from maasserver.models.timestampedmodel import now
3127+=======
3128+ )
3129+from maasserver.models.node import (
3130+ PowerInfo,
3131+ validate_hostname,
3132+ )
3133+from maasserver.models.staticipaddress import StaticIPAddress
3134+>>>>>>> MERGE-SOURCE
3135 from maasserver.models.user import create_auth_token
3136 from maasserver.node_status import (
3137 get_failed_status,
3138@@ -134,8 +143,13 @@
3139 Is,
3140 IsInstance,
3141 MatchesStructure,
3142+<<<<<<< TREE
3143 Not,
3144 )
3145+=======
3146+ Not,
3147+ )
3148+>>>>>>> MERGE-SOURCE
3149 from twisted.internet import defer
3150 from twisted.internet.threads import deferToThread
3151 from twisted.protocols import amp
3152@@ -196,6 +210,7 @@
3153 node = factory.make_Node(memory=2048)
3154 self.assertEqual('2', node.display_memory())
3155
3156+<<<<<<< TREE
3157 def test_physicalblockdevice_set_returns_physicalblockdevices(self):
3158 node = factory.make_Node()
3159 device = factory.make_PhysicalBlockDevice(node=node)
3160@@ -212,11 +227,20 @@
3161 def test_display_storage_returns_decimal_less_than_1000(self):
3162 node = factory.make_Node()
3163 factory.make_PhysicalBlockDevice(node=node, size=500 * (1000 ** 2))
3164+=======
3165+ def test_display_storage_returns_decimal_less_than_1000(self):
3166+ node = factory.make_Node(storage=500)
3167+>>>>>>> MERGE-SOURCE
3168 self.assertEqual('0.5', node.display_storage())
3169
3170+<<<<<<< TREE
3171 def test_display_storage_returns_value_divided_by_1000(self):
3172 node = factory.make_Node()
3173 factory.make_PhysicalBlockDevice(node=node, size=2000 * (1000 ** 2))
3174+=======
3175+ def test_display_storage_returns_value_divided_by_1000(self):
3176+ node = factory.make_Node(storage=2000)
3177+>>>>>>> MERGE-SOURCE
3178 self.assertEqual('2', node.display_storage())
3179
3180 def test_add_node_with_token(self):
3181@@ -1228,6 +1252,7 @@
3182 self.assertThat(
3183 node_stop, MockCalledOnceWith(user))
3184
3185+<<<<<<< TREE
3186 def test_release_doesnt_power_off_node_when_off(self):
3187 user = factory.make_User()
3188 node = factory.make_Node(
3189@@ -1292,6 +1317,60 @@
3190 # silence remove_host_maps
3191 self.patch_autospec(node_module, "remove_host_maps")
3192 dns_update_zones = self.patch(dns_config.dns_update_zones)
3193+=======
3194+ def test_release_deallocates_static_ip_when_node_is_off(self):
3195+ user = factory.make_User()
3196+ node = factory.make_node_with_mac_attached_to_nodegroupinterface(
3197+ owner=user, status=NODE_STATUS.ALLOCATED,
3198+ power_state=POWER_STATE.OFF)
3199+ deallocate_static_ip_addresses = self.patch_autospec(
3200+ node, "deallocate_static_ip_addresses")
3201+ self.patch(node, 'start_transition_monitor')
3202+ node.release()
3203+ self.assertThat(
3204+ deallocate_static_ip_addresses, MockCalledOnceWith())
3205+
3206+ def test_release_deallocates_static_ip_when_node_cannot_be_queried(self):
3207+ user = factory.make_User()
3208+ node = factory.make_node_with_mac_attached_to_nodegroupinterface(
3209+ owner=user, status=NODE_STATUS.ALLOCATED,
3210+ power_state=POWER_STATE.ON, power_type='ether_wake')
3211+ deallocate_static_ip_addresses = self.patch_autospec(
3212+ node, "deallocate_static_ip_addresses")
3213+ self.patch(node, 'start_transition_monitor')
3214+ node.release()
3215+ self.assertThat(
3216+ deallocate_static_ip_addresses, MockCalledOnceWith())
3217+
3218+ def test_release_doesnt_deallocate_static_ip_when_node_releasing(self):
3219+ user = factory.make_User()
3220+ node = factory.make_node_with_mac_attached_to_nodegroupinterface(
3221+ owner=user, status=NODE_STATUS.ALLOCATED,
3222+ power_state=POWER_STATE.ON, power_type='virsh')
3223+ deallocate_static_ip_addresses = self.patch_autospec(
3224+ node, "deallocate_static_ip_addresses")
3225+ self.patch_autospec(node, 'stop')
3226+ self.patch(node, 'start_transition_monitor')
3227+ node.release()
3228+ self.assertThat(
3229+ deallocate_static_ip_addresses, MockNotCalled())
3230+
3231+ def test_deallocate_static_ip_deletes_static_ip_host_maps(self):
3232+ remove_host_maps = self.patch_autospec(
3233+ node_module, "remove_host_maps")
3234+ user = factory.make_User()
3235+ node = factory.make_node_with_mac_attached_to_nodegroupinterface(
3236+ owner=user, status=NODE_STATUS.ALLOCATED)
3237+ sips = node.get_primary_mac().claim_static_ips()
3238+ node.release()
3239+ expected = {sip.ip.format() for sip in sips}
3240+ self.assertThat(
3241+ remove_host_maps, MockCalledOnceWith(
3242+ {node.nodegroup: expected}))
3243+
3244+ def test_deallocate_static_ip_updates_dns(self):
3245+ change_dns_zones = self.patch(dns_config, 'change_dns_zones')
3246+>>>>>>> MERGE-SOURCE
3247 nodegroup = factory.make_NodeGroup(
3248 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP_AND_DNS,
3249 status=NODEGROUP_STATUS.ENABLED)
3250@@ -1922,24 +2001,45 @@
3251 node.update_power_state(POWER_STATE.ON)
3252 self.expectThat(node.status, Equals(NODE_STATUS.ALLOCATED))
3253
3254- def test_update_power_state_deallocates_static_ips_if_releasing(self):
3255- node = factory.make_Node(
3256- power_state=POWER_STATE.ON, status=NODE_STATUS.RELEASING,
3257- owner=None)
3258- deallocate_static_ip_addresses = self.patch_autospec(
3259- node, '_async_deallocate_static_ip_addresses')
3260- self.patch(node, 'stop_transition_monitor')
3261- node.update_power_state(POWER_STATE.OFF)
3262- self.assertThat(deallocate_static_ip_addresses, MockCalledOnceWith())
3263-
3264- def test_update_power_state_doesnt_deallocates_static_ips_if_not_off(self):
3265- node = factory.make_Node(
3266- power_state=POWER_STATE.OFF, status=NODE_STATUS.ALLOCATED)
3267- deallocate_static_ip_addresses = self.patch_autospec(
3268- node, '_async_deallocate_static_ip_addresses')
3269- node.update_power_state(POWER_STATE.ON)
3270- self.assertThat(deallocate_static_ip_addresses, MockNotCalled())
3271-
3272+<<<<<<< TREE
3273+ def test_update_power_state_deallocates_static_ips_if_releasing(self):
3274+ node = factory.make_Node(
3275+ power_state=POWER_STATE.ON, status=NODE_STATUS.RELEASING,
3276+ owner=None)
3277+ deallocate_static_ip_addresses = self.patch_autospec(
3278+ node, '_async_deallocate_static_ip_addresses')
3279+ self.patch(node, 'stop_transition_monitor')
3280+ node.update_power_state(POWER_STATE.OFF)
3281+ self.assertThat(deallocate_static_ip_addresses, MockCalledOnceWith())
3282+
3283+ def test_update_power_state_doesnt_deallocates_static_ips_if_not_off(self):
3284+ node = factory.make_Node(
3285+ power_state=POWER_STATE.OFF, status=NODE_STATUS.ALLOCATED)
3286+ deallocate_static_ip_addresses = self.patch_autospec(
3287+ node, '_async_deallocate_static_ip_addresses')
3288+ node.update_power_state(POWER_STATE.ON)
3289+ self.assertThat(deallocate_static_ip_addresses, MockNotCalled())
3290+
3291+=======
3292+ def test_update_power_state_deallocates_static_ips_if_releasing(self):
3293+ node = factory.make_Node(
3294+ power_state=POWER_STATE.ON, status=NODE_STATUS.RELEASING,
3295+ owner=None)
3296+ deallocate_static_ip_addresses = self.patch_autospec(
3297+ node, 'deallocate_static_ip_addresses')
3298+ self.patch(node, 'stop_transition_monitor')
3299+ node.update_power_state(POWER_STATE.OFF)
3300+ self.assertThat(deallocate_static_ip_addresses, MockCalledOnceWith())
3301+
3302+ def test_update_power_state_doesnt_deallocates_static_ips_if_not_off(self):
3303+ node = factory.make_Node(
3304+ power_state=POWER_STATE.OFF, status=NODE_STATUS.ALLOCATED)
3305+ deallocate_static_ip_addresses = self.patch_autospec(
3306+ node, 'deallocate_static_ip_addresses')
3307+ node.update_power_state(POWER_STATE.ON)
3308+ self.assertThat(deallocate_static_ip_addresses, MockNotCalled())
3309+
3310+>>>>>>> MERGE-SOURCE
3311 def test_end_deployment_changes_state(self):
3312 self.disable_node_query()
3313 node = factory.make_Node(status=NODE_STATUS.DEPLOYING)
3314@@ -2032,6 +2132,7 @@
3315 factory.make_MACAddress(node=node)
3316 self.assertEqual(node.macaddress_set.first(), node.get_pxe_mac())
3317
3318+<<<<<<< TREE
3319 def test_pxe_mac_deletion_does_not_delete_node(self):
3320 node = factory.make_Node(mac=True)
3321 node.pxe_mac = factory.make_MACAddress(node=node)
3322@@ -2150,6 +2251,46 @@
3323 "Filesystem on partition on virtual block device should have "
3324 "been removed.")
3325
3326+=======
3327+ def test_pxe_mac_deletion_does_not_delete_node(self):
3328+ node = factory.make_Node(mac=True)
3329+ node.pxe_mac = factory.make_MACAddress(node=node)
3330+ node.save()
3331+ node.pxe_mac.delete()
3332+ self.assertThat(reload_object(node), Not(Is(None)))
3333+
3334+ def test_get_pxe_mac_vendor_returns_vendor(self):
3335+ node = factory.make_Node()
3336+ mac = factory.make_MACAddress(address='ec:a8:6b:fd:ae:3f', node=node)
3337+ node.pxe_mac = mac
3338+ node.save()
3339+ self.assertEqual(
3340+ "ELITEGROUP COMPUTER SYSTEMS CO., LTD.",
3341+ node.get_pxe_mac_vendor())
3342+
3343+ def test_get_extra_macs_returns_all_but_pxe_mac(self):
3344+ node = factory.make_Node()
3345+ macs = [factory.make_MACAddress(node=node) for _ in xrange(3)]
3346+ # Do not set the pxe mac to the first mac to make sure the pxe mac
3347+ # (and not the first created) is excluded from the list returned by
3348+ # `get_extra_macs`.
3349+ pxe_mac_index = 1
3350+ node.pxe_mac = macs[pxe_mac_index]
3351+ node.save()
3352+ del macs[pxe_mac_index]
3353+ self.assertItemsEqual(
3354+ macs,
3355+ node.get_extra_macs())
3356+
3357+ def test_get_extra_macs_returns_all_but_first_mac_if_no_pxe_mac(self):
3358+ node = factory.make_Node()
3359+ macs = [factory.make_MACAddress(node=node) for _ in xrange(3)]
3360+ node.save()
3361+ self.assertItemsEqual(
3362+ macs[1:],
3363+ node.get_extra_macs())
3364+
3365+>>>>>>> MERGE-SOURCE
3366
3367 class TestNode_pxe_mac_on_managed_interface(MAASServerTestCase):
3368
3369
3370=== modified file 'src/maasserver/models/tests/test_nodegroup.py'
3371--- src/maasserver/models/tests/test_nodegroup.py 2015-06-30 09:33:28 +0000
3372+++ src/maasserver/models/tests/test_nodegroup.py 2015-07-08 00:07:16 +0000
3373@@ -567,9 +567,15 @@
3374
3375 self.expectThat(
3376 protocol.AddVirsh,
3377+<<<<<<< TREE
3378 MockCalledOnceWith(
3379 ANY, user=user, poweraddr=poweraddr,
3380 password=password, prefix_filter=None, accept_all=True))
3381+=======
3382+ MockCalledOnceWith(
3383+ ANY, poweraddr=poweraddr,
3384+ password=password, prefix_filter=None))
3385+>>>>>>> MERGE-SOURCE
3386
3387 def test_add_virsh_calls_client_with_resource_endpoint(self):
3388 getClientFor = self.patch(nodegroup_module, 'getClientFor')
3389@@ -584,8 +590,13 @@
3390 self.expectThat(
3391 client,
3392 MockCalledOnceWith(
3393+<<<<<<< TREE
3394 AddVirsh, user=user, poweraddr=poweraddr,
3395 password=password, prefix_filter=None, accept_all=True))
3396+=======
3397+ AddVirsh, poweraddr=poweraddr,
3398+ password=password, prefix_filter=None))
3399+>>>>>>> MERGE-SOURCE
3400
3401 def test_add_virsh_raises_if_no_connection_to_cluster(self):
3402 getClientFor = self.patch(nodegroup_module, 'getClientFor')
3403
3404=== modified file 'src/maasserver/node_constraint_filter_forms.py'
3405=== modified file 'src/maasserver/preseed.py'
3406=== modified file 'src/maasserver/rpc/leases.py'
3407=== modified file 'src/maasserver/rpc/nodes.py'
3408--- src/maasserver/rpc/nodes.py 2015-07-01 17:09:47 +0000
3409+++ src/maasserver/rpc/nodes.py 2015-07-08 00:07:16 +0000
3410@@ -71,6 +71,7 @@
3411
3412 :return: A generator yielding `dict`s.
3413 """
3414+<<<<<<< TREE
3415 five_minutes_ago = now() - timedelta(minutes=5)
3416
3417 # This is meant to be temporary until all the power types support querying
3418@@ -99,6 +100,19 @@
3419 power_info = node.get_effective_power_info()
3420 if power_info.power_type is not None:
3421 yield {
3422+=======
3423+ try:
3424+ nodegroup = NodeGroup.objects.get_by_natural_key(uuid)
3425+ except NodeGroup.DoesNotExist:
3426+ raise NoSuchCluster.from_uuid(uuid)
3427+ else:
3428+ power_info_by_node = (
3429+ (node, node.get_effective_power_info())
3430+ for node in nodegroup.node_set.exclude(status=NODE_STATUS.BROKEN)
3431+ )
3432+ return [
3433+ {
3434+>>>>>>> MERGE-SOURCE
3435 'system_id': node.system_id,
3436 'hostname': node.hostname,
3437 'power_state': node.power_state,
3438
3439=== modified file 'src/maasserver/rpc/tests/test_nodes.py'
3440--- src/maasserver/rpc/tests/test_nodes.py 2015-07-02 09:05:43 +0000
3441+++ src/maasserver/rpc/tests/test_nodes.py 2015-07-08 00:07:16 +0000
3442@@ -55,6 +55,7 @@
3443 )
3444 from provisioningserver.rpc.power import QUERY_POWER_TYPES
3445 from simplejson import dumps
3446+<<<<<<< TREE
3447 from testtools import ExpectedException
3448 from testtools.matchers import (
3449 Contains,
3450@@ -64,6 +65,12 @@
3451 LessThan,
3452 Not,
3453 )
3454+=======
3455+from testtools.matchers import (
3456+ Contains,
3457+ Not,
3458+ )
3459+>>>>>>> MERGE-SOURCE
3460
3461
3462 class TestCreateNode(MAASServerTestCase):
3463@@ -319,6 +326,7 @@
3464 node, boot_purpose = request_node_info_by_mac_address(
3465 mac_address.mac_address.get_raw())
3466 self.assertEqual(node, mac_address.node)
3467+<<<<<<< TREE
3468
3469
3470 class TestListClusterNodesPowerParameters(MAASServerTestCase):
3471@@ -454,3 +462,27 @@
3472 self.expectThat(nodes_json_length, LessThan(expected_maximum + 1))
3473 expected_minimum = 50 * (2 ** 10) # 50kiB
3474 self.expectThat(nodes_json_length, GreaterThan(expected_minimum - 1))
3475+=======
3476+
3477+
3478+class TestListClusterNodesPowerParameters(MAASServerTestCase):
3479+ """Tests for the `list_cluster_nodes_power_parameters()` function."""
3480+
3481+ # Note that there are other, one-level-removed tests for this
3482+ # function in the TestRegionProtocol_ListNodePowerParameters
3483+ # testcase in maasserver.rpc.tests.test_regionservice.
3484+ # Those tests have been left there for now because they also check
3485+ # that the return values are being formatted correctly for RPC.
3486+
3487+ def test_does_not_return_power_info_for_broken_nodes(self):
3488+ cluster = factory.make_NodeGroup()
3489+ broken_node = factory.make_Node(
3490+ nodegroup=cluster, status=NODE_STATUS.BROKEN)
3491+
3492+ power_parameters = list_cluster_nodes_power_parameters(cluster.uuid)
3493+ returned_system_ids = [
3494+ power_params['system_id'] for power_params in power_parameters]
3495+
3496+ self.assertThat(
3497+ returned_system_ids, Not(Contains(broken_node.system_id)))
3498+>>>>>>> MERGE-SOURCE
3499
3500=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
3501=== modified file 'src/maasserver/start_up.py'
3502--- src/maasserver/start_up.py 2015-05-29 16:47:37 +0000
3503+++ src/maasserver/start_up.py 2015-07-08 00:07:16 +0000
3504@@ -23,13 +23,23 @@
3505 from maasserver import (
3506 locks,
3507 security,
3508+<<<<<<< TREE
3509 )
3510 from maasserver.bootresources import (
3511 ensure_boot_source_definition,
3512 import_resources,
3513 )
3514 from maasserver.dns.config import dns_update_all_zones
3515+=======
3516+ )
3517+from maasserver.bootresources import (
3518+ ensure_boot_source_definition,
3519+ import_resources,
3520+ )
3521+from maasserver.dns.config import write_full_dns_config
3522+>>>>>>> MERGE-SOURCE
3523 from maasserver.fields import register_mac_type
3524+<<<<<<< TREE
3525 from maasserver.models import (
3526 BootResource,
3527 BootSource,
3528@@ -44,6 +54,15 @@
3529 )
3530 from provisioningserver.logger import get_maas_logger
3531 from provisioningserver.rpc.boot_images import list_boot_images
3532+=======
3533+from maasserver.models import (
3534+ BootResource,
3535+ BootSource,
3536+ BootSourceSelection,
3537+ NodeGroup,
3538+ )
3539+from provisioningserver.rpc.boot_images import list_boot_images
3540+>>>>>>> MERGE-SOURCE
3541 from provisioningserver.upgrade_cluster import create_gnupg_home
3542 from provisioningserver.utils.twisted import (
3543 asynchronous,
3544@@ -71,6 +90,7 @@
3545 but this method uses database locking to ensure that the methods it calls
3546 internally are not run concurrently.
3547 """
3548+<<<<<<< TREE
3549 while True:
3550 try:
3551 # Get the shared secret from Tidmouth sheds which was generated
3552@@ -165,6 +185,73 @@
3553
3554 @transactional
3555 @synchronised(locks.startup)
3556+=======
3557+ # Get the shared secret from Tidmouth sheds which was generated when Sir
3558+ # Topham Hatt graduated Sodor Academy. (Ensure we have a shared-secret so
3559+ # that clusters on the same host can use it to authenticate.)
3560+ security.get_shared_secret()
3561+
3562+ with transaction.atomic():
3563+ with locks.startup:
3564+ inner_start_up()
3565+
3566+ eventloop.start().wait(10)
3567+
3568+
3569+def start_import_on_upgrade():
3570+ """Starts importing `BootResource`s on upgrade from MAAS where the boot
3571+ images where only stored on the clusters."""
3572+ # Do nothing, because `BootResource`s already exist.
3573+ if BootResource.objects.exists():
3574+ return
3575+
3576+ # Do nothing if the cluster on the machine does not have
3577+ # boot images present.
3578+ boot_images = list_boot_images()
3579+ if len(boot_images) == 0:
3580+ return
3581+
3582+ # Build the selections that need to be set based on the images
3583+ # that live on the cluster.
3584+ osystems = dict()
3585+ for image in boot_images:
3586+ osystem = image["osystem"]
3587+ if osystem not in osystems:
3588+ osystems[osystem] = {
3589+ "arches": set(),
3590+ "releases": set(),
3591+ "labels": set(),
3592+ }
3593+ osystems[osystem]["arches"].add(
3594+ image["architecture"])
3595+ osystems[osystem]["releases"].add(
3596+ image["release"])
3597+ osystems[osystem]["labels"].add(
3598+ image["label"])
3599+
3600+ # We have no way to truly know which boot source this came
3601+ # from, but since this should only occur on upgrade we
3602+ # take the first source, which will be the default source and
3603+ # apply the selection to that source.
3604+ boot_source = BootSource.objects.first()
3605+
3606+ # Clear all current selections and create the new selections
3607+ # based on the information retrieved from list_boot_images.
3608+ boot_source.bootsourceselection_set.all().delete()
3609+ for osystem, options in osystems.items():
3610+ for release in options["releases"]:
3611+ BootSourceSelection.objects.create(
3612+ boot_source=boot_source, os=osystem,
3613+ release=release, arches=list(options["arches"]),
3614+ subarches=["*"], labels=list(options["labels"]))
3615+
3616+ # Start the import process for the user, since the cluster
3617+ # already has images. Even though the cluster is usable the
3618+ # region will not be usable until it has boot images as well.
3619+ import_resources()
3620+
3621+
3622+>>>>>>> MERGE-SOURCE
3623 def inner_start_up():
3624 """Startup jobs that must run serialized w.r.t. other starting servers."""
3625 # Register our MAC data type with psycopg.
3626@@ -182,11 +269,17 @@
3627 # If there are no boot-source definitions yet, create defaults.
3628 ensure_boot_source_definition()
3629
3630+<<<<<<< TREE
3631 # Start import on upgrade if needed.
3632 start_import_on_upgrade()
3633
3634 # Register all of the triggers.
3635 register_all_triggers()
3636
3637+=======
3638+ # Start import on upgrade if needed.
3639+ start_import_on_upgrade()
3640+
3641+>>>>>>> MERGE-SOURCE
3642 # Regenerate MAAS's DNS configuration. This should be reentrant, really.
3643 dns_update_all_zones(reload_retry=True)
3644
3645=== added file 'src/maasserver/static/js/node_check.js.OTHER'
3646--- src/maasserver/static/js/node_check.js.OTHER 1970-01-01 00:00:00 +0000
3647+++ src/maasserver/static/js/node_check.js.OTHER 2015-07-08 00:07:16 +0000
3648@@ -0,0 +1,186 @@
3649+/* Copyright 2012 Canonical Ltd. This software is licensed under the
3650+ * GNU Affero General Public License version 3 (see the file LICENSE).
3651+ *
3652+ * Utilities for the node page.
3653+ *
3654+ * @module Y.maas.node_check
3655+ */
3656+
3657+YUI.add('maas.node_check', function(Y) {
3658+
3659+Y.log('loading maas.node_check');
3660+var module = Y.namespace('maas.node_check');
3661+
3662+// Only used to mockup io in tests.
3663+module._io = new Y.IO();
3664+
3665+var PowerCheckWidget;
3666+
3667+PowerCheckWidget = function() {
3668+ PowerCheckWidget.superclass.constructor.apply(this, arguments);
3669+};
3670+
3671+PowerCheckWidget.NAME = 'powercheck-widget';
3672+
3673+PowerCheckWidget.ATTRS = {
3674+ // The status text.
3675+ status_text: {
3676+ readOnly: true,
3677+ getter: function() {
3678+ return this.status_check.get('text');
3679+ }
3680+ },
3681+
3682+ // The error text.
3683+ error_text: {
3684+ readOnly: true,
3685+ getter: function() {
3686+ return this.error_msg.get('text');
3687+ }
3688+ }
3689+};
3690+
3691+Y.extend(PowerCheckWidget, Y.Widget, {
3692+
3693+ initializer: function(cfg) {
3694+ this.system_id = cfg.system_id;
3695+ // Create action button.
3696+ this.button = Y.Node.create('<button />')
3697+ .addClass('secondary')
3698+ .addClass('space-top')
3699+ .setAttribute('type', 'submit')
3700+ .setAttribute('name', 'action')
3701+ .setAttribute('value', 'check-powerstate')
3702+ .set('text', 'Check power state');
3703+ // Store initial conditions.
3704+ this.initial_background = this.button.getStyle('background');
3705+ this.initial_color = this.button.getStyle('color');
3706+ // Initialize widget elements.
3707+ this.status_check = Y.Node.create('<div />')
3708+ .addClass('power-check-ok');
3709+ this.error_msg = Y.Node.create('<div />')
3710+ .addClass('power-check-error');
3711+ this.button.setStyle('position', 'relative');
3712+ this.spinnerNode = Y.Node.create('<img />')
3713+ .addClass('spinner')
3714+ .setStyle('position', 'absolute')
3715+ .setStyle('top', '4px')
3716+ .setStyle('right', '70px')
3717+ .setStyle('margin', '0')
3718+ .set('src', MAAS_config.uris.statics + 'img/spinner_grey.gif');
3719+ },
3720+
3721+ bindUI: function() {
3722+ var self = this;
3723+ this.button.on('click', function(e) {
3724+ e.preventDefault();
3725+ self.requestPowerState();
3726+ });
3727+ },
3728+
3729+ destructor: function() {
3730+ this.button.remove();
3731+ this.status_check.remove();
3732+ this.error_msg.remove();
3733+ },
3734+
3735+ extractStateFromResponse: function(out) {
3736+ return 'on';
3737+ },
3738+
3739+ /**
3740+ * Request the power state of this node.
3741+ *
3742+ * @method requestPowerState
3743+ */
3744+ requestPowerState: function() {
3745+ var self = this;
3746+ var cfg = {
3747+ method: 'GET',
3748+ data: Y.QueryString.stringify({
3749+ op: 'query_power_state'
3750+ }),
3751+ sync: false,
3752+ on: {
3753+ start: Y.bind(self.showSpinner, self),
3754+ end: Y.bind(self.hideSpinner, self),
3755+ success: function(id, out) {
3756+ Y.log(out);
3757+ try {
3758+ stateResponse = JSON.parse(out.response);
3759+ }
3760+ catch(e) {
3761+ // Parsing error.
3762+ self.displayErrorMessage('Unable to parse response.');
3763+ }
3764+ var state = stateResponse.state;
3765+ if (state === 'on' || state === 'off') {
3766+ self.displaySuccessMessage(
3767+ "Success: node is " + state + ".");
3768+ }
3769+ else {
3770+ self.displayErrorMessage(
3771+ "Error: " + state + ".");
3772+ }
3773+ },
3774+ failure: function(id, out) {
3775+ Y.log(out.responseText);
3776+ self.displayErrorMessage(
3777+ "Error: " + out.responseText + ".");
3778+ }
3779+ }
3780+ };
3781+ var url = MAAS_config.uris.nodes_handler + this.system_id;
3782+ module._io.send(url, cfg);
3783+ },
3784+
3785+ displayErrorMessage: function(message) {
3786+ this.error_msg.set('text', message);
3787+ },
3788+
3789+ displaySuccessMessage: function(message) {
3790+ this.status_check.set('text', message);
3791+ },
3792+
3793+ showSpinner: function() {
3794+ // Reset messages.
3795+ this.displayErrorMessage('');
3796+ this.displaySuccessMessage('');
3797+ // Set in-progress color and background.
3798+ this.button
3799+ .setStyle('color', '#BBB')
3800+ .setStyle('background', '#999')
3801+ .append(this.spinnerNode);
3802+ },
3803+
3804+ hideSpinner: function() {
3805+ // Restore color and background.
3806+ this.button
3807+ .setStyle('background', this.initial_background);
3808+ this.button
3809+ .setStyle('color', this.initial_color);
3810+ this.spinnerNode.remove();
3811+ },
3812+
3813+ render: function() {
3814+ this.button.remove();
3815+ this.status_check.remove();
3816+ this.error_msg.remove();
3817+ var srcNode = this.get('srcNode');
3818+ var formActions = srcNode.one('form#node_actions');
3819+ if (Y.Lang.isValue(formActions)) {
3820+ formActions.insert(this.button, 'after');
3821+ this.button
3822+ .insert(this.status_check, "after");
3823+ this.button
3824+ .insert(this.error_msg, "after");
3825+ }
3826+ this.bindUI();
3827+ }
3828+
3829+});
3830+
3831+module.PowerCheckWidget = PowerCheckWidget;
3832+
3833+}, '0.1', {'requires': ['widget', 'io']}
3834+);
3835
3836=== added file 'src/maasserver/static/js/node_views.js.OTHER'
3837--- src/maasserver/static/js/node_views.js.OTHER 1970-01-01 00:00:00 +0000
3838+++ src/maasserver/static/js/node_views.js.OTHER 2015-07-08 00:07:16 +0000
3839@@ -0,0 +1,680 @@
3840+/* Copyright 2012-2014 Canonical Ltd. This software is licensed under the
3841+ * GNU Affero General Public License version 3 (see the file LICENSE).
3842+ *
3843+ * Node model.
3844+ *
3845+ * @module Y.maas.node_views
3846+ */
3847+
3848+YUI.add('maas.node_views', function(Y) {
3849+
3850+Y.log('loading maas.node_views');
3851+var module = Y.namespace('maas.node_views');
3852+
3853+var NODE_STATUS = Y.maas.enums.NODE_STATUS;
3854+
3855+
3856+/**
3857+ * A base view class to display a set of Nodes (Y.maas.node.Node).
3858+ *
3859+ * It will load the list of visible nodes (in this.modelList) when rendered
3860+ * for the first time and be subscribed to 'nodeAdded' events published by
3861+ * Y.maas.node_add.AddNodeDispatcher. Changes to this.modelList will trigger
3862+ * re-rendering.
3863+ *
3864+ * You can provide your custom rendering method by defining a 'render'
3865+ * method (also, you can provide methods named 'loadNodesStarted' and
3866+ * 'loadNodesEnded' to customize the display during the initial loading of the
3867+ * visible nodes and a method named 'displayGlobalError' to display a message
3868+ * when errors occur during loading).
3869+ *
3870+ */
3871+module.NodeListLoader = Y.Base.create('nodeListLoader', Y.View, [], {
3872+
3873+ initializer: function(config) {
3874+ this.modelList = new Y.maas.node.NodeList();
3875+ this.loaded = false;
3876+ },
3877+
3878+ render: function () {
3879+ },
3880+
3881+ /**
3882+ * Add a loader, a Y.IO object. Events fired by this IO object will
3883+ * be followed, and will drive updates to this object's model.
3884+ *
3885+ * It may be wiser to remodel this to consume a YUI DataSource. That
3886+ * would make testing easier, for one, but it would also mean we can
3887+ * eliminate our polling code: DataSource has support for polling
3888+ * via the datasource-polling module.
3889+ *
3890+ * @method addLoader
3891+ */
3892+ addLoader: function(loader) {
3893+ loader.on("io:start", this.loadNodesStarted, this);
3894+ loader.on("io:end", this.loadNodesEnded, this);
3895+ loader.on("io:failure", this.loadNodesFailed, this);
3896+ loader.on("io:success", function(id, request) {
3897+ this.loadNodes(request.responseText);
3898+ }, this);
3899+ },
3900+
3901+ /**
3902+ * Load the nodes from the given data.
3903+ *
3904+ * @method loadNodes
3905+ */
3906+ loadNodes: function(data) {
3907+ try {
3908+ var nodes = JSON.parse(data);
3909+ this.mergeNodes(nodes);
3910+ }
3911+ catch(e) {
3912+ this.loadNodesFailed();
3913+ }
3914+ // Record that at least one load has been done.
3915+ this.loaded = true;
3916+ },
3917+
3918+ /**
3919+ * Process an array of nodes, merging them into modelList with the
3920+ * fewest modifications possible.
3921+ *
3922+ * @method mergeNodes
3923+ */
3924+ mergeNodes: function(nodes) {
3925+ var self = this; // JavaScript sucks.
3926+
3927+ // Attributes that we're checking for changes.
3928+ var attrs = ["hostname", "status"];
3929+
3930+ var nodesBySystemID = {};
3931+ Y.Array.each(nodes, function(node) {
3932+ nodesBySystemID[node.system_id] = node;
3933+ });
3934+ var modelsBySystemID = {};
3935+ this.modelList.each(function(model) {
3936+ modelsBySystemID[model.get("system_id")] = model;
3937+ });
3938+
3939+ Y.each(nodesBySystemID, function(node, system_id) {
3940+ var model = modelsBySystemID[system_id];
3941+ if (Y.Lang.isValue(model)) {
3942+ // Compare the node and the model.
3943+ var modelAttrs = model.getAttrs(attrs);
3944+ var modelChanges = {};
3945+ Y.each(modelAttrs, function(value, key) {
3946+ if (node[key] !== value) {
3947+ modelChanges[key] = node[key];
3948+ }
3949+ });
3950+ // Update the node.
3951+ model.setAttrs(modelChanges);
3952+ }
3953+ else {
3954+ // Add the node.
3955+ self.modelList.add(node);
3956+ }
3957+ });
3958+
3959+ Y.each(modelsBySystemID, function(model, system_id) {
3960+ // Remove models that don't correspond to a node.
3961+ if (!Y.Object.owns(nodesBySystemID, system_id)) {
3962+ self.modelList.remove(model);
3963+ }
3964+ });
3965+ },
3966+
3967+ /**
3968+ * Function called when rendering occurs. this.modelList is guaranteed
3969+ * to be populated.
3970+ *
3971+ * @method display
3972+ */
3973+ display: function () {
3974+ },
3975+
3976+ /**
3977+ * Function called if an error occurs during the initial node loading.
3978+ * to be populated.
3979+ *
3980+ * @method displayGlobalError
3981+ */
3982+ displayGlobalError: function (error_message) {
3983+ },
3984+
3985+ /**
3986+ * Function called when the Node list starts loading.
3987+ *
3988+ * @method loadNodesStarted
3989+ */
3990+ loadNodesStarted: function() {
3991+ },
3992+
3993+ /**
3994+ * Function called when the Node list has loaded.
3995+ *
3996+ * @method loadNodesEnded
3997+ */
3998+ loadNodesEnded: function() {
3999+ },
4000+
4001+ /**
4002+ * Function called when the Node list failed to load.
4003+ *
4004+ * @method loadNodesFailed
4005+ */
4006+ loadNodesFailed: function() {
4007+ this.displayGlobalError('Unable to load nodes.');
4008+ }
4009+
4010+});
4011+
4012+/**
4013+ * A customized view based on NodeListLoader that will display a dashboard
4014+ * of the nodes.
4015+ */
4016+module.NodesDashboard = Y.Base.create(
4017+ 'nodesDashboard', module.NodeListLoader, [], {
4018+
4019+ // Templates.
4020+ added_template: 'node{plural} added but never seen',
4021+ all_template: 'node{plural} in this MAAS',
4022+ allocated_template: 'node{plural} allocated',
4023+ commissioned_template: 'node{plural} commissioned',
4024+ offline_template: 'node{plural} offline',
4025+ queued_template: 'node{plural} queued',
4026+ reserved_template: '{nodes} node{plural} reserved for named deployment.',
4027+ retired_template: '{nodes} retired node{plural} not represented.',
4028+
4029+ initializer: function(config) {
4030+ this.srcNode = Y.one(config.srcNode);
4031+ this.summaryNode = this.srcNode.one(config.summaryNode);
4032+ this.numberNode = this.srcNode.one(config.numberNode);
4033+ this.descriptionNode = this.srcNode.one(config.descriptionNode);
4034+ this.reservedNode = this.srcNode.one(config.reservedNode);
4035+ // XXX: GavinPanella 2012-04-17 bug=984117:
4036+ // Hidden until we support reserved nodes.
4037+ this.reservedNode.hide();
4038+ this.retiredNode = this.srcNode.one(config.retiredNode);
4039+ // XXX: GavinPanella 2012-04-17 bug=984116:
4040+ // Hidden until we support retired nodes.
4041+ this.retiredNode.hide();
4042+
4043+ this.stats = new Y.maas.node.NodeStats();
4044+
4045+ this.fade_out = new Y.Anim({
4046+ node: this.summaryNode,
4047+ to: {opacity: 0},
4048+ duration: 0.1,
4049+ easing: 'easeIn'
4050+ });
4051+ this.fade_in = new Y.Anim({
4052+ node: this.summaryNode,
4053+ to: {opacity: 1},
4054+ duration: 0.2,
4055+ easing: 'easeIn'
4056+ });
4057+ // Prepare spinnerNode.
4058+ this.spinnerNode = Y.Node.create('<img />')
4059+ .set('src', MAAS_config.uris.statics + 'img/spinner.gif');
4060+
4061+ // Set up the chart
4062+ this.chart = new Y.maas.nodes_chart.NodesChartWidget(
4063+ {node_id: 'chart', width: 300, stats: this.stats});
4064+
4065+ // Set up the hovers for changing the dashboard text
4066+ var events = [
4067+ {event: 'hover.offline.over', template: this.offline_template},
4068+ {event: 'hover.offline.out'},
4069+ {event: 'hover.added.over', template: this.added_template},
4070+ {event: 'hover.added.out'},
4071+ {event: 'hover.allocated.over', template: this.allocated_template},
4072+ {event: 'hover.allocated.out'},
4073+ {
4074+ event: 'hover.commissioned.over',
4075+ template: this.commissioned_template
4076+ },
4077+ {event: 'hover.commissioned.out'},
4078+ {event: 'hover.queued.over', template: this.queued_template},
4079+ {event: 'hover.queued.out'}
4080+ ];
4081+ Y.Array.each(events, function(event) {
4082+ this.chart.on(event.event, function(e, template, widget) {
4083+ if (Y.Lang.isValue(e.nodes)) {
4084+ widget.setSummary(true, e.nodes, template, true);
4085+ }
4086+ else {
4087+ // Set the text to the default
4088+ widget.setSummary(true);
4089+ }
4090+ }, null, event.template, this);
4091+ }, this);
4092+
4093+ var self = this; // JavaScript sucks.
4094+
4095+ // Wire up the model list to the chart and summary.
4096+ this.modelList.after("add", function(e) {
4097+ self.updateStatus('add', e.model.get("status"));
4098+ self.setSummary(self.loaded);
4099+ });
4100+ this.modelList.after("remove", function(e) {
4101+ self.updateStatus('remove', e.model.get("status"));
4102+ self.setSummary(self.loaded);
4103+ });
4104+ this.modelList.after("*:change", function(e) {
4105+ var status_change = e.changed.status;
4106+ if (Y.Lang.isValue(status_change)) {
4107+ self.updateStatus('remove', status_change.prevVal);
4108+ self.updateStatus('add', status_change.newVal);
4109+ }
4110+ });
4111+ this.modelList.after("reset", function(e) {
4112+ self.stats.reset();
4113+ self.modelList.each(function(model) {
4114+ self.updateStatus("add", model.get("status"));
4115+ });
4116+ self.render();
4117+ });
4118+ },
4119+
4120+ /**
4121+ * Display a dashboard of the nodes.
4122+ *
4123+ * @method render
4124+ */
4125+ render: function () {
4126+ // Set the default text on the dashboard
4127+ this.setSummary(this.loaded);
4128+ this.setNodeText(
4129+ this.reservedNode, this.reserved_template,
4130+ this.stats.get("reserved"));
4131+ this.setNodeText(
4132+ this.retiredNode, this.retired_template,
4133+ this.stats.get("retired"));
4134+ },
4135+
4136+ loadNodesStarted: function() {
4137+ this.srcNode.insert(this.spinnerNode, 0);
4138+ },
4139+
4140+ loadNodesEnded: function() {
4141+ this.spinnerNode.remove();
4142+ },
4143+
4144+ /**
4145+ * Update the number of nodes for a status, one node at a time.
4146+ *
4147+ * @method updateStatus
4148+ */
4149+ updateStatus: function(action, status) {
4150+ var node_counter = 0;
4151+
4152+ /* This seems like an ugly way to calculate the change, but it stops
4153+ duplication of checking for the action for each status.
4154+ */
4155+ if (action === 'add') {
4156+ node_counter = 1;
4157+ }
4158+ else if (action === 'remove') {
4159+ node_counter = -1;
4160+ }
4161+
4162+ switch (status) {
4163+ case NODE_STATUS.NEW:
4164+ // Added nodes
4165+ this.stats.update("added", node_counter);
4166+ break;
4167+ case NODE_STATUS.COMMISSIONING:
4168+ case NODE_STATUS.FAILED_COMMISSIONING:
4169+ case NODE_STATUS.MISSING:
4170+ // Offline nodes
4171+ this.stats.update("offline", node_counter);
4172+ break;
4173+ case NODE_STATUS.READY:
4174+ // Queued nodes
4175+ this.stats.update("queued", node_counter);
4176+ break;
4177+ case NODE_STATUS.RESERVED:
4178+ // Reserved nodes
4179+ this.setNodeText(
4180+ this.reservedNode, this.reserved_template,
4181+ this.stats.update("reserved", node_counter));
4182+ break;
4183+ case NODE_STATUS.ALLOCATED:
4184+ case NODE_STATUS.DEPLOYING:
4185+ case NODE_STATUS.DEPLOYED:
4186+ // Allocated/Deploying/Deployed nodes
4187+ this.stats.update("allocated", node_counter);
4188+ break;
4189+ case NODE_STATUS.RETIRED:
4190+ // Retired nodes
4191+ this.setNodeText(
4192+ this.retiredNode, this.retired_template,
4193+ this.stats.update("retired", node_counter));
4194+ break;
4195+ }
4196+ },
4197+
4198+ /**
4199+ * Set the text for the number of nodes for a status.
4200+ */
4201+ setSummary: function(animate, nodes, template) {
4202+ // By default we just want to display the total nodes.
4203+ if (!nodes || !template) {
4204+ nodes = this.getNodeCount();
4205+ template = this.all_template;
4206+ }
4207+ var plural = (nodes === 1) ? '' : 's';
4208+ var text = Y.Lang.sub(template, {plural: plural});
4209+
4210+ if (animate) {
4211+ this.fade_out.on('end', function (e, self, nodes, text) {
4212+ self.numberNode.setContent(nodes);
4213+ self.descriptionNode.setContent(text);
4214+ self.fade_in.run();
4215+ }, null, this, nodes, text);
4216+ this.fade_out.run();
4217+ }
4218+ else {
4219+ this.numberNode.setContent(nodes);
4220+ this.descriptionNode.setContent(text);
4221+ }
4222+ },
4223+
4224+ /**
4225+ * Set the text from a template for a DOM node.
4226+ */
4227+ setNodeText: function(element, template, nodes) {
4228+ var plural = (nodes === 1) ? '' : 's';
4229+ var text = Y.Lang.sub(template, {plural: plural, nodes: nodes});
4230+ element.setContent(text);
4231+ },
4232+
4233+ /**
4234+ * Get the number of nodes (excluding retired).
4235+ */
4236+ getNodeCount: function() {
4237+ return Y.Array.filter(this.modelList.toArray(), function (model) {
4238+ return model.get('status') !== NODE_STATUS.RETIRED;
4239+ }).length;
4240+ }
4241+});
4242+
4243+/**
4244+ * A customized view based on NodeListLoader that will reload the node
4245+ * information in the table provided in srcNode.
4246+ *
4247+ * This should only be used on an element type of "table". It also requires
4248+ * that the table contain a "tbody" as that is used to identify the "tr".
4249+ *
4250+ * This view uses data-* attributes to update the elements in the view. The
4251+ * data-field attribute sets which attribute on the node object should be bound
4252+ * to that element. By default when the node listing is updated, that element's
4253+ * text will be set to that value. Two modifiers exist that allow the
4254+ * modification of this behaviour. data-field-attr changes which attribute the
4255+ * value from the object should be set on that attribute. data-field-class
4256+ * allows the ability to set a class using the value from the node object.
4257+ * data-field-class uses a prefix to identify the previous class on the element
4258+ * before the value of the node object changed (e.g. "power-").
4259+ */
4260+module.NodesTableReloader = Y.Base.create('nodesTableReloader', Y.View, [], {
4261+
4262+ initializer: function(config) {
4263+ this.srcNode = Y.one(config.srcNode);
4264+ },
4265+
4266+ /**
4267+ * Add a loader, a Y.IO object. Events fired by this IO object will
4268+ * be followed, and will drive updates to this object's model.
4269+ *
4270+ * @method addLoader
4271+ */
4272+ addLoader: function(loader) {
4273+ loader.on("io:success", function(id, request) {
4274+ this.loadNodes(request.responseText);
4275+ }, this);
4276+ },
4277+
4278+ /**
4279+ * Load the nodes from the given data.
4280+ *
4281+ * @method loadNodes
4282+ */
4283+ loadNodes: function(data) {
4284+ try {
4285+ var nodes = JSON.parse(data);
4286+ this.nodes = nodes;
4287+ }
4288+ catch(e) {
4289+ console.log("Failed to decode node listing JSON data.");
4290+ return;
4291+ }
4292+ // Record that at least one load has been done.
4293+ this.loaded = true;
4294+ this.render();
4295+ },
4296+
4297+ /**
4298+ * Update the contents in the srcNode, based on the data attributes.
4299+ *
4300+ * @method render
4301+ */
4302+ render: function () {
4303+ var self = this;
4304+ var tbody = this.srcNode.one('tbody');
4305+ Y.Array.each(this.nodes, function(node) {
4306+ var row = self.getRowWithSystemId(tbody.all('tr'), node.system_id);
4307+ if (Y.Lang.isValue(row)) {
4308+ var elements = row.all('[data-field]');
4309+ Y.maas.utils.updateElements(elements, node);
4310+ }
4311+ });
4312+ },
4313+
4314+ /**
4315+ * Return the "tr" that contains the data-system-id="system_id".
4316+ *
4317+ * This is needed for Firefox, as it fails to return a "tr" when using
4318+ * [data-system-id="system_id"] selector.
4319+ *
4320+ * @method getRowWithSystemId
4321+ */
4322+ getRowWithSystemId: function(rows, system_id) {
4323+ var foundRow = null;
4324+ rows.each(function(row) {
4325+ if (Y.Lang.isValue(foundRow)) {
4326+ // Alread found the row.
4327+ return;
4328+ }
4329+ var sysid = Y.one(row).getData('system-id');
4330+ if (Y.Lang.isValue(sysid) && sysid === system_id) {
4331+ foundRow = row;
4332+ }
4333+ });
4334+ return foundRow;
4335+ },
4336+
4337+ /**
4338+ * Return list of node system_ids in the table.
4339+ *
4340+ * @method getNodesList
4341+ */
4342+ getNodesList: function () {
4343+ var rows = this.srcNode.one('tbody').all('tr');
4344+ var system_ids = [];
4345+ rows.each(function(row) {
4346+ var id = row.getData('system-id');
4347+ if (Y.Lang.isValue(id)) {
4348+ system_ids.push(id);
4349+ }
4350+ });
4351+ return system_ids;
4352+ }
4353+});
4354+
4355+
4356+// Header for the event table that is rendered by NodeViewReloader.
4357+var EVENT_TABLE_HEADER =
4358+ '<thead>' +
4359+ '<tr>' +
4360+ '<th width="10%">Level</th>' +
4361+ '<th width="30%">Emitted</th>' +
4362+ '<th>Event</th>' +
4363+ '</tr>' +
4364+ '</thead>';
4365+
4366+/**
4367+ * View that will reload information on the node view page, render the
4368+ * event log, and update the sidebar.
4369+ *
4370+ * This view uses data-* attributes to update the elements in the view. See
4371+ * js/utils.js:updateElements for complete description of data-* attributes.
4372+ */
4373+module.NodeView = Y.Base.create('nodeView', Y.View, [], {
4374+
4375+ initializer: function(config) {
4376+ this.srcNode = Y.one(config.srcNode);
4377+ this.eventList = Y.one(config.eventList);
4378+ this.actionView = Y.one(config.actionView);
4379+ this.powerChecker = new Y.maas.node_check.PowerCheckWidget({
4380+ srcNode: config.actionView,
4381+ system_id: config.system_id
4382+ });
4383+ },
4384+
4385+ /**
4386+ * Add a loader, a Y.IO object. Events fired by this IO object will
4387+ * be followed, and will drive updates to this object's model.
4388+ *
4389+ * @method addLoader
4390+ */
4391+ addLoader: function(loader) {
4392+ loader.on("io:success", function(id, request) {
4393+ this.loadNode(request.responseText);
4394+ }, this);
4395+ },
4396+
4397+ /**
4398+ * Load the node from the given data.
4399+ *
4400+ * @method loadNode
4401+ */
4402+ loadNode: function(data) {
4403+ try {
4404+ var node = JSON.parse(data);
4405+ this.node = node;
4406+ }
4407+ catch(e) {
4408+ console.log("Failed to decode node JSON data.");
4409+ return;
4410+ }
4411+ // Record that at least one load has been done.
4412+ this.loaded = true;
4413+ this.render();
4414+ },
4415+
4416+ /**
4417+ * Update all of the elements with data-field attributes with the
4418+ * attributes on the given node.
4419+ *
4420+ * @method render
4421+ */
4422+ render: function () {
4423+ var elements = this.srcNode.all('[data-field]');
4424+ Y.maas.utils.updateElements(elements, this.node);
4425+ this.renderEventList();
4426+ this.renderActionView();
4427+ this.powerChecker.render();
4428+ },
4429+
4430+ /**
4431+ * Render the event listing table, with the most recent events.
4432+ *
4433+ * @method renderEventList
4434+ */
4435+ renderEventList: function() {
4436+ var self = this;
4437+ var events = this.node.events;
4438+
4439+ // Clear the old table.
4440+ this.eventList.get('childNodes').remove();
4441+
4442+ // If no events add "No events" div.
4443+ if (!Y.Lang.isValue(events) || events.count === 0) {
4444+ this.eventList.append(
4445+ Y.Node.create('<div />').set('text', 'No events'));
4446+ return;
4447+ }
4448+
4449+ // Create the new table.
4450+ var table = Y.Node.create('<table />');
4451+ table.addClass('list');
4452+ table.append(Y.Node.create(EVENT_TABLE_HEADER));
4453+
4454+ // Create the tbody, inserting each row.
4455+ var tbody = Y.Node.create('<tbody />');
4456+ Y.Array.each(this.node.events.events, function(evt) {
4457+ tbody.append(self.createEventRow(evt));
4458+ });
4459+ table.append(tbody);
4460+
4461+ // Add the table to the dom.
4462+ this.eventList.append(table);
4463+
4464+ // Add the link to more event data.
4465+ if (events.count < events.total) {
4466+ var link = Y.Node.create('<a />')
4467+ .set('href', events.more_url)
4468+ .set(
4469+ 'text',
4470+ 'Full node event log (' + events.total + ' events).');
4471+ this.eventList.append(link);
4472+ }
4473+ },
4474+
4475+ /**
4476+ * Render the event table row for the given event.
4477+ *
4478+ * @method createEventRow
4479+ */
4480+ createEventRow: function(evt) {
4481+ // Create the row, setting the id and class based on level.
4482+ var tr = Y.Node.create('<tr />');
4483+ tr.set('id', 'node-event-' + evt.id);
4484+ tr.addClass(evt.level.toLowerCase());
4485+
4486+ // Add the data to the row
4487+ tr.append(
4488+ Y.Node.create('<td />')
4489+ .set('text', evt.level));
4490+ tr.append(Y.Node.create('<td />')
4491+ .set('text', evt.created));
4492+ var rowText = evt.type;
4493+ if (Y.Lang.isString(evt.description) && evt.description.length > 0) {
4494+ rowText += ' &mdash; ' + evt.description;
4495+ }
4496+ tr.append(
4497+ Y.Node.create('<td />')
4498+ .setHTML(rowText));
4499+ return tr;
4500+ },
4501+
4502+ /**
4503+ * Render the updated action view.
4504+ *
4505+ * @method renderActionView
4506+ */
4507+ renderActionView: function() {
4508+ // Clear the old action view.
4509+ this.actionView.get('childNodes').remove();
4510+
4511+ // Add the updated action view.
4512+ this.actionView.append(Y.Node.create(this.node.action_view));
4513+ }
4514+});
4515+
4516+}, '0.1', {'requires': [
4517+ 'view', 'io', 'maas.enums', 'maas.node', 'maas.node_add',
4518+ 'maas.nodes_chart', 'maas.node_check', 'maas.morph', 'maas.utils', 'anim']}
4519+);
4520
4521=== added file 'src/maasserver/static/js/tests/test_node_check.html.OTHER'
4522--- src/maasserver/static/js/tests/test_node_check.html.OTHER 1970-01-01 00:00:00 +0000
4523+++ src/maasserver/static/js/tests/test_node_check.html.OTHER 2015-07-08 00:07:16 +0000
4524@@ -0,0 +1,36 @@
4525+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4526+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4527+ <head>
4528+ <title>Test maas.node_check</title>
4529+
4530+ <!-- YUI and test setup -->
4531+ <script type="text/javascript" src="../testing/yui_test_conf.js"></script>
4532+ <script type="text/javascript" src="/usr/share/javascript/yui3/yui/yui.js"></script>
4533+ <script type="text/javascript" src="../testing/testrunner.js"></script>
4534+ <script type="text/javascript" src="../testing/testing.js"></script>
4535+ <!-- The module under test -->
4536+ <script type="text/javascript" src="../node_check.js"></script>
4537+ <!-- The test suite -->
4538+ <script type="text/javascript" src="test_node_check.js"></script>
4539+ <script type="text/javascript">
4540+ <!--
4541+ var MAAS_config = {
4542+ uris: {
4543+ statics: '/static/',
4544+ nodes_handler: '/api/nodes/'
4545+ }
4546+ };
4547+ // -->
4548+ </script>
4549+ </head>
4550+ <body>
4551+ <span id="suite">maas.node_check.tests</span>
4552+ <script type="text/x-template" id="template">
4553+ <div id="placeholder">
4554+ <form id="node_actions">
4555+ </form>
4556+ </div>
4557+ </script>
4558+
4559+ </body>
4560+</html>
4561
4562=== added file 'src/maasserver/static/js/tests/test_node_check.js.OTHER'
4563--- src/maasserver/static/js/tests/test_node_check.js.OTHER 1970-01-01 00:00:00 +0000
4564+++ src/maasserver/static/js/tests/test_node_check.js.OTHER 2015-07-08 00:07:16 +0000
4565@@ -0,0 +1,128 @@
4566+/* Copyright 2014 Canonical Ltd. This software is licensed under the
4567+ * GNU Affero General Public License version 3 (see the file LICENSE).
4568+ */
4569+
4570+YUI({ useBrowserConsole: true }).add('maas.node_check.tests', function(Y) {
4571+
4572+Y.log('loading maas.node_check.tests');
4573+var namespace = Y.namespace('maas.node_check.tests');
4574+
4575+var module = Y.maas.node_check;
4576+var suite = new Y.Test.Suite("maas.node_check Tests");
4577+
4578+var template = Y.one('#template').getContent();
4579+
4580+suite.add(new Y.maas.testing.TestCase({
4581+ name: 'test-node_check',
4582+
4583+ setUp: function() {
4584+ Y.one("body").append(Y.Node.create(template));
4585+ },
4586+
4587+ createWidget: function(system_id) {
4588+ var selector = '#placeholder';
4589+ var widget = new module.PowerCheckWidget(
4590+ {srcNode: selector, system_id: system_id});
4591+ this.addCleanup(function() { widget.destroy(); });
4592+ return widget;
4593+ },
4594+
4595+ testInitializer: function() {
4596+ var widget = this.createWidget('system_id');
4597+ widget.render();
4598+ // The placeholders for errors and status have been created.
4599+ var error_msg = Y.one('#placeholder').one('div.power-check-error');
4600+ var status_check = Y.one('#placeholder').one('div.power-check-ok');
4601+ Y.Assert.isNotNull(error_msg);
4602+ Y.Assert.isNotNull(status_check);
4603+ },
4604+
4605+ testClickPowerCheckCall: function() {
4606+ // A click on the button calls the API to query the power state.
4607+ var log = this.logIO(module);
4608+ var widget = this.createWidget('system_id');
4609+ widget.render();
4610+ var button = widget.button;
4611+ button.simulate('click');
4612+ Y.Assert.areEqual(1, log.length);
4613+ var request_info = log.pop();
4614+ Y.Assert.areEqual(
4615+ MAAS_config.uris.nodes_handler + 'system_id', request_info[0]);
4616+ Y.Assert.areEqual(
4617+ "op=query_power_state", request_info[1].data);
4618+ },
4619+
4620+ testPowerCheckDisplaysOnResults: function() {
4621+ // If the API call to check the power state returns
4622+ // 'on', this is considered a success.
4623+ var response = {
4624+ state: 'on'
4625+ };
4626+ var log = this.mockSuccess(Y.JSON.stringify(response), module);
4627+ var widget = this.createWidget('system_id');
4628+ widget.render();
4629+ var button = widget.button;
4630+ button.simulate('click');
4631+ Y.Assert.areEqual("", widget.get('error_text'));
4632+ Y.Assert.areEqual(
4633+ "Success: node is on.",
4634+ widget.get('status_text'));
4635+ },
4636+
4637+ testPowerCheckDisplaysOffResults: function() {
4638+ // If the API call to check the power state returns
4639+ // 'off', this is considered a success.
4640+ var response = {
4641+ state: 'off'
4642+ };
4643+ var log = this.mockSuccess(Y.JSON.stringify(response), module);
4644+ var widget = this.createWidget('system_id');
4645+ widget.render();
4646+ var button = widget.button;
4647+ button.simulate('click');
4648+ Y.Assert.areEqual(1, log.length);
4649+ Y.Assert.areEqual("", widget.get('error_text'));
4650+ Y.Assert.areEqual(
4651+ "Success: node is off.",
4652+ widget.get('status_text'));
4653+ },
4654+
4655+ testPowerCheckErrorDisplaysErrors: function() {
4656+ // If the API call to check the power state errors, the error is
4657+ // displayed.
4658+ this.mockFailure('error message', module, 500);
4659+ var widget = this.createWidget('system_id');
4660+ widget.render();
4661+ var button = widget.button;
4662+ button.simulate('click');
4663+ Y.Assert.areEqual(
4664+ "Error: error message.",
4665+ widget.get('error_text'));
4666+ Y.Assert.areEqual("", widget.get('status_text'));
4667+ },
4668+
4669+ testPowerCheckDisplaysUnknownResults: function() {
4670+ // If the API call to check the power state returns
4671+ // something different than 'on' or 'off',
4672+ // this is considered an error.
4673+ var response = {
4674+ state: 'unknown error'
4675+ };
4676+ var log = this.mockSuccess(Y.JSON.stringify(response), module);
4677+ var widget = this.createWidget('system_id');
4678+ widget.render();
4679+ var button = widget.button;
4680+ button.simulate('click');
4681+ Y.Assert.areEqual(1, log.length);
4682+ Y.Assert.areEqual(
4683+ "Error: unknown error.", widget.get('error_text'));
4684+ Y.Assert.areEqual("", widget.get('status_text'));
4685+ }
4686+
4687+}));
4688+
4689+namespace.suite = suite;
4690+
4691+}, '0.1', {'requires': [
4692+ 'node-event-simulate', 'node', 'test', 'maas.testing', 'maas.node_check']}
4693+);
4694
4695=== added file 'src/maasserver/static/js/tests/test_node_views.html.OTHER'
4696--- src/maasserver/static/js/tests/test_node_views.html.OTHER 1970-01-01 00:00:00 +0000
4697+++ src/maasserver/static/js/tests/test_node_views.html.OTHER 2015-07-08 00:07:16 +0000
4698@@ -0,0 +1,55 @@
4699+<!DOCTYPE html>
4700+<html>
4701+ <head>
4702+ <title>Test maas.node_views</title>
4703+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></meta>
4704+ <!-- YUI and test setup -->
4705+ <script type="text/javascript" src="/usr/share/javascript/raphael/raphael-min.js"></script>
4706+ <script type="text/javascript" src="../testing/yui_test_conf.js"></script>
4707+ <script type="text/javascript" src="/usr/share/javascript/yui3/yui/yui.js"></script>
4708+ <script type="text/javascript" src="../testing/testrunner.js"></script>
4709+ <script type="text/javascript" src="../testing/testing.js"></script>
4710+ <script type="text/javascript" src="../enums.js"></script>
4711+ <script type="text/javascript" src="../utils.js"></script>
4712+ <script type="text/javascript" src="../license_key.js"></script>
4713+ <script type="text/javascript" src="../morph.js"></script>
4714+ <script type="text/javascript" src="../node.js"></script>
4715+ <script type="text/javascript" src="../node_add.js"></script>
4716+ <script type="text/javascript" src="../nodes_chart.js"></script>
4717+ <script type="text/javascript" src="../node_check.js"></script>
4718+ <script type="text/javascript" src="../os_distro_select.js"></script>
4719+ <script type="text/javascript" src="../power_parameters.js"></script>
4720+ <!-- The module under test -->
4721+ <script type="text/javascript" src="../node_views.js"></script>
4722+ <!-- The test suite -->
4723+ <script type="text/javascript" src="test_node_views.js"></script>
4724+ <script type="text/javascript">
4725+ <!--
4726+ var MAAS_config = {
4727+ uris: {
4728+ statics: '../../',
4729+ nodes_handler: '/api/nodes/'
4730+ }
4731+ };
4732+ // -->
4733+ </script>
4734+ </head>
4735+ <body>
4736+ <span id="suite">maas.node_views.tests</span>
4737+
4738+ <!-- Reusable template: nodes for the dashboard to hook into. -->
4739+ <script type="text/x-template" id="dashboard-hooks">
4740+ <div id="chart"></div>
4741+ <div id="summary">
4742+ <h2 id="nodes-number"></h2>
4743+ <p id="nodes-description"></p>
4744+ </div>
4745+ <p id="reserved-nodes"></p>
4746+ <p id="retired-nodes"></p>
4747+ </script>
4748+
4749+ <!-- Place for tests to hook instances of the template in the DOM. -->
4750+ <div id="placeholder"></div>
4751+
4752+ </body>
4753+</html>
4754
4755=== added file 'src/maasserver/static/js/tests/test_node_views.js.OTHER'
4756--- src/maasserver/static/js/tests/test_node_views.js.OTHER 1970-01-01 00:00:00 +0000
4757+++ src/maasserver/static/js/tests/test_node_views.js.OTHER 2015-07-08 00:07:16 +0000
4758@@ -0,0 +1,1035 @@
4759+/* Copyright 2012 Canonical Ltd. This software is licensed under the
4760+ * GNU Affero General Public License version 3 (see the file LICENSE).
4761+ */
4762+
4763+YUI({ useBrowserConsole: true }).add('maas.node_views.tests', function(Y) {
4764+
4765+Y.log('loading maas.node_views.tests');
4766+var namespace = Y.namespace('maas.node_views.tests');
4767+
4768+var module = Y.maas.node_views;
4769+var suite = new Y.Test.Suite("maas.node_views Tests");
4770+
4771+
4772+// Dump this HTML into #placeholder to get DOM hooks for the dashboard.
4773+var dashboard_hooks = Y.one('#dashboard-hooks').getContent();
4774+
4775+
4776+suite.add(new Y.maas.testing.TestCase({
4777+ name: 'test-node-views-NodeListLoader',
4778+
4779+ exampleNodes: [
4780+ {system_id: '3', hostname: 'dan'},
4781+ {system_id: '4', hostname: 'dee'}
4782+ ],
4783+
4784+ makeNodeListLoader: function() {
4785+ var view = new Y.maas.node_views.NodeListLoader();
4786+ this.addCleanup(Y.bind(view.destroy, view));
4787+ return view;
4788+ },
4789+
4790+ testInitialization: function() {
4791+ var view = this.makeNodeListLoader();
4792+ Y.Assert.areEqual('nodeList', view.modelList.name);
4793+ },
4794+
4795+ testRenderDoesNotCallLoad: function() {
4796+ // The initial call to .render() does *not* trigger the loading
4797+ // of the nodes.
4798+ var self = this;
4799+
4800+ var mockXhr = Y.Mock();
4801+ Y.Mock.expect(mockXhr, {
4802+ method: 'send',
4803+ args: [MAAS_config.uris.nodes_handler, Y.Mock.Value.Any],
4804+ run: function(uri, cfg) {
4805+ var out = new Y.Base();
4806+ out.response = Y.JSON.stringify(self.exampleNodes);
4807+ cfg.on.success(Y.guid(), out);
4808+ }
4809+ });
4810+ this.mockIO(mockXhr, module);
4811+
4812+ var view = this.makeNodeListLoader();
4813+
4814+ view.render();
4815+ // The model list has not been populated.
4816+ Y.Assert.areEqual(0, view.modelList.size());
4817+ },
4818+
4819+ testAddLoader: function() {
4820+ // A mock loader.
4821+ var loader = new Y.Base();
4822+
4823+ // Capture event registrations.
4824+ var events = {};
4825+ loader.on = function(event, callback) {
4826+ events[event] = callback;
4827+ };
4828+
4829+ var view = this.makeNodeListLoader();
4830+ view.addLoader(loader);
4831+
4832+ // Several events are registered.
4833+ Y.Assert.areSame(view.loadNodesStarted, events["io:start"]);
4834+ Y.Assert.areSame(view.loadNodesEnded, events["io:end"]);
4835+ Y.Assert.areSame(view.loadNodesFailed, events["io:failure"]);
4836+ Y.Assert.isFunction(events["io:success"]);
4837+ },
4838+
4839+ testLoadNodes: function() {
4840+ var response = Y.JSON.stringify(this.exampleNodes);
4841+ var view = this.makeNodeListLoader();
4842+ view.loadNodes(response);
4843+ Y.Assert.isTrue(view.loaded);
4844+ Y.Assert.areEqual(2, view.modelList.size());
4845+ Y.Assert.areEqual('dan', view.modelList.item(0).get('hostname'));
4846+ Y.Assert.areEqual('dee', view.modelList.item(1).get('hostname'));
4847+ },
4848+
4849+ testLoadNodes_invalid_data: function() {
4850+ var response = "{garbled data}";
4851+ var view = this.makeNodeListLoader();
4852+
4853+ var loadNodesFailedCalled = false;
4854+ view.loadNodesFailed = function() {
4855+ loadNodesFailedCalled = true;
4856+ };
4857+
4858+ view.loadNodes(response);
4859+ Y.Assert.isTrue(view.loaded);
4860+ Y.Assert.areEqual(0, view.modelList.size());
4861+ Y.Assert.isTrue(loadNodesFailedCalled);
4862+ },
4863+
4864+ assertModelListMatchesNodes: function(modelList, nodes) {
4865+ Y.Assert.areEqual(nodes.length, modelList.size());
4866+ Y.Array.each(nodes, function(node) {
4867+ var model = modelList.getById(node.system_id);
4868+ Y.Assert.isObject(model);
4869+ Y.Assert.areEqual(node.hostname, model.get("hostname"));
4870+ Y.Assert.areEqual(node.status, model.get("status"));
4871+ });
4872+ },
4873+
4874+ test_mergeNodes_when_modelList_is_empty: function() {
4875+ var view = this.makeNodeListLoader();
4876+ var nodes = [
4877+ {system_id: "1", hostname: "host1", status: 1},
4878+ {system_id: "2", hostname: "host2", status: 2},
4879+ {system_id: "3", hostname: "host3", status: 3}
4880+ ];
4881+ Y.Assert.areEqual(0, view.modelList.size());
4882+ view.mergeNodes(nodes);
4883+ this.assertModelListMatchesNodes(view.modelList, nodes);
4884+ },
4885+
4886+ test_mergeNodes_when_modelList_is_not_empty: function() {
4887+ var view = this.makeNodeListLoader();
4888+ var nodes_before = [
4889+ {system_id: "1", hostname: "host1", status: 1},
4890+ {system_id: "3", hostname: "host3", status: 3}
4891+ ];
4892+ var nodes_after = [
4893+ {system_id: "1", hostname: "host1after", status: 11},
4894+ {system_id: "2", hostname: "host2after", status: 22}
4895+ ];
4896+ view.mergeNodes(nodes_before);
4897+ this.assertModelListMatchesNodes(view.modelList, nodes_before);
4898+ view.mergeNodes(nodes_after);
4899+ this.assertModelListMatchesNodes(view.modelList, nodes_after);
4900+ },
4901+
4902+ test_mergeNodes_events: function() {
4903+ var view = this.makeNodeListLoader();
4904+ var events = [];
4905+ view.modelList.after(
4906+ ["add", "remove", "*:change", "reset"],
4907+ Y.bind(events.push, events));
4908+
4909+ var getEventType = function(e) { return e.type; };
4910+
4911+ var nodes_before = [
4912+ {system_id: "1", hostname: "host1", status: 1},
4913+ {system_id: "3", hostname: "host3", status: 3}
4914+ ];
4915+ view.mergeNodes(nodes_before);
4916+ Y.ArrayAssert.itemsAreSame(
4917+ ["nodeList:add", "nodeList:add"],
4918+ Y.Array.map(events, getEventType));
4919+
4920+ var nodes_after = [
4921+ {system_id: "1", hostname: "host1after", status: 11},
4922+ {system_id: "2", hostname: "host2after", status: 22}
4923+ ];
4924+ view.mergeNodes(nodes_after);
4925+ Y.ArrayAssert.itemsAreSame(
4926+ ["nodeList:add", "nodeList:add", "nodeModel:change",
4927+ "nodeList:add", "nodeList:remove"],
4928+ Y.Array.map(events, getEventType));
4929+ }
4930+
4931+}));
4932+
4933+suite.add(new Y.maas.testing.TestCase({
4934+ name: 'test-node-views-NodeDashBoard',
4935+
4936+ setUp : function () {
4937+ Y.one('#placeholder').empty();
4938+ var NODE_STATUS = Y.maas.enums.NODE_STATUS;
4939+ this.data = [
4940+ {
4941+ system_id: 'sys1',
4942+ hostname: 'host1',
4943+ status: NODE_STATUS.NEW
4944+ },
4945+ {
4946+ system_id: 'sys2',
4947+ hostname: 'host2',
4948+ status: NODE_STATUS.NEW
4949+ },
4950+ {
4951+ system_id: 'sys3',
4952+ hostname: 'host3',
4953+ status: NODE_STATUS.COMMISSIONING
4954+ },
4955+ {
4956+ system_id: 'sys4',
4957+ hostname: 'host4',
4958+ status: NODE_STATUS.FAILED_COMMISSIONING
4959+ },
4960+ {
4961+ system_id: 'sys5',
4962+ hostname: 'host5',
4963+ status: NODE_STATUS.FAILED_COMMISSIONING
4964+ },
4965+ {
4966+ system_id: 'sys6',
4967+ hostname: 'host6',
4968+ status: NODE_STATUS.MISSING
4969+ },
4970+ {
4971+ system_id: 'sys7',
4972+ hostname: 'host7',
4973+ status: NODE_STATUS.READY
4974+ },
4975+ {
4976+ system_id: 'sys8',
4977+ hostname: 'host8',
4978+ status: NODE_STATUS.READY
4979+ },
4980+ {
4981+ system_id: 'sys9',
4982+ hostname: 'host9',
4983+ status: NODE_STATUS.RESERVED
4984+ },
4985+ {
4986+ system_id: 'sys10',
4987+ hostname: 'host10',
4988+ status: NODE_STATUS.RESERVED
4989+ },
4990+ {
4991+ system_id: 'sys11',
4992+ hostname: 'host11',
4993+ status: NODE_STATUS.RESERVED
4994+ },
4995+ {
4996+ system_id: 'sys12',
4997+ hostname: 'host12',
4998+ status: NODE_STATUS.ALLOCATED
4999+ },
5000+ {
The diff has been truncated for viewing.