Merge lp:~lutostag/ubuntu/trusty/maas/1.5.4 into lp:ubuntu/trusty-proposed/maas
- Trusty (14.04)
- 1.5.4
- Merge into trusty-proposed
Proposed by
Greg Lutostanski
Status: | Merged |
---|---|
Merged at revision: | 65 |
Proposed branch: | lp:~lutostag/ubuntu/trusty/maas/1.5.4 |
Merge into: | lp:ubuntu/trusty-proposed/maas |
Diff against target: |
2056 lines (+1398/-41) 42 files modified
debian/changelog (+39/-0) debian/control (+2/-1) debian/extras/99-maas-sudoers (+1/-0) debian/maas-cluster-controller.postinst (+1/-0) docs/_templates/maas/layout.html (+6/-0) docs/_templates/maas/static/css/main.css (+1/-1) docs/_templates/maas/static/flasky.css_t (+6/-1) docs/changelog.rst (+31/-0) etc/maas/templates/commissioning-user-data/snippets/maas_api_helper.py (+2/-1) etc/maas/templates/dhcp/dhcpd.conf.template (+1/-0) etc/maas/templates/power/mscm.template (+15/-0) etc/maas/templates/pxe/config.commissioning.ppc64el.template (+6/-0) etc/maas/templates/pxe/config.install.ppc64el.template (+6/-0) src/apiclient/maas_client.py (+3/-1) src/maasserver/api.py (+26/-0) src/maasserver/enum.py (+4/-6) src/maasserver/models/nodegroup.py (+11/-0) src/maasserver/models/tests/test_node.py (+2/-2) src/maasserver/rpc/regionservice.py (+1/-1) src/maasserver/rpc/tests/test_regionservice.py (+2/-4) src/maasserver/start_up.py (+7/-3) src/maasserver/testing/tests/test_rabbit.py (+2/-1) src/maasserver/tests/test_api_nodegroup.py (+26/-0) src/maasserver/utils/dblocks.py (+31/-2) src/maasserver/utils/tests/test_dblocks.py (+18/-1) src/maastesting/factory.py (+9/-0) src/metadataserver/address.py (+2/-2) src/metadataserver/tests/test_address.py (+40/-0) src/provisioningserver/boot/__init__.py (+6/-0) src/provisioningserver/boot/powernv.py (+158/-0) src/provisioningserver/boot/tests/test_powernv.py (+337/-0) src/provisioningserver/dhcp/config.py (+24/-11) src/provisioningserver/driver/__init__.py (+14/-1) src/provisioningserver/drivers/hardware/mscm.py (+187/-0) src/provisioningserver/drivers/hardware/tests/test_mscm.py (+259/-0) src/provisioningserver/power/tests/test_poweraction.py (+11/-0) src/provisioningserver/power_schema.py (+13/-0) src/provisioningserver/tasks.py (+9/-1) src/provisioningserver/tests/test_tasks.py (+12/-0) src/provisioningserver/utils/__init__.py (+21/-0) src/provisioningserver/utils/tests/test_utils.py (+45/-0) versions.cfg (+1/-1) |
To merge this branch: | bzr merge lp:~lutostag/ubuntu/trusty/maas/1.5.4 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Chuck Short (community) | Approve | ||
Review via email: mp+232757@code.launchpad.net |
Commit message
Description of the change
Proposed update to bring maas bugfix release 1.5.4 into trusty via SRU.
Builds and installs fine.
To post a comment you must log in.
Revision history for this message
Chuck Short (zulcss) : | # |
review:
Approve
Revision history for this message
Greg Lutostanski (lutostag) wrote : | # |
Revision history for this message
Greg Lutostanski (lutostag) wrote : | # |
New changelog that clearly shows supported releases for install.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'debian/changelog' | |||
2 | --- debian/changelog 2014-06-20 10:10:47 +0000 | |||
3 | +++ debian/changelog 2014-08-29 19:22:35 +0000 | |||
4 | @@ -1,3 +1,42 @@ | |||
5 | 1 | maas (1.5.4+bzr2294-0ubuntu1) trusty-proposed; urgency=medium | ||
6 | 2 | |||
7 | 3 | * New upstream bug fix release: | ||
8 | 4 | - Package fails to install when the default route is through an | ||
9 | 5 | aliased/tagged interface (LP: #1350235) | ||
10 | 6 | - ERROR Nonce already used (LP: #1190986) | ||
11 | 7 | - Add MAAS arm64/xgene support (LP: #1338851) | ||
12 | 8 | - Add utopic support (LP: #1337437) | ||
13 | 9 | - API documentation for nodegroup op=details missing parameter | ||
14 | 10 | (LP: #1331982) | ||
15 | 11 | - Reduce number of celery tasks emitted when updating a cluster controller | ||
16 | 12 | (LP: #1324944) | ||
17 | 13 | - Fix VirshSSH template which was referencing invalid attributes | ||
18 | 14 | (LP: #1324966) | ||
19 | 15 | - Fix a start up problems where a database lock was being taken outside of | ||
20 | 16 | a transaction (LP: #1325640, LP: #1325759) | ||
21 | 17 | - Reformat badly formatted Architecture error message (LP: #1301465) | ||
22 | 18 | - Final changes to support ppc64el (now known as PowerNV) (LP: #1315154) | ||
23 | 19 | - UI tweak to make navigation elements visible for documentation | ||
24 | 20 | |||
25 | 21 | [ Greg Lutostanski ] | ||
26 | 22 | * debian/control: | ||
27 | 23 | - maas-provisioningserver not maas-cluster-controller depends on | ||
28 | 24 | python-pexpect (LP: #1352273) | ||
29 | 25 | |||
30 | 26 | [ Gavin Panella ] | ||
31 | 27 | * debian/maas-cluster-controller.postinst | ||
32 | 28 | - Allow maas-pserv to bind to all IPv6 addresses too. (LP: #1342302) | ||
33 | 29 | |||
34 | 30 | [ Diogo Matsubara ] | ||
35 | 31 | * debian/control: | ||
36 | 32 | - python-maas-provisioningserver depends on python-paramiko (LP: #1334401) | ||
37 | 33 | |||
38 | 34 | [ Raphaƫl Badin ] | ||
39 | 35 | * debian/extras/99-maas-sudoers: | ||
40 | 36 | - Add rule 'maas-dhcp-server stop' job. | ||
41 | 37 | |||
42 | 38 | -- Greg Lutostanski <gregory.lutostanski@canonical.com> Fri, 29 Aug 2014 13:27:34 -0500 | ||
43 | 39 | |||
44 | 1 | maas (1.5.2+bzr2282-0ubuntu0.2) trusty-proposed; urgency=medium | 40 | maas (1.5.2+bzr2282-0ubuntu0.2) trusty-proposed; urgency=medium |
45 | 2 | 41 | ||
46 | 3 | * debian/control: | 42 | * debian/control: |
47 | 4 | 43 | ||
48 | === modified file 'debian/control' | |||
49 | --- debian/control 2014-06-20 10:10:47 +0000 | |||
50 | +++ debian/control 2014-08-29 19:22:35 +0000 | |||
51 | @@ -99,6 +99,8 @@ | |||
52 | 99 | python-oops-amqp, | 99 | python-oops-amqp, |
53 | 100 | python-oops-datedir-repo, | 100 | python-oops-datedir-repo, |
54 | 101 | python-oops-twisted, | 101 | python-oops-twisted, |
55 | 102 | python-paramiko, | ||
56 | 103 | python-pexpect, | ||
57 | 102 | python-pyparsing, | 104 | python-pyparsing, |
58 | 103 | python-seamicroclient, | 105 | python-seamicroclient, |
59 | 104 | python-simplestreams, | 106 | python-simplestreams, |
60 | @@ -159,7 +161,6 @@ | |||
61 | 159 | python-maas-provisioningserver (=${binary:Version}), | 161 | python-maas-provisioningserver (=${binary:Version}), |
62 | 160 | python-netaddr, | 162 | python-netaddr, |
63 | 161 | python-oauth, | 163 | python-oauth, |
64 | 162 | python-pexpect, | ||
65 | 163 | python-tempita, | 164 | python-tempita, |
66 | 164 | python-twisted, | 165 | python-twisted, |
67 | 165 | python-zope.interface, | 166 | python-zope.interface, |
68 | 166 | 167 | ||
69 | === modified file 'debian/extras/99-maas-sudoers' | |||
70 | --- debian/extras/99-maas-sudoers 2013-06-03 16:53:14 +0000 | |||
71 | +++ debian/extras/99-maas-sudoers 2014-08-29 19:22:35 +0000 | |||
72 | @@ -1,3 +1,4 @@ | |||
73 | 1 | maas ALL= NOPASSWD: /usr/sbin/service maas-dhcp-server restart | 1 | maas ALL= NOPASSWD: /usr/sbin/service maas-dhcp-server restart |
74 | 2 | maas ALL= NOPASSWD: /usr/sbin/service maas-dhcp-server stop | ||
75 | 2 | maas ALL= NOPASSWD: /usr/sbin/maas-provision | 3 | maas ALL= NOPASSWD: /usr/sbin/maas-provision |
76 | 3 | maas ALL= NOPASSWD: SETENV: /usr/sbin/maas-import-pxe-files | 4 | maas ALL= NOPASSWD: SETENV: /usr/sbin/maas-import-pxe-files |
77 | 4 | 5 | ||
78 | === modified file 'debian/maas-cluster-controller.postinst' | |||
79 | --- debian/maas-cluster-controller.postinst 2014-04-03 13:45:02 +0000 | |||
80 | +++ debian/maas-cluster-controller.postinst 2014-08-29 19:22:35 +0000 | |||
81 | @@ -103,6 +103,7 @@ | |||
82 | 103 | fi | 103 | fi |
83 | 104 | fi | 104 | fi |
84 | 105 | echo '0.0.0.0/0:68,69' >/etc/authbind/byuid/$MAAS_UID | 105 | echo '0.0.0.0/0:68,69' >/etc/authbind/byuid/$MAAS_UID |
85 | 106 | echo '::/0,68-69' >>/etc/authbind/byuid/$MAAS_UID | ||
86 | 106 | chown maas:maas /etc/authbind/byuid/$MAAS_UID | 107 | chown maas:maas /etc/authbind/byuid/$MAAS_UID |
87 | 107 | chmod 700 /etc/authbind/byuid/$MAAS_UID | 108 | chmod 700 /etc/authbind/byuid/$MAAS_UID |
88 | 108 | } | 109 | } |
89 | 109 | 110 | ||
90 | === modified file 'docs/_templates/maas/layout.html' | |||
91 | --- docs/_templates/maas/layout.html 2014-04-15 14:41:32 +0000 | |||
92 | +++ docs/_templates/maas/layout.html 2014-08-29 19:22:35 +0000 | |||
93 | @@ -10,6 +10,12 @@ | |||
94 | 10 | <br/> | 10 | <br/> |
95 | 11 | {% endblock %} | 11 | {% endblock %} |
96 | 12 | 12 | ||
97 | 13 | {# Remove 'modules' and 'index' from rellinks: they point to | ||
98 | 14 | autogenerated code documentation pages that we don't want | ||
99 | 15 | to advertise too much. | ||
100 | 16 | #} | ||
101 | 17 | {%- set rellinks = rellinks[2:] %} | ||
102 | 18 | |||
103 | 13 | {%- block footer %} | 19 | {%- block footer %} |
104 | 14 | <footer class="global clearfix"> | 20 | <footer class="global clearfix"> |
105 | 15 | <div class="legal clearfix"> | 21 | <div class="legal clearfix"> |
106 | 16 | 22 | ||
107 | === modified file 'docs/_templates/maas/static/css/main.css' | |||
108 | --- docs/_templates/maas/static/css/main.css 2014-02-15 12:08:23 +0000 | |||
109 | +++ docs/_templates/maas/static/css/main.css 2014-08-29 19:22:35 +0000 | |||
110 | @@ -9,7 +9,7 @@ | |||
111 | 9 | 9 | ||
112 | 10 | div.document { | 10 | div.document { |
113 | 11 | width: 984px; | 11 | width: 984px; |
115 | 12 | margin: 30px auto 0 auto; | 12 | margin: 10px auto 0 auto; |
116 | 13 | } | 13 | } |
117 | 14 | 14 | ||
118 | 15 | div.body h1 { | 15 | div.body h1 { |
119 | 16 | 16 | ||
120 | === modified file 'docs/_templates/maas/static/flasky.css_t' | |||
121 | --- docs/_templates/maas/static/flasky.css_t 2013-10-10 17:07:51 +0000 | |||
122 | +++ docs/_templates/maas/static/flasky.css_t 2014-08-29 19:22:35 +0000 | |||
123 | @@ -25,7 +25,7 @@ | |||
124 | 25 | 25 | ||
125 | 26 | div.document { | 26 | div.document { |
126 | 27 | width: {{ page_width }}; | 27 | width: {{ page_width }}; |
128 | 28 | margin: 30px auto 0 auto; | 28 | margin: 10px auto 0 auto; |
129 | 29 | } | 29 | } |
130 | 30 | 30 | ||
131 | 31 | div.documentwrapper { | 31 | div.documentwrapper { |
132 | @@ -69,6 +69,11 @@ | |||
133 | 69 | } | 69 | } |
134 | 70 | 70 | ||
135 | 71 | div.related { | 71 | div.related { |
136 | 72 | width: {{ page_width }}; | ||
137 | 73 | margin: 10px auto 0 auto; | ||
138 | 74 | } | ||
139 | 75 | |||
140 | 76 | div.related h3 { | ||
141 | 72 | display: none; | 77 | display: none; |
142 | 73 | } | 78 | } |
143 | 74 | 79 | ||
144 | 75 | 80 | ||
145 | === modified file 'docs/changelog.rst' | |||
146 | --- docs/changelog.rst 2014-06-02 11:57:58 +0000 | |||
147 | +++ docs/changelog.rst 2014-08-29 19:22:35 +0000 | |||
148 | @@ -2,6 +2,37 @@ | |||
149 | 2 | Changelog | 2 | Changelog |
150 | 3 | ========= | 3 | ========= |
151 | 4 | 4 | ||
152 | 5 | 1.5.4 | ||
153 | 6 | ===== | ||
154 | 7 | |||
155 | 8 | Bug fix update | ||
156 | 9 | -------------- | ||
157 | 10 | |||
158 | 11 | - Package fails to install when the default route is through an | ||
159 | 12 | aliased/tagged interface (LP: #1350235) | ||
160 | 13 | - ERROR Nonce already used (LP: #1190986) | ||
161 | 14 | - Add MAAS arm64/xgene support (LP: #1338851) | ||
162 | 15 | - Add utopic support (LP: #1337437) | ||
163 | 16 | - API documentation for nodegroup op=details missing parameter | ||
164 | 17 | (LP: #1331982) | ||
165 | 18 | |||
166 | 19 | |||
167 | 20 | 1.5.3 | ||
168 | 21 | ===== | ||
169 | 22 | |||
170 | 23 | Bug fix update | ||
171 | 24 | -------------- | ||
172 | 25 | |||
173 | 26 | - Reduce number of celery tasks emitted when updating a cluster controller | ||
174 | 27 | (LP: #1324944) | ||
175 | 28 | - Fix VirshSSH template which was referencing invalid attributes | ||
176 | 29 | (LP: #1324966) | ||
177 | 30 | - Fix a start up problem where a database lock was being taken outside of | ||
178 | 31 | a transaction (LP: #1325759) | ||
179 | 32 | - Reformat badly formatted Architecture error message (LP: #1301465) | ||
180 | 33 | - Final changes to support ppc64el (now known as PowerNV) (LP: #1315154) | ||
181 | 34 | |||
182 | 35 | |||
183 | 5 | 1.5.2 | 36 | 1.5.2 |
184 | 6 | ===== | 37 | ===== |
185 | 7 | 38 | ||
186 | 8 | 39 | ||
187 | === modified file 'etc/maas/templates/commissioning-user-data/snippets/maas_api_helper.py' | |||
188 | --- etc/maas/templates/commissioning-user-data/snippets/maas_api_helper.py 2013-10-10 17:07:51 +0000 | |||
189 | +++ etc/maas/templates/commissioning-user-data/snippets/maas_api_helper.py 2014-08-29 19:22:35 +0000 | |||
190 | @@ -12,6 +12,7 @@ | |||
191 | 12 | import sys | 12 | import sys |
192 | 13 | import time | 13 | import time |
193 | 14 | import urllib2 | 14 | import urllib2 |
194 | 15 | import uuid | ||
195 | 15 | 16 | ||
196 | 16 | import oauth.oauth as oauth | 17 | import oauth.oauth as oauth |
197 | 17 | import yaml | 18 | import yaml |
198 | @@ -60,7 +61,7 @@ | |||
199 | 60 | 61 | ||
200 | 61 | params = { | 62 | params = { |
201 | 62 | 'oauth_version': "1.0", | 63 | 'oauth_version': "1.0", |
203 | 63 | 'oauth_nonce': oauth.generate_nonce(), | 64 | 'oauth_nonce': uuid.uuid4().get_hex(), |
204 | 64 | 'oauth_timestamp': timestamp, | 65 | 'oauth_timestamp': timestamp, |
205 | 65 | 'oauth_token': token.key, | 66 | 'oauth_token': token.key, |
206 | 66 | 'oauth_consumer_key': consumer.key, | 67 | 'oauth_consumer_key': consumer.key, |
207 | 67 | 68 | ||
208 | === modified file 'etc/maas/templates/dhcp/dhcpd.conf.template' | |||
209 | --- etc/maas/templates/dhcp/dhcpd.conf.template 2014-04-09 19:02:00 +0000 | |||
210 | +++ etc/maas/templates/dhcp/dhcpd.conf.template 2014-08-29 19:22:35 +0000 | |||
211 | @@ -6,6 +6,7 @@ | |||
212 | 6 | # the nodegroup's configuration in MAAS to trigger an update. | 6 | # the nodegroup's configuration in MAAS to trigger an update. |
213 | 7 | 7 | ||
214 | 8 | option arch code 93 = unsigned integer 16; # RFC4578 | 8 | option arch code 93 = unsigned integer 16; # RFC4578 |
215 | 9 | option path-prefix code 210 = text; #RFC5071 | ||
216 | 9 | {{for dhcp_subnet in dhcp_subnets}} | 10 | {{for dhcp_subnet in dhcp_subnets}} |
217 | 10 | subnet {{dhcp_subnet['subnet']}} netmask {{dhcp_subnet['subnet_mask']}} { | 11 | subnet {{dhcp_subnet['subnet']}} netmask {{dhcp_subnet['subnet_mask']}} { |
218 | 11 | {{bootloader}} | 12 | {{bootloader}} |
219 | 12 | 13 | ||
220 | === added file 'etc/maas/templates/power/mscm.template' | |||
221 | --- etc/maas/templates/power/mscm.template 1970-01-01 00:00:00 +0000 | |||
222 | +++ etc/maas/templates/power/mscm.template 2014-08-29 19:22:35 +0000 | |||
223 | @@ -0,0 +1,15 @@ | |||
224 | 1 | # -*- mode: shell-script -*- | ||
225 | 2 | # | ||
226 | 3 | # Control a system via Moonshot HP iLO Chassis Manager (MSCM). | ||
227 | 4 | |||
228 | 5 | {{py: from provisioningserver.utils import escape_py_literal}} | ||
229 | 6 | python - << END | ||
230 | 7 | from provisioningserver.drivers.hardware.mscm import power_control_mscm | ||
231 | 8 | power_control_mscm( | ||
232 | 9 | {{escape_py_literal(power_address) | safe}}, | ||
233 | 10 | {{escape_py_literal(power_user) | safe}}, | ||
234 | 11 | {{escape_py_literal(power_pass) | safe}}, | ||
235 | 12 | {{escape_py_literal(node_id) | safe}}, | ||
236 | 13 | {{escape_py_literal(power_change) | safe}}, | ||
237 | 14 | ) | ||
238 | 15 | END | ||
239 | 0 | 16 | ||
240 | === added symlink 'etc/maas/templates/pxe/config.commissioning.arm64.template' | |||
241 | === target is u'config.commissioning.armhf.template' | |||
242 | === added file 'etc/maas/templates/pxe/config.commissioning.ppc64el.template' | |||
243 | --- etc/maas/templates/pxe/config.commissioning.ppc64el.template 1970-01-01 00:00:00 +0000 | |||
244 | +++ etc/maas/templates/pxe/config.commissioning.ppc64el.template 2014-08-29 19:22:35 +0000 | |||
245 | @@ -0,0 +1,6 @@ | |||
246 | 1 | DEFAULT execute | ||
247 | 2 | |||
248 | 3 | LABEL execute | ||
249 | 4 | KERNEL {{kernel_params | kernel_path }} | ||
250 | 5 | INITRD {{kernel_params | initrd_path }} | ||
251 | 6 | APPEND {{kernel_params | kernel_command}} | ||
252 | 0 | 7 | ||
253 | === added symlink 'etc/maas/templates/pxe/config.install.arm64.template' | |||
254 | === target is u'config.install.armhf.template' | |||
255 | === added file 'etc/maas/templates/pxe/config.install.ppc64el.template' | |||
256 | --- etc/maas/templates/pxe/config.install.ppc64el.template 1970-01-01 00:00:00 +0000 | |||
257 | +++ etc/maas/templates/pxe/config.install.ppc64el.template 2014-08-29 19:22:35 +0000 | |||
258 | @@ -0,0 +1,6 @@ | |||
259 | 1 | DEFAULT execute | ||
260 | 2 | |||
261 | 3 | LABEL execute | ||
262 | 4 | KERNEL {{kernel_params | kernel_path }} | ||
263 | 5 | INITRD {{kernel_params | initrd_path }} | ||
264 | 6 | APPEND {{kernel_params | kernel_command}} | ||
265 | 0 | 7 | ||
266 | === added symlink 'etc/maas/templates/pxe/config.xinstall.arm64.template' | |||
267 | === target is u'config.xinstall.armhf.template' | |||
268 | === added symlink 'etc/maas/templates/pxe/config.xinstall.ppc64el.template' | |||
269 | === target is u'config.install.ppc64el.template' | |||
270 | === modified file 'src/apiclient/maas_client.py' | |||
271 | --- src/apiclient/maas_client.py 2014-04-15 14:41:32 +0000 | |||
272 | +++ src/apiclient/maas_client.py 2014-08-29 19:22:35 +0000 | |||
273 | @@ -21,6 +21,7 @@ | |||
274 | 21 | import gzip | 21 | import gzip |
275 | 22 | from io import BytesIO | 22 | from io import BytesIO |
276 | 23 | import urllib2 | 23 | import urllib2 |
277 | 24 | import uuid | ||
278 | 24 | 25 | ||
279 | 25 | from apiclient.encode_json import encode_json_data | 26 | from apiclient.encode_json import encode_json_data |
280 | 26 | from apiclient.multipart import encode_multipart_data | 27 | from apiclient.multipart import encode_multipart_data |
281 | @@ -45,7 +46,8 @@ | |||
282 | 45 | with the signature. | 46 | with the signature. |
283 | 46 | """ | 47 | """ |
284 | 47 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( | 48 | oauth_request = oauth.OAuthRequest.from_consumer_and_token( |
286 | 48 | self.consumer_token, token=self.resource_token, http_url=url) | 49 | self.consumer_token, token=self.resource_token, http_url=url, |
287 | 50 | parameters={'oauth_nonce': uuid.uuid4().get_hex()}) | ||
288 | 49 | oauth_request.sign_request( | 51 | oauth_request.sign_request( |
289 | 50 | oauth.OAuthSignatureMethod_PLAINTEXT(), self.consumer_token, | 52 | oauth.OAuthSignatureMethod_PLAINTEXT(), self.consumer_token, |
290 | 51 | self.resource_token) | 53 | self.resource_token) |
291 | 52 | 54 | ||
292 | === modified file 'src/maasserver/api.py' | |||
293 | --- src/maasserver/api.py 2014-06-04 14:31:41 +0000 | |||
294 | +++ src/maasserver/api.py 2014-08-29 19:22:35 +0000 | |||
295 | @@ -1580,6 +1580,8 @@ | |||
296 | 1580 | Returns a ``{system_id: {detail_type: xml, ...}, ...}`` map, | 1580 | Returns a ``{system_id: {detail_type: xml, ...}, ...}`` map, |
297 | 1581 | where ``detail_type`` is something like "lldp" or "lshw". | 1581 | where ``detail_type`` is something like "lldp" or "lshw". |
298 | 1582 | 1582 | ||
299 | 1583 | :param system_ids: System ids of nodes for which to get system details. | ||
300 | 1584 | |||
301 | 1583 | Note that this is returned as BSON and not JSON. This is for | 1585 | Note that this is returned as BSON and not JSON. This is for |
302 | 1584 | efficiency, but mainly because JSON can't do binary content | 1586 | efficiency, but mainly because JSON can't do binary content |
303 | 1585 | without applying additional encoding like base-64. | 1587 | without applying additional encoding like base-64. |
304 | @@ -1761,6 +1763,30 @@ | |||
305 | 1761 | 1763 | ||
306 | 1762 | return HttpResponse(status=httplib.OK) | 1764 | return HttpResponse(status=httplib.OK) |
307 | 1763 | 1765 | ||
308 | 1766 | @admin_method | ||
309 | 1767 | @operation(idempotent=False) | ||
310 | 1768 | def probe_and_enlist_mscm(self, request, uuid): | ||
311 | 1769 | """Add the nodes from a Moonshot HP iLO Chassis Manager (MSCM). | ||
312 | 1770 | |||
313 | 1771 | :param host: IP Address for the MSCM. | ||
314 | 1772 | :type host: unicode | ||
315 | 1773 | :param username: The username for the MSCM. | ||
316 | 1774 | :type username: unicode | ||
317 | 1775 | :param password: The password for the MSCM. | ||
318 | 1776 | :type password: unicode | ||
319 | 1777 | |||
320 | 1778 | """ | ||
321 | 1779 | nodegroup = get_object_or_404(NodeGroup, uuid=uuid) | ||
322 | 1780 | |||
323 | 1781 | host = get_mandatory_param(request.data, 'host') | ||
324 | 1782 | username = get_mandatory_param(request.data, 'username') | ||
325 | 1783 | password = get_mandatory_param(request.data, 'password') | ||
326 | 1784 | |||
327 | 1785 | nodegroup.enlist_nodes_from_mscm(host, username, password) | ||
328 | 1786 | |||
329 | 1787 | return HttpResponse(status=httplib.OK) | ||
330 | 1788 | |||
331 | 1789 | |||
332 | 1764 | DISPLAYED_NODEGROUPINTERFACE_FIELDS = ( | 1790 | DISPLAYED_NODEGROUPINTERFACE_FIELDS = ( |
333 | 1765 | 'ip', 'management', 'interface', 'subnet_mask', | 1791 | 'ip', 'management', 'interface', 'subnet_mask', |
334 | 1766 | 'broadcast_ip', 'ip_range_low', 'ip_range_high') | 1792 | 'broadcast_ip', 'ip_range_low', 'ip_range_high') |
335 | 1767 | 1793 | ||
336 | === modified file 'src/maasserver/enum.py' | |||
337 | --- src/maasserver/enum.py 2014-03-28 10:43:53 +0000 | |||
338 | +++ src/maasserver/enum.py 2014-08-29 19:22:35 +0000 | |||
339 | @@ -93,21 +93,19 @@ | |||
340 | 93 | #: | 93 | #: |
341 | 94 | precise = 'precise' | 94 | precise = 'precise' |
342 | 95 | #: | 95 | #: |
343 | 96 | quantal = 'quantal' | ||
344 | 97 | #: | ||
345 | 98 | raring = 'raring' | ||
346 | 99 | #: | ||
347 | 100 | saucy = 'saucy' | 96 | saucy = 'saucy' |
348 | 101 | #: | 97 | #: |
349 | 102 | trusty = 'trusty' | 98 | trusty = 'trusty' |
350 | 99 | #: | ||
351 | 100 | utopic = 'utopic' | ||
352 | 101 | |||
353 | 103 | 102 | ||
354 | 104 | DISTRO_SERIES_CHOICES = ( | 103 | DISTRO_SERIES_CHOICES = ( |
355 | 105 | (DISTRO_SERIES.default, 'Default Ubuntu Release'), | 104 | (DISTRO_SERIES.default, 'Default Ubuntu Release'), |
356 | 106 | (DISTRO_SERIES.precise, 'Ubuntu 12.04 LTS "Precise Pangolin"'), | 105 | (DISTRO_SERIES.precise, 'Ubuntu 12.04 LTS "Precise Pangolin"'), |
357 | 107 | (DISTRO_SERIES.quantal, 'Ubuntu 12.10 "Quantal Quetzal"'), | ||
358 | 108 | (DISTRO_SERIES.raring, 'Ubuntu 13.04 "Raring Ringtail"'), | ||
359 | 109 | (DISTRO_SERIES.saucy, 'Ubuntu 13.10 "Saucy Salamander"'), | 106 | (DISTRO_SERIES.saucy, 'Ubuntu 13.10 "Saucy Salamander"'), |
360 | 110 | (DISTRO_SERIES.trusty, 'Ubuntu 14.04 LTS "Trusty Tahr"'), | 107 | (DISTRO_SERIES.trusty, 'Ubuntu 14.04 LTS "Trusty Tahr"'), |
361 | 108 | (DISTRO_SERIES.utopic, 'Ubuntu 14.10 "Utopic Unicorn"'), | ||
362 | 111 | ) | 109 | ) |
363 | 112 | 110 | ||
364 | 113 | 111 | ||
365 | 114 | 112 | ||
366 | === modified file 'src/maasserver/models/nodegroup.py' | |||
367 | --- src/maasserver/models/nodegroup.py 2014-06-04 14:31:41 +0000 | |||
368 | +++ src/maasserver/models/nodegroup.py 2014-08-29 19:22:35 +0000 | |||
369 | @@ -42,6 +42,7 @@ | |||
370 | 42 | add_new_dhcp_host_map, | 42 | add_new_dhcp_host_map, |
371 | 43 | add_seamicro15k, | 43 | add_seamicro15k, |
372 | 44 | add_virsh, | 44 | add_virsh, |
373 | 45 | enlist_nodes_from_mscm, | ||
374 | 45 | enlist_nodes_from_ucsm, | 46 | enlist_nodes_from_ucsm, |
375 | 46 | import_boot_images, | 47 | import_boot_images, |
376 | 47 | report_boot_images, | 48 | report_boot_images, |
377 | @@ -307,6 +308,16 @@ | |||
378 | 307 | args = (url, username, password) | 308 | args = (url, username, password) |
379 | 308 | enlist_nodes_from_ucsm.apply_async(queue=self.uuid, args=args) | 309 | enlist_nodes_from_ucsm.apply_async(queue=self.uuid, args=args) |
380 | 309 | 310 | ||
381 | 311 | def enlist_nodes_from_mscm(self, host, username, password): | ||
382 | 312 | """ Add the servers from a Moonshot HP iLO Chassis Manager. | ||
383 | 313 | |||
384 | 314 | :param host: IP address for the MSCM. | ||
385 | 315 | :param username: username for MSCM. | ||
386 | 316 | :param password: password for MSCM. | ||
387 | 317 | """ | ||
388 | 318 | args = (host, username, password) | ||
389 | 319 | enlist_nodes_from_mscm.apply_async(queue=self.uuid, args=args) | ||
390 | 320 | |||
391 | 310 | def add_dhcp_host_maps(self, new_leases): | 321 | def add_dhcp_host_maps(self, new_leases): |
392 | 311 | if len(new_leases) > 0 and len(self.get_managed_interfaces()) > 0: | 322 | if len(new_leases) > 0 and len(self.get_managed_interfaces()) > 0: |
393 | 312 | # XXX JeroenVermeulen 2012-08-21, bug=1039362: the DHCP | 323 | # XXX JeroenVermeulen 2012-08-21, bug=1039362: the DHCP |
394 | 313 | 324 | ||
395 | === modified file 'src/maasserver/models/tests/test_node.py' | |||
396 | --- src/maasserver/models/tests/test_node.py 2014-04-15 14:41:32 +0000 | |||
397 | +++ src/maasserver/models/tests/test_node.py 2014-08-29 19:22:35 +0000 | |||
398 | @@ -162,7 +162,7 @@ | |||
399 | 162 | 162 | ||
400 | 163 | def test_set_get_distro_series_returns_series(self): | 163 | def test_set_get_distro_series_returns_series(self): |
401 | 164 | node = factory.make_node() | 164 | node = factory.make_node() |
403 | 165 | series = DISTRO_SERIES.quantal | 165 | series = DISTRO_SERIES.utopic |
404 | 166 | node.set_distro_series(series) | 166 | node.set_distro_series(series) |
405 | 167 | self.assertEqual(series, node.get_distro_series()) | 167 | self.assertEqual(series, node.get_distro_series()) |
406 | 168 | 168 | ||
407 | @@ -474,7 +474,7 @@ | |||
408 | 474 | def test_release_clears_distro_series(self): | 474 | def test_release_clears_distro_series(self): |
409 | 475 | node = factory.make_node( | 475 | node = factory.make_node( |
410 | 476 | status=NODE_STATUS.ALLOCATED, owner=factory.make_user()) | 476 | status=NODE_STATUS.ALLOCATED, owner=factory.make_user()) |
412 | 477 | node.set_distro_series(series=DISTRO_SERIES.quantal) | 477 | node.set_distro_series(series=DISTRO_SERIES.utopic) |
413 | 478 | node.release() | 478 | node.release() |
414 | 479 | self.assertEqual("", node.distro_series) | 479 | self.assertEqual("", node.distro_series) |
415 | 480 | 480 | ||
416 | 481 | 481 | ||
417 | === modified file 'src/maasserver/rpc/regionservice.py' | |||
418 | --- src/maasserver/rpc/regionservice.py 2014-04-03 13:45:02 +0000 | |||
419 | +++ src/maasserver/rpc/regionservice.py 2014-08-29 19:22:35 +0000 | |||
420 | @@ -325,8 +325,8 @@ | |||
421 | 325 | 325 | ||
422 | 326 | @synchronous | 326 | @synchronous |
423 | 327 | @synchronised(lock) | 327 | @synchronised(lock) |
424 | 328 | @transactional | ||
425 | 328 | @synchronised(locks.eventloop) | 329 | @synchronised(locks.eventloop) |
426 | 329 | @transactional | ||
427 | 330 | def prepare(self): | 330 | def prepare(self): |
428 | 331 | """Ensure that the ``eventloops`` table exists. | 331 | """Ensure that the ``eventloops`` table exists. |
429 | 332 | 332 | ||
430 | 333 | 333 | ||
431 | === modified file 'src/maasserver/rpc/tests/test_regionservice.py' | |||
432 | --- src/maasserver/rpc/tests/test_regionservice.py 2014-04-15 14:41:32 +0000 | |||
433 | +++ src/maasserver/rpc/tests/test_regionservice.py 2014-08-29 19:22:35 +0000 | |||
434 | @@ -49,6 +49,7 @@ | |||
435 | 49 | common, | 49 | common, |
436 | 50 | exceptions, | 50 | exceptions, |
437 | 51 | ) | 51 | ) |
438 | 52 | from provisioningserver.rpc.interfaces import IConnection | ||
439 | 52 | from provisioningserver.rpc.region import ( | 53 | from provisioningserver.rpc.region import ( |
440 | 53 | Identify, | 54 | Identify, |
441 | 54 | ReportBootImages, | 55 | ReportBootImages, |
442 | @@ -82,6 +83,7 @@ | |||
443 | 82 | from twisted.internet.threads import deferToThread | 83 | from twisted.internet.threads import deferToThread |
444 | 83 | from twisted.protocols import amp | 84 | from twisted.protocols import amp |
445 | 84 | from twisted.python import log | 85 | from twisted.python import log |
446 | 86 | from zope.interface.verify import verifyObject | ||
447 | 85 | 87 | ||
448 | 86 | 88 | ||
449 | 87 | class TestRegionProtocol_Identify(MAASTestCase): | 89 | class TestRegionProtocol_Identify(MAASTestCase): |
450 | @@ -195,10 +197,6 @@ | |||
451 | 195 | return d.addCallback(check) | 197 | return d.addCallback(check) |
452 | 196 | 198 | ||
453 | 197 | 199 | ||
454 | 198 | from provisioningserver.rpc.interfaces import IConnection | ||
455 | 199 | from zope.interface.verify import verifyObject | ||
456 | 200 | |||
457 | 201 | |||
458 | 202 | class TestRegionServer(MAASServerTestCase): | 200 | class TestRegionServer(MAASServerTestCase): |
459 | 203 | 201 | ||
460 | 204 | def test_interfaces(self): | 202 | def test_interfaces(self): |
461 | 205 | 203 | ||
462 | === modified file 'src/maasserver/start_up.py' | |||
463 | --- src/maasserver/start_up.py 2014-04-09 19:02:00 +0000 | |||
464 | +++ src/maasserver/start_up.py 2014-08-29 19:22:35 +0000 | |||
465 | @@ -18,7 +18,10 @@ | |||
466 | 18 | 18 | ||
467 | 19 | from textwrap import dedent | 19 | from textwrap import dedent |
468 | 20 | 20 | ||
470 | 21 | from django.db import connection | 21 | from django.db import ( |
471 | 22 | connection, | ||
472 | 23 | transaction, | ||
473 | 24 | ) | ||
474 | 22 | from maasserver import ( | 25 | from maasserver import ( |
475 | 23 | eventloop, | 26 | eventloop, |
476 | 24 | locks, | 27 | locks, |
477 | @@ -51,8 +54,9 @@ | |||
478 | 51 | but this method uses file-based locking to ensure that the methods it calls | 54 | but this method uses file-based locking to ensure that the methods it calls |
479 | 52 | internally are not ran concurrently. | 55 | internally are not ran concurrently. |
480 | 53 | """ | 56 | """ |
483 | 54 | with locks.startup: | 57 | with transaction.atomic(): |
484 | 55 | inner_start_up() | 58 | with locks.startup: |
485 | 59 | inner_start_up() | ||
486 | 56 | 60 | ||
487 | 57 | eventloop.start().wait(10) | 61 | eventloop.start().wait(10) |
488 | 58 | 62 | ||
489 | 59 | 63 | ||
490 | === modified file 'src/maasserver/testing/tests/test_rabbit.py' | |||
491 | --- src/maasserver/testing/tests/test_rabbit.py 2013-10-04 12:33:05 +0000 | |||
492 | +++ src/maasserver/testing/tests/test_rabbit.py 2014-08-29 19:22:35 +0000 | |||
493 | @@ -26,7 +26,8 @@ | |||
494 | 26 | def test_patch(self): | 26 | def test_patch(self): |
495 | 27 | config = RabbitServerResources( | 27 | config = RabbitServerResources( |
496 | 28 | hostname=factory.getRandomString(), | 28 | hostname=factory.getRandomString(), |
498 | 29 | port=factory.getRandomPort()) | 29 | port=factory.getRandomPort(), |
499 | 30 | dist_port=factory.getRandomPort()) | ||
500 | 30 | self.useFixture(config) | 31 | self.useFixture(config) |
501 | 31 | self.useFixture(RabbitServerSettings(config)) | 32 | self.useFixture(RabbitServerSettings(config)) |
502 | 32 | self.assertEqual( | 33 | self.assertEqual( |
503 | 33 | 34 | ||
504 | === modified file 'src/maasserver/tests/test_api_nodegroup.py' | |||
505 | --- src/maasserver/tests/test_api_nodegroup.py 2014-04-15 14:41:32 +0000 | |||
506 | +++ src/maasserver/tests/test_api_nodegroup.py 2014-08-29 19:22:35 +0000 | |||
507 | @@ -460,6 +460,32 @@ | |||
508 | 460 | matcher = MockCalledOnceWith(queue=nodegroup.uuid, args=args) | 460 | matcher = MockCalledOnceWith(queue=nodegroup.uuid, args=args) |
509 | 461 | self.assertThat(mock.apply_async, matcher) | 461 | self.assertThat(mock.apply_async, matcher) |
510 | 462 | 462 | ||
511 | 463 | def test_probe_and_enlist_mscm_adds_mscm(self): | ||
512 | 464 | nodegroup = factory.make_node_group() | ||
513 | 465 | host = 'http://host' | ||
514 | 466 | username = factory.make_name('user') | ||
515 | 467 | password = factory.make_name('password') | ||
516 | 468 | self.become_admin() | ||
517 | 469 | |||
518 | 470 | mock = self.patch(nodegroup_module, 'enlist_nodes_from_mscm') | ||
519 | 471 | |||
520 | 472 | response = self.client.post( | ||
521 | 473 | reverse('nodegroup_handler', args=[nodegroup.uuid]), | ||
522 | 474 | { | ||
523 | 475 | 'op': 'probe_and_enlist_mscm', | ||
524 | 476 | 'host': host, | ||
525 | 477 | 'username': username, | ||
526 | 478 | 'password': password, | ||
527 | 479 | }) | ||
528 | 480 | |||
529 | 481 | self.assertEqual( | ||
530 | 482 | httplib.OK, response.status_code, | ||
531 | 483 | explain_unexpected_response(httplib.OK, response)) | ||
532 | 484 | |||
533 | 485 | args = (host, username, password) | ||
534 | 486 | matcher = MockCalledOnceWith(queue=nodegroup.uuid, args=args) | ||
535 | 487 | self.assertThat(mock.apply_async, matcher) | ||
536 | 488 | |||
537 | 463 | 489 | ||
538 | 464 | class TestNodeGroupAPIAuth(MAASServerTestCase): | 490 | class TestNodeGroupAPIAuth(MAASServerTestCase): |
539 | 465 | """Authorization tests for nodegroup API.""" | 491 | """Authorization tests for nodegroup API.""" |
540 | 466 | 492 | ||
541 | === modified file 'src/maasserver/utils/dblocks.py' | |||
542 | --- src/maasserver/utils/dblocks.py 2014-04-03 13:45:02 +0000 | |||
543 | +++ src/maasserver/utils/dblocks.py 2014-08-29 19:22:35 +0000 | |||
544 | @@ -14,6 +14,8 @@ | |||
545 | 14 | __metaclass__ = type | 14 | __metaclass__ = type |
546 | 15 | __all__ = [ | 15 | __all__ = [ |
547 | 16 | "DatabaseLock", | 16 | "DatabaseLock", |
548 | 17 | "DatabaseLockAttemptOutsideTransaction", | ||
549 | 18 | "DatabaseLockNotHeld", | ||
550 | 17 | ] | 19 | ] |
551 | 18 | 20 | ||
552 | 19 | from contextlib import closing | 21 | from contextlib import closing |
553 | @@ -26,6 +28,19 @@ | |||
554 | 26 | classid = 20120116 | 28 | classid = 20120116 |
555 | 27 | 29 | ||
556 | 28 | 30 | ||
557 | 31 | class DatabaseLockAttemptOutsideTransaction(Exception): | ||
558 | 32 | """A locking attempt was made outside of a transaction. | ||
559 | 33 | |||
560 | 34 | :class:`DatabaseLock` should only be used within a transaction. | ||
561 | 35 | Django agressively closes connections outside of atomic blocks to | ||
562 | 36 | the extent that session-level locks are rendered unreliable at best. | ||
563 | 37 | """ | ||
564 | 38 | |||
565 | 39 | |||
566 | 40 | class DatabaseLockNotHeld(Exception): | ||
567 | 41 | """A particular lock was not held.""" | ||
568 | 42 | |||
569 | 43 | |||
570 | 29 | class DatabaseLock(tuple): | 44 | class DatabaseLock(tuple): |
571 | 30 | """An advisory lock held in the database. | 45 | """An advisory lock held in the database. |
572 | 31 | 46 | ||
573 | @@ -58,12 +73,16 @@ | |||
574 | 58 | return super(cls, DatabaseLock).__new__(cls, (classid, objid)) | 73 | return super(cls, DatabaseLock).__new__(cls, (classid, objid)) |
575 | 59 | 74 | ||
576 | 60 | def __enter__(self): | 75 | def __enter__(self): |
577 | 76 | if not connection.in_atomic_block: | ||
578 | 77 | raise DatabaseLockAttemptOutsideTransaction(self) | ||
579 | 61 | with closing(connection.cursor()) as cursor: | 78 | with closing(connection.cursor()) as cursor: |
580 | 62 | cursor.execute("SELECT pg_advisory_lock(%s, %s)", self) | 79 | cursor.execute("SELECT pg_advisory_lock(%s, %s)", self) |
581 | 63 | 80 | ||
582 | 64 | def __exit__(self, *exc_info): | 81 | def __exit__(self, *exc_info): |
583 | 65 | with closing(connection.cursor()) as cursor: | 82 | with closing(connection.cursor()) as cursor: |
584 | 66 | cursor.execute("SELECT pg_advisory_unlock(%s, %s)", self) | 83 | cursor.execute("SELECT pg_advisory_unlock(%s, %s)", self) |
585 | 84 | if cursor.fetchone() != (True,): | ||
586 | 85 | raise DatabaseLockNotHeld(self) | ||
587 | 67 | 86 | ||
588 | 68 | def __repr__(self): | 87 | def __repr__(self): |
589 | 69 | return b"<%s classid=%d objid=%d>" % ( | 88 | return b"<%s classid=%d objid=%d>" % ( |
590 | @@ -71,8 +90,18 @@ | |||
591 | 71 | 90 | ||
592 | 72 | def is_locked(self): | 91 | def is_locked(self): |
593 | 73 | stmt = ( | 92 | stmt = ( |
596 | 74 | "SELECT 1 FROM pg_locks" | 93 | "SELECT 1 FROM pg_locks, pg_database" |
597 | 75 | " WHERE classid = %s AND objid = %s AND granted" | 94 | " WHERE pg_locks.locktype = 'advisory'" |
598 | 95 | " AND pg_locks.classid = %s" | ||
599 | 96 | " AND pg_locks.objid = %s" | ||
600 | 97 | # objsubid is 2 when using the 2-argument version of the | ||
601 | 98 | # pg_advisory_* locking functions. | ||
602 | 99 | " AND pg_locks.objsubid = 2" | ||
603 | 100 | " AND pg_locks.granted" | ||
604 | 101 | # Advisory locks are local to each database so we join to | ||
605 | 102 | # pg_databases to discover the OID of the currrent database. | ||
606 | 103 | " AND pg_locks.database = pg_database.oid" | ||
607 | 104 | " AND pg_database.datname = current_database()" | ||
608 | 76 | ) | 105 | ) |
609 | 77 | with closing(connection.cursor()) as cursor: | 106 | with closing(connection.cursor()) as cursor: |
610 | 78 | cursor.execute(stmt, self) | 107 | cursor.execute(stmt, self) |
611 | 79 | 108 | ||
612 | === modified file 'src/maasserver/utils/tests/test_dblocks.py' | |||
613 | --- src/maasserver/utils/tests/test_dblocks.py 2014-03-28 10:43:53 +0000 | |||
614 | +++ src/maasserver/utils/tests/test_dblocks.py 2014-08-29 19:22:35 +0000 | |||
615 | @@ -16,7 +16,10 @@ | |||
616 | 16 | 16 | ||
617 | 17 | from contextlib import closing | 17 | from contextlib import closing |
618 | 18 | 18 | ||
620 | 19 | from django.db import connection | 19 | from django.db import ( |
621 | 20 | connection, | ||
622 | 21 | transaction, | ||
623 | 22 | ) | ||
624 | 20 | from maasserver.utils import dblocks | 23 | from maasserver.utils import dblocks |
625 | 21 | from maastesting.testcase import MAASTestCase | 24 | from maastesting.testcase import MAASTestCase |
626 | 22 | 25 | ||
627 | @@ -40,6 +43,7 @@ | |||
628 | 40 | lock = dblocks.DatabaseLock(self.getUniqueInteger()) | 43 | lock = dblocks.DatabaseLock(self.getUniqueInteger()) |
629 | 41 | self.assertEqual(lock, (lock.classid, lock.objid)) | 44 | self.assertEqual(lock, (lock.classid, lock.objid)) |
630 | 42 | 45 | ||
631 | 46 | @transaction.atomic | ||
632 | 43 | def test_lock_actually_locked(self): | 47 | def test_lock_actually_locked(self): |
633 | 44 | objid = self.getUniqueInteger() | 48 | objid = self.getUniqueInteger() |
634 | 45 | lock = dblocks.DatabaseLock(objid) | 49 | lock = dblocks.DatabaseLock(objid) |
635 | @@ -55,6 +59,7 @@ | |||
636 | 55 | locks_released = locks_held - locks_held_after | 59 | locks_released = locks_held - locks_held_after |
637 | 56 | self.assertEqual({objid}, locks_released) | 60 | self.assertEqual({objid}, locks_released) |
638 | 57 | 61 | ||
639 | 62 | @transaction.atomic | ||
640 | 58 | def test_is_locked(self): | 63 | def test_is_locked(self): |
641 | 59 | objid = self.getUniqueInteger() | 64 | objid = self.getUniqueInteger() |
642 | 60 | lock = dblocks.DatabaseLock(objid) | 65 | lock = dblocks.DatabaseLock(objid) |
643 | @@ -64,6 +69,18 @@ | |||
644 | 64 | self.assertTrue(lock.is_locked()) | 69 | self.assertTrue(lock.is_locked()) |
645 | 65 | self.assertFalse(lock.is_locked()) | 70 | self.assertFalse(lock.is_locked()) |
646 | 66 | 71 | ||
647 | 72 | def test_obtaining_lock_fails_when_outside_of_transaction(self): | ||
648 | 73 | objid = self.getUniqueInteger() | ||
649 | 74 | lock = dblocks.DatabaseLock(objid) | ||
650 | 75 | self.assertRaises( | ||
651 | 76 | dblocks.DatabaseLockAttemptOutsideTransaction, | ||
652 | 77 | lock.__enter__) | ||
653 | 78 | |||
654 | 79 | def test_releasing_lock_fails_when_lock_not_held(self): | ||
655 | 80 | objid = self.getUniqueInteger() | ||
656 | 81 | lock = dblocks.DatabaseLock(objid) | ||
657 | 82 | self.assertRaises(dblocks.DatabaseLockNotHeld, lock.__exit__) | ||
658 | 83 | |||
659 | 67 | def test_repr(self): | 84 | def test_repr(self): |
660 | 68 | lock = dblocks.DatabaseLock(self.getUniqueInteger()) | 85 | lock = dblocks.DatabaseLock(self.getUniqueInteger()) |
661 | 69 | self.assertEqual( | 86 | self.assertEqual( |
662 | 70 | 87 | ||
663 | === modified file 'src/maastesting/factory.py' | |||
664 | --- src/maastesting/factory.py 2014-03-28 10:43:53 +0000 | |||
665 | +++ src/maastesting/factory.py 2014-08-29 19:22:35 +0000 | |||
666 | @@ -35,6 +35,7 @@ | |||
667 | 35 | from uuid import uuid1 | 35 | from uuid import uuid1 |
668 | 36 | 36 | ||
669 | 37 | from maastesting.fixtures import TempDirectory | 37 | from maastesting.fixtures import TempDirectory |
670 | 38 | import mock | ||
671 | 38 | from netaddr import ( | 39 | from netaddr import ( |
672 | 39 | IPAddress, | 40 | IPAddress, |
673 | 40 | IPNetwork, | 41 | IPNetwork, |
674 | @@ -264,6 +265,14 @@ | |||
675 | 264 | 265 | ||
676 | 265 | return tarball | 266 | return tarball |
677 | 266 | 267 | ||
678 | 268 | def make_streams(self, stdin=None, stdout=None, stderr=None): | ||
679 | 269 | """Make a fake return value for a SSHClient.exec_command.""" | ||
680 | 270 | # stdout.read() is called so stdout can't be None. | ||
681 | 271 | if stdout is None: | ||
682 | 272 | stdout = mock.Mock() | ||
683 | 273 | |||
684 | 274 | return (stdin, stdout, stderr) | ||
685 | 275 | |||
686 | 267 | 276 | ||
687 | 268 | # Create factory singleton. | 277 | # Create factory singleton. |
688 | 269 | factory = Factory() | 278 | factory = Factory() |
689 | 270 | 279 | ||
690 | === modified file 'src/metadataserver/address.py' | |||
691 | --- src/metadataserver/address.py 2013-10-10 17:07:51 +0000 | |||
692 | +++ src/metadataserver/address.py 2014-08-29 19:22:35 +0000 | |||
693 | @@ -63,13 +63,13 @@ | |||
694 | 63 | """ | 63 | """ |
695 | 64 | route_lines = list(ip_route_output) | 64 | route_lines = list(ip_route_output) |
696 | 65 | for line in route_lines: | 65 | for line in route_lines: |
698 | 66 | match = re.match('default\s+.*\sdev\s+(\w+)', line) | 66 | match = re.match('default\s+.*\sdev\s+([^\s]+)', line) |
699 | 67 | if match is not None: | 67 | if match is not None: |
700 | 68 | return match.groups()[0] | 68 | return match.groups()[0] |
701 | 69 | 69 | ||
702 | 70 | # Still nothing? Try the first recognizable interface in the list. | 70 | # Still nothing? Try the first recognizable interface in the list. |
703 | 71 | for line in route_lines: | 71 | for line in route_lines: |
705 | 72 | match = re.match('\s*(?:\S+\s+)*dev\s+(\w+)', line) | 72 | match = re.match('\s*(?:\S+\s+)*dev\s+([^\s]+)', line) |
706 | 73 | if match is not None: | 73 | if match is not None: |
707 | 74 | return match.groups()[0] | 74 | return match.groups()[0] |
708 | 75 | return None | 75 | return None |
709 | 76 | 76 | ||
710 | === modified file 'src/metadataserver/tests/test_address.py' | |||
711 | --- src/metadataserver/tests/test_address.py 2013-10-10 17:07:51 +0000 | |||
712 | +++ src/metadataserver/tests/test_address.py 2014-08-29 19:22:35 +0000 | |||
713 | @@ -59,6 +59,26 @@ | |||
714 | 59 | self.assertEqual( | 59 | self.assertEqual( |
715 | 60 | 'eth1', address.find_default_interface(sample_ip_route)) | 60 | 'eth1', address.find_default_interface(sample_ip_route)) |
716 | 61 | 61 | ||
717 | 62 | def test_find_default_interface_finds_default_tagged_interface(self): | ||
718 | 63 | sample_ip_route = [ | ||
719 | 64 | "default via 10.20.64.1 dev eth0.2", | ||
720 | 65 | "10.14.0.0/16 dev br0 proto kernel scope link src 10.14.4.1", | ||
721 | 66 | "10.90.90.0/24 dev br0 proto kernel scope link src 10.90.90.1", | ||
722 | 67 | "169.254.0.0/16 dev br0 scope link metric 1000", | ||
723 | 68 | ] | ||
724 | 69 | self.assertEqual( | ||
725 | 70 | 'eth0.2', address.find_default_interface(sample_ip_route)) | ||
726 | 71 | |||
727 | 72 | def test_find_default_interface_finds_default_aliased_interface(self): | ||
728 | 73 | sample_ip_route = [ | ||
729 | 74 | "default via 10.20.64.1 dev eth0:2", | ||
730 | 75 | "10.14.0.0/16 dev br0 proto kernel scope link src 10.14.4.1", | ||
731 | 76 | "10.90.90.0/24 dev br0 proto kernel scope link src 10.90.90.1", | ||
732 | 77 | "169.254.0.0/16 dev br0 scope link metric 1000", | ||
733 | 78 | ] | ||
734 | 79 | self.assertEqual( | ||
735 | 80 | 'eth0:2', address.find_default_interface(sample_ip_route)) | ||
736 | 81 | |||
737 | 62 | def test_find_default_interface_makes_a_guess_if_no_default(self): | 82 | def test_find_default_interface_makes_a_guess_if_no_default(self): |
738 | 63 | sample_ip_route = [ | 83 | sample_ip_route = [ |
739 | 64 | "10.0.0.0/24 dev eth2 proto kernel scope link src 10.0.0.11 " | 84 | "10.0.0.0/24 dev eth2 proto kernel scope link src 10.0.0.11 " |
740 | @@ -69,6 +89,26 @@ | |||
741 | 69 | self.assertEqual( | 89 | self.assertEqual( |
742 | 70 | 'eth2', address.find_default_interface(sample_ip_route)) | 90 | 'eth2', address.find_default_interface(sample_ip_route)) |
743 | 71 | 91 | ||
744 | 92 | def test_find_default_tagged_interface_makes_a_guess_if_no_default(self): | ||
745 | 93 | sample_ip_route = [ | ||
746 | 94 | "10.0.0.0/24 dev eth2.4 proto kernel scope link src 10.0.0.11 " | ||
747 | 95 | "metric 2", | ||
748 | 96 | "10.1.0.0/24 dev virbr0 proto kernel scope link src 10.1.0.1", | ||
749 | 97 | "10.1.1.0/24 dev virbr1 proto kernel scope link src 10.1.1.1", | ||
750 | 98 | ] | ||
751 | 99 | self.assertEqual( | ||
752 | 100 | 'eth2.4', address.find_default_interface(sample_ip_route)) | ||
753 | 101 | |||
754 | 102 | def test_find_default_aliased_interface_makes_a_guess_if_no_default(self): | ||
755 | 103 | sample_ip_route = [ | ||
756 | 104 | "10.0.0.0/24 dev eth2:4 proto kernel scope link src 10.0.0.11 " | ||
757 | 105 | "metric 2", | ||
758 | 106 | "10.1.0.0/24 dev virbr0 proto kernel scope link src 10.1.0.1", | ||
759 | 107 | "10.1.1.0/24 dev virbr1 proto kernel scope link src 10.1.1.1", | ||
760 | 108 | ] | ||
761 | 109 | self.assertEqual( | ||
762 | 110 | 'eth2:4', address.find_default_interface(sample_ip_route)) | ||
763 | 111 | |||
764 | 72 | def test_find_default_interface_returns_None_on_failure(self): | 112 | def test_find_default_interface_returns_None_on_failure(self): |
765 | 73 | self.assertIsNone(address.find_default_interface([])) | 113 | self.assertIsNone(address.find_default_interface([])) |
766 | 74 | 114 | ||
767 | 75 | 115 | ||
768 | === modified file 'src/provisioningserver/boot/__init__.py' | |||
769 | --- src/provisioningserver/boot/__init__.py 2014-06-02 11:57:58 +0000 | |||
770 | +++ src/provisioningserver/boot/__init__.py 2014-08-29 19:22:35 +0000 | |||
771 | @@ -99,6 +99,10 @@ | |||
772 | 99 | 99 | ||
773 | 100 | __metaclass__ = ABCMeta | 100 | __metaclass__ = ABCMeta |
774 | 101 | 101 | ||
775 | 102 | # Path prefix that is used for the pxelinux.cfg. Used for | ||
776 | 103 | # the dhcpd.conf that is generated. | ||
777 | 104 | path_prefix = None | ||
778 | 105 | |||
779 | 102 | @abstractproperty | 106 | @abstractproperty |
780 | 103 | def name(self): | 107 | def name(self): |
781 | 104 | """Name of the boot method.""" | 108 | """Name of the boot method.""" |
782 | @@ -223,12 +227,14 @@ | |||
783 | 223 | from provisioningserver.boot.pxe import PXEBootMethod | 227 | from provisioningserver.boot.pxe import PXEBootMethod |
784 | 224 | from provisioningserver.boot.uefi import UEFIBootMethod | 228 | from provisioningserver.boot.uefi import UEFIBootMethod |
785 | 225 | from provisioningserver.boot.powerkvm import PowerKVMBootMethod | 229 | from provisioningserver.boot.powerkvm import PowerKVMBootMethod |
786 | 230 | from provisioningserver.boot.powernv import PowerNVBootMethod | ||
787 | 226 | 231 | ||
788 | 227 | 232 | ||
789 | 228 | builtin_boot_methods = [ | 233 | builtin_boot_methods = [ |
790 | 229 | PXEBootMethod(), | 234 | PXEBootMethod(), |
791 | 230 | UEFIBootMethod(), | 235 | UEFIBootMethod(), |
792 | 231 | PowerKVMBootMethod(), | 236 | PowerKVMBootMethod(), |
793 | 237 | PowerNVBootMethod(), | ||
794 | 232 | ] | 238 | ] |
795 | 233 | for method in builtin_boot_methods: | 239 | for method in builtin_boot_methods: |
796 | 234 | BootMethodRegistry.register_item(method.name, method) | 240 | BootMethodRegistry.register_item(method.name, method) |
797 | 235 | 241 | ||
798 | === added file 'src/provisioningserver/boot/powernv.py' | |||
799 | --- src/provisioningserver/boot/powernv.py 1970-01-01 00:00:00 +0000 | |||
800 | +++ src/provisioningserver/boot/powernv.py 2014-08-29 19:22:35 +0000 | |||
801 | @@ -0,0 +1,158 @@ | |||
802 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
803 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
804 | 3 | |||
805 | 4 | """PowerNV Boot Method""" | ||
806 | 5 | |||
807 | 6 | from __future__ import ( | ||
808 | 7 | absolute_import, | ||
809 | 8 | print_function, | ||
810 | 9 | unicode_literals, | ||
811 | 10 | ) | ||
812 | 11 | |||
813 | 12 | str = None | ||
814 | 13 | |||
815 | 14 | __metaclass__ = type | ||
816 | 15 | __all__ = [ | ||
817 | 16 | 'PowerNVBootMethod', | ||
818 | 17 | ] | ||
819 | 18 | |||
820 | 19 | import re | ||
821 | 20 | |||
822 | 21 | from provisioningserver.boot import ( | ||
823 | 22 | BootMethod, | ||
824 | 23 | BytesReader, | ||
825 | 24 | get_parameters, | ||
826 | 25 | ) | ||
827 | 26 | from provisioningserver.boot.pxe import ( | ||
828 | 27 | ARP_HTYPE, | ||
829 | 28 | re_mac_address, | ||
830 | 29 | ) | ||
831 | 30 | from provisioningserver.kernel_opts import compose_kernel_command_line | ||
832 | 31 | from provisioningserver.utils import find_mac_via_arp | ||
833 | 32 | from tftp.backend import FilesystemReader | ||
834 | 33 | from twisted.python.context import get | ||
835 | 34 | |||
836 | 35 | # The pxelinux.cfg path is prefixed with the architecture for the | ||
837 | 36 | # PowerNV nodes. This prefix is set by the path-prefix dhcpd option. | ||
838 | 37 | # We assume that the ARP HTYPE (hardware type) that PXELINUX sends is | ||
839 | 38 | # always Ethernet. | ||
840 | 39 | re_config_file = r''' | ||
841 | 40 | # Optional leading slash(es). | ||
842 | 41 | ^/* | ||
843 | 42 | ppc64el # PowerNV pxe prefix, set by dhcpd | ||
844 | 43 | / | ||
845 | 44 | pxelinux[.]cfg # PXELINUX expects this. | ||
846 | 45 | / | ||
847 | 46 | (?: # either a MAC | ||
848 | 47 | {htype:02x} # ARP HTYPE. | ||
849 | 48 | - | ||
850 | 49 | (?P<mac>{re_mac_address.pattern}) # Capture MAC. | ||
851 | 50 | | # or "default" | ||
852 | 51 | default | ||
853 | 52 | ) | ||
854 | 53 | $ | ||
855 | 54 | ''' | ||
856 | 55 | |||
857 | 56 | re_config_file = re_config_file.format( | ||
858 | 57 | htype=ARP_HTYPE.ETHERNET, re_mac_address=re_mac_address) | ||
859 | 58 | re_config_file = re.compile(re_config_file, re.VERBOSE) | ||
860 | 59 | |||
861 | 60 | |||
862 | 61 | def format_bootif(mac): | ||
863 | 62 | """Formats a mac address into the BOOTIF format, expected by | ||
864 | 63 | the linux kernel.""" | ||
865 | 64 | mac = mac.replace(':', '-') | ||
866 | 65 | mac = mac.upper() | ||
867 | 66 | return '%02x-%s' % (ARP_HTYPE.ETHERNET, mac) | ||
868 | 67 | |||
869 | 68 | |||
870 | 69 | class PowerNVBootMethod(BootMethod): | ||
871 | 70 | |||
872 | 71 | name = "powernv" | ||
873 | 72 | template_subdir = "pxe" | ||
874 | 73 | bootloader_path = "pxelinux.0" | ||
875 | 74 | arch_octet = "00:0E" | ||
876 | 75 | path_prefix = "ppc64el/" | ||
877 | 76 | |||
878 | 77 | def get_remote_mac(self): | ||
879 | 78 | """Gets the requestors MAC address from arp cache. | ||
880 | 79 | |||
881 | 80 | This is used, when the pxelinux.cfg is requested without the mac | ||
882 | 81 | address appended. This is needed to inject the BOOTIF into the | ||
883 | 82 | pxelinux.cfg that is returned to the node. | ||
884 | 83 | """ | ||
885 | 84 | remote_host, remote_port = get("remote", (None, None)) | ||
886 | 85 | return find_mac_via_arp(remote_host) | ||
887 | 86 | |||
888 | 87 | def get_params(self, backend, path): | ||
889 | 88 | """Gets the matching parameters from the requested path.""" | ||
890 | 89 | match = re_config_file.match(path) | ||
891 | 90 | if match is not None: | ||
892 | 91 | return get_parameters(match) | ||
893 | 92 | if path.lstrip('/').startswith(self.path_prefix): | ||
894 | 93 | return {'path': path} | ||
895 | 94 | return None | ||
896 | 95 | |||
897 | 96 | def match_path(self, backend, path): | ||
898 | 97 | """Checks path for the configuration file that needs to be | ||
899 | 98 | generated. | ||
900 | 99 | |||
901 | 100 | :param backend: requesting backend | ||
902 | 101 | :param path: requested path | ||
903 | 102 | :returns: dict of match params from path, None if no match | ||
904 | 103 | """ | ||
905 | 104 | params = self.get_params(backend, path) | ||
906 | 105 | if params is None: | ||
907 | 106 | return None | ||
908 | 107 | params['arch'] = "ppc64el" | ||
909 | 108 | if 'mac' not in params: | ||
910 | 109 | mac = self.get_remote_mac() | ||
911 | 110 | if mac is not None: | ||
912 | 111 | params['mac'] = mac | ||
913 | 112 | return params | ||
914 | 113 | |||
915 | 114 | def get_reader(self, backend, kernel_params, **extra): | ||
916 | 115 | """Render a configuration file as a unicode string. | ||
917 | 116 | |||
918 | 117 | :param backend: requesting backend | ||
919 | 118 | :param kernel_params: An instance of `KernelParameters`. | ||
920 | 119 | :param extra: Allow for other arguments. This is a safety valve; | ||
921 | 120 | parameters generated in another component (for example, see | ||
922 | 121 | `TFTPBackend.get_config_reader`) won't cause this to break. | ||
923 | 122 | """ | ||
924 | 123 | # Due to the path prefix, all requested files from the client will | ||
925 | 124 | # contain that prefix. Removing the prefix from the path will return | ||
926 | 125 | # the correct path in the tftp root. | ||
927 | 126 | if 'path' in extra: | ||
928 | 127 | path = extra['path'] | ||
929 | 128 | path = path.replace(self.path_prefix, '', 1) | ||
930 | 129 | target_path = backend.base.descendant(path.split('/')) | ||
931 | 130 | return FilesystemReader(target_path) | ||
932 | 131 | |||
933 | 132 | # Return empty config for PowerNV local. PowerNV fails to | ||
934 | 133 | # support the LOCALBOOT flag. Empty config will allow it | ||
935 | 134 | # to select the first device. | ||
936 | 135 | if kernel_params.purpose == 'local': | ||
937 | 136 | return BytesReader("".encode("utf-8")) | ||
938 | 137 | |||
939 | 138 | template = self.get_template( | ||
940 | 139 | kernel_params.purpose, kernel_params.arch, | ||
941 | 140 | kernel_params.subarch) | ||
942 | 141 | namespace = self.compose_template_namespace(kernel_params) | ||
943 | 142 | |||
944 | 143 | # Modify the kernel_command to inject the BOOTIF. PowerNV fails to | ||
945 | 144 | # support the IPAPPEND pxelinux flag. | ||
946 | 145 | def kernel_command(params): | ||
947 | 146 | cmd_line = compose_kernel_command_line(params) | ||
948 | 147 | if 'mac' in extra: | ||
949 | 148 | mac = extra['mac'] | ||
950 | 149 | mac = format_bootif(mac) | ||
951 | 150 | return '%s BOOTIF=%s' % (cmd_line, mac) | ||
952 | 151 | return cmd_line | ||
953 | 152 | |||
954 | 153 | namespace['kernel_command'] = kernel_command | ||
955 | 154 | return BytesReader(template.substitute(namespace).encode("utf-8")) | ||
956 | 155 | |||
957 | 156 | def install_bootloader(self, destination): | ||
958 | 157 | """Does nothing. No extra boot files are required. All of the boot | ||
959 | 158 | files from PXEBootMethod will suffice.""" | ||
960 | 0 | 159 | ||
961 | === added file 'src/provisioningserver/boot/tests/test_powernv.py' | |||
962 | --- src/provisioningserver/boot/tests/test_powernv.py 1970-01-01 00:00:00 +0000 | |||
963 | +++ src/provisioningserver/boot/tests/test_powernv.py 2014-08-29 19:22:35 +0000 | |||
964 | @@ -0,0 +1,337 @@ | |||
965 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
966 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
967 | 3 | |||
968 | 4 | """Tests for `provisioningserver.boot.powernv`.""" | ||
969 | 5 | |||
970 | 6 | from __future__ import ( | ||
971 | 7 | absolute_import, | ||
972 | 8 | print_function, | ||
973 | 9 | unicode_literals, | ||
974 | 10 | ) | ||
975 | 11 | |||
976 | 12 | str = None | ||
977 | 13 | |||
978 | 14 | __metaclass__ = type | ||
979 | 15 | __all__ = [] | ||
980 | 16 | |||
981 | 17 | import os | ||
982 | 18 | import re | ||
983 | 19 | |||
984 | 20 | from maastesting.factory import factory | ||
985 | 21 | from maastesting.testcase import MAASTestCase | ||
986 | 22 | from provisioningserver.boot import BytesReader | ||
987 | 23 | from provisioningserver.boot.powernv import ( | ||
988 | 24 | ARP_HTYPE, | ||
989 | 25 | format_bootif, | ||
990 | 26 | PowerNVBootMethod, | ||
991 | 27 | re_config_file, | ||
992 | 28 | ) | ||
993 | 29 | from provisioningserver.boot.tests.test_pxe import parse_pxe_config | ||
994 | 30 | from provisioningserver.boot.tftppath import compose_image_path | ||
995 | 31 | from provisioningserver.testing.config import set_tftp_root | ||
996 | 32 | from provisioningserver.tests.test_kernel_opts import make_kernel_parameters | ||
997 | 33 | from provisioningserver.tftp import TFTPBackend | ||
998 | 34 | from testtools.matchers import ( | ||
999 | 35 | IsInstance, | ||
1000 | 36 | MatchesAll, | ||
1001 | 37 | MatchesRegex, | ||
1002 | 38 | Not, | ||
1003 | 39 | StartsWith, | ||
1004 | 40 | ) | ||
1005 | 41 | |||
1006 | 42 | |||
1007 | 43 | def compose_config_path(mac): | ||
1008 | 44 | """Compose the TFTP path for a PowerNV PXE configuration file. | ||
1009 | 45 | |||
1010 | 46 | The path returned is relative to the TFTP root, as it would be | ||
1011 | 47 | identified by clients on the network. | ||
1012 | 48 | |||
1013 | 49 | :param mac: A MAC address, in IEEE 802 hyphen-separated form, | ||
1014 | 50 | corresponding to the machine for which this configuration is | ||
1015 | 51 | relevant. This relates to PXELINUX's lookup protocol. | ||
1016 | 52 | :return: Path for the corresponding PXE config file as exposed over | ||
1017 | 53 | TFTP. | ||
1018 | 54 | """ | ||
1019 | 55 | # Not using os.path.join: this is a TFTP path, not a native path. Yes, in | ||
1020 | 56 | # practice for us they're the same. We always assume that the ARP HTYPE | ||
1021 | 57 | # (hardware type) that PXELINUX sends is Ethernet. | ||
1022 | 58 | return "ppc64el/pxelinux.cfg/{htype:02x}-{mac}".format( | ||
1023 | 59 | htype=ARP_HTYPE.ETHERNET, mac=mac) | ||
1024 | 60 | |||
1025 | 61 | |||
1026 | 62 | def get_example_path_and_components(): | ||
1027 | 63 | """Return a plausible path and its components. | ||
1028 | 64 | |||
1029 | 65 | The path is intended to match `re_config_file`, and the components are | ||
1030 | 66 | the expected groups from a match. | ||
1031 | 67 | """ | ||
1032 | 68 | components = {"mac": factory.getRandomMACAddress("-")} | ||
1033 | 69 | config_path = compose_config_path(components["mac"]) | ||
1034 | 70 | return config_path, components | ||
1035 | 71 | |||
1036 | 72 | |||
1037 | 73 | class TestPowerNVBootMethod(MAASTestCase): | ||
1038 | 74 | |||
1039 | 75 | def make_tftp_root(self): | ||
1040 | 76 | """Set, and return, a temporary TFTP root directory.""" | ||
1041 | 77 | tftproot = self.make_dir() | ||
1042 | 78 | self.useFixture(set_tftp_root(tftproot)) | ||
1043 | 79 | return tftproot | ||
1044 | 80 | |||
1045 | 81 | def test_compose_config_path_follows_maas_pxe_directory_layout(self): | ||
1046 | 82 | name = factory.make_name('config') | ||
1047 | 83 | self.assertEqual( | ||
1048 | 84 | 'ppc64el/pxelinux.cfg/%02x-%s' % (ARP_HTYPE.ETHERNET, name), | ||
1049 | 85 | compose_config_path(name)) | ||
1050 | 86 | |||
1051 | 87 | def test_compose_config_path_does_not_include_tftp_root(self): | ||
1052 | 88 | tftproot = self.make_tftp_root() | ||
1053 | 89 | name = factory.make_name('config') | ||
1054 | 90 | self.assertThat( | ||
1055 | 91 | compose_config_path(name), | ||
1056 | 92 | Not(StartsWith(tftproot))) | ||
1057 | 93 | |||
1058 | 94 | def test_bootloader_path(self): | ||
1059 | 95 | method = PowerNVBootMethod() | ||
1060 | 96 | self.assertEqual('pxelinux.0', method.bootloader_path) | ||
1061 | 97 | |||
1062 | 98 | def test_bootloader_path_does_not_include_tftp_root(self): | ||
1063 | 99 | tftproot = self.make_tftp_root() | ||
1064 | 100 | method = PowerNVBootMethod() | ||
1065 | 101 | self.assertThat( | ||
1066 | 102 | method.bootloader_path, | ||
1067 | 103 | Not(StartsWith(tftproot))) | ||
1068 | 104 | |||
1069 | 105 | def test_name(self): | ||
1070 | 106 | method = PowerNVBootMethod() | ||
1071 | 107 | self.assertEqual('powernv', method.name) | ||
1072 | 108 | |||
1073 | 109 | def test_template_subdir(self): | ||
1074 | 110 | method = PowerNVBootMethod() | ||
1075 | 111 | self.assertEqual('pxe', method.template_subdir) | ||
1076 | 112 | |||
1077 | 113 | def test_arch_octet(self): | ||
1078 | 114 | method = PowerNVBootMethod() | ||
1079 | 115 | self.assertEqual('00:0E', method.arch_octet) | ||
1080 | 116 | |||
1081 | 117 | def test_path_prefix(self): | ||
1082 | 118 | method = PowerNVBootMethod() | ||
1083 | 119 | self.assertEqual('ppc64el/', method.path_prefix) | ||
1084 | 120 | |||
1085 | 121 | |||
1086 | 122 | class TestPowerNVBootMethodMatchPath(MAASTestCase): | ||
1087 | 123 | """Tests for | ||
1088 | 124 | `provisioningserver.boot.powernv.PowerNVBootMethod.match_path`. | ||
1089 | 125 | """ | ||
1090 | 126 | |||
1091 | 127 | def test_match_path_pxe_config_with_mac(self): | ||
1092 | 128 | method = PowerNVBootMethod() | ||
1093 | 129 | config_path, expected = get_example_path_and_components() | ||
1094 | 130 | params = method.match_path(None, config_path) | ||
1095 | 131 | expected['arch'] = 'ppc64el' | ||
1096 | 132 | self.assertEqual(expected, params) | ||
1097 | 133 | |||
1098 | 134 | def test_match_path_pxe_config_without_mac(self): | ||
1099 | 135 | method = PowerNVBootMethod() | ||
1100 | 136 | fake_mac = factory.getRandomMACAddress() | ||
1101 | 137 | self.patch(method, 'get_remote_mac').return_value = fake_mac | ||
1102 | 138 | config_path = 'ppc64el/pxelinux.cfg/default' | ||
1103 | 139 | params = method.match_path(None, config_path) | ||
1104 | 140 | expected = { | ||
1105 | 141 | 'arch': 'ppc64el', | ||
1106 | 142 | 'mac': fake_mac, | ||
1107 | 143 | } | ||
1108 | 144 | self.assertEqual(expected, params) | ||
1109 | 145 | |||
1110 | 146 | def test_match_path_pxe_prefix_request(self): | ||
1111 | 147 | method = PowerNVBootMethod() | ||
1112 | 148 | fake_mac = factory.getRandomMACAddress() | ||
1113 | 149 | self.patch(method, 'get_remote_mac').return_value = fake_mac | ||
1114 | 150 | file_path = 'ppc64el/file' | ||
1115 | 151 | params = method.match_path(None, file_path) | ||
1116 | 152 | expected = { | ||
1117 | 153 | 'arch': 'ppc64el', | ||
1118 | 154 | 'mac': fake_mac, | ||
1119 | 155 | 'path': file_path, | ||
1120 | 156 | } | ||
1121 | 157 | self.assertEqual(expected, params) | ||
1122 | 158 | |||
1123 | 159 | |||
1124 | 160 | class TestPowerNVBootMethodRenderConfig(MAASTestCase): | ||
1125 | 161 | """Tests for | ||
1126 | 162 | `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader` | ||
1127 | 163 | """ | ||
1128 | 164 | |||
1129 | 165 | def test_get_reader_install(self): | ||
1130 | 166 | # Given the right configuration options, the PXE configuration is | ||
1131 | 167 | # correctly rendered. | ||
1132 | 168 | method = PowerNVBootMethod() | ||
1133 | 169 | params = make_kernel_parameters(self, purpose="install") | ||
1134 | 170 | output = method.get_reader(backend=None, kernel_params=params) | ||
1135 | 171 | # The output is a BytesReader. | ||
1136 | 172 | self.assertThat(output, IsInstance(BytesReader)) | ||
1137 | 173 | output = output.read(10000) | ||
1138 | 174 | # The template has rendered without error. PXELINUX configurations | ||
1139 | 175 | # typically start with a DEFAULT line. | ||
1140 | 176 | self.assertThat(output, StartsWith("DEFAULT ")) | ||
1141 | 177 | # The PXE parameters are all set according to the options. | ||
1142 | 178 | image_dir = compose_image_path( | ||
1143 | 179 | arch=params.arch, subarch=params.subarch, | ||
1144 | 180 | release=params.release, label=params.label) | ||
1145 | 181 | self.assertThat( | ||
1146 | 182 | output, MatchesAll( | ||
1147 | 183 | MatchesRegex( | ||
1148 | 184 | r'.*^\s+KERNEL %s/di-kernel$' % re.escape(image_dir), | ||
1149 | 185 | re.MULTILINE | re.DOTALL), | ||
1150 | 186 | MatchesRegex( | ||
1151 | 187 | r'.*^\s+INITRD %s/di-initrd$' % re.escape(image_dir), | ||
1152 | 188 | re.MULTILINE | re.DOTALL), | ||
1153 | 189 | MatchesRegex( | ||
1154 | 190 | r'.*^\s+APPEND .+?$', | ||
1155 | 191 | re.MULTILINE | re.DOTALL))) | ||
1156 | 192 | |||
1157 | 193 | def test_get_reader_with_extra_arguments_does_not_affect_output(self): | ||
1158 | 194 | # get_reader() allows any keyword arguments as a safety valve. | ||
1159 | 195 | method = PowerNVBootMethod() | ||
1160 | 196 | options = { | ||
1161 | 197 | "backend": None, | ||
1162 | 198 | "kernel_params": make_kernel_parameters(self, purpose="install"), | ||
1163 | 199 | } | ||
1164 | 200 | # Capture the output before sprinking in some random options. | ||
1165 | 201 | output_before = method.get_reader(**options).read(10000) | ||
1166 | 202 | # Sprinkle some magic in. | ||
1167 | 203 | options.update( | ||
1168 | 204 | (factory.make_name("name"), factory.make_name("value")) | ||
1169 | 205 | for _ in range(10)) | ||
1170 | 206 | # Capture the output after sprinking in some random options. | ||
1171 | 207 | output_after = method.get_reader(**options).read(10000) | ||
1172 | 208 | # The generated template is the same. | ||
1173 | 209 | self.assertEqual(output_before, output_after) | ||
1174 | 210 | |||
1175 | 211 | def test_get_reader_with_local_purpose(self): | ||
1176 | 212 | # If purpose is "local", output should be empty string. | ||
1177 | 213 | method = PowerNVBootMethod() | ||
1178 | 214 | options = { | ||
1179 | 215 | "backend": None, | ||
1180 | 216 | "kernel_params": make_kernel_parameters(purpose="local"), | ||
1181 | 217 | } | ||
1182 | 218 | output = method.get_reader(**options).read(10000) | ||
1183 | 219 | self.assertIn("", output) | ||
1184 | 220 | |||
1185 | 221 | def test_get_reader_appends_bootif(self): | ||
1186 | 222 | method = PowerNVBootMethod() | ||
1187 | 223 | fake_mac = factory.getRandomMACAddress() | ||
1188 | 224 | params = make_kernel_parameters(self, purpose="install") | ||
1189 | 225 | output = method.get_reader( | ||
1190 | 226 | backend=None, kernel_params=params, arch='ppc64el', mac=fake_mac) | ||
1191 | 227 | output = output.read(10000) | ||
1192 | 228 | config = parse_pxe_config(output) | ||
1193 | 229 | expected = 'BOOTIF=%s' % format_bootif(fake_mac) | ||
1194 | 230 | self.assertIn(expected, config['execute']['APPEND']) | ||
1195 | 231 | |||
1196 | 232 | |||
1197 | 233 | class TestPowerNVBootMethodPathPrefix(MAASTestCase): | ||
1198 | 234 | """Tests for | ||
1199 | 235 | `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`. | ||
1200 | 236 | """ | ||
1201 | 237 | |||
1202 | 238 | def test_get_reader_path_prefix(self): | ||
1203 | 239 | data = factory.getRandomString().encode("ascii") | ||
1204 | 240 | temp_file = self.make_file(name="example", contents=data) | ||
1205 | 241 | temp_dir = os.path.dirname(temp_file) | ||
1206 | 242 | backend = TFTPBackend(temp_dir, "http://nowhere.example.com/") | ||
1207 | 243 | method = PowerNVBootMethod() | ||
1208 | 244 | options = { | ||
1209 | 245 | 'backend': backend, | ||
1210 | 246 | 'kernel_params': make_kernel_parameters(), | ||
1211 | 247 | 'path': 'ppc64el/example', | ||
1212 | 248 | } | ||
1213 | 249 | reader = method.get_reader(**options) | ||
1214 | 250 | self.addCleanup(reader.finish) | ||
1215 | 251 | self.assertEqual(len(data), reader.size) | ||
1216 | 252 | self.assertEqual(data, reader.read(len(data))) | ||
1217 | 253 | self.assertEqual(b"", reader.read(1)) | ||
1218 | 254 | |||
1219 | 255 | def test_get_reader_path_prefix_only_removes_first_occurrence(self): | ||
1220 | 256 | data = factory.getRandomString().encode("ascii") | ||
1221 | 257 | temp_dir = self.make_dir() | ||
1222 | 258 | temp_subdir = os.path.join(temp_dir, 'ppc64el') | ||
1223 | 259 | os.mkdir(temp_subdir) | ||
1224 | 260 | factory.make_file(temp_subdir, "example", data) | ||
1225 | 261 | backend = TFTPBackend(temp_dir, "http://nowhere.example.com/") | ||
1226 | 262 | method = PowerNVBootMethod() | ||
1227 | 263 | options = { | ||
1228 | 264 | 'backend': backend, | ||
1229 | 265 | 'kernel_params': make_kernel_parameters(), | ||
1230 | 266 | 'path': 'ppc64el/ppc64el/example', | ||
1231 | 267 | } | ||
1232 | 268 | reader = method.get_reader(**options) | ||
1233 | 269 | self.addCleanup(reader.finish) | ||
1234 | 270 | self.assertEqual(len(data), reader.size) | ||
1235 | 271 | self.assertEqual(data, reader.read(len(data))) | ||
1236 | 272 | self.assertEqual(b"", reader.read(1)) | ||
1237 | 273 | |||
1238 | 274 | |||
1239 | 275 | class TestPowerNVBootMethodRegex(MAASTestCase): | ||
1240 | 276 | """Tests for | ||
1241 | 277 | `provisioningserver.boot.powernv.PowerNVBootMethod.re_config_file`. | ||
1242 | 278 | """ | ||
1243 | 279 | |||
1244 | 280 | def test_re_config_file_is_compatible_with_config_path_generator(self): | ||
1245 | 281 | # The regular expression for extracting components of the file path is | ||
1246 | 282 | # compatible with the PXE config path generator. | ||
1247 | 283 | for iteration in range(10): | ||
1248 | 284 | config_path, args = get_example_path_and_components() | ||
1249 | 285 | match = re_config_file.match(config_path) | ||
1250 | 286 | self.assertIsNotNone(match, config_path) | ||
1251 | 287 | self.assertEqual(args, match.groupdict()) | ||
1252 | 288 | |||
1253 | 289 | def test_re_config_file_with_leading_slash(self): | ||
1254 | 290 | # The regular expression for extracting components of the file path | ||
1255 | 291 | # doesn't care if there's a leading forward slash; the TFTP server is | ||
1256 | 292 | # easy on this point, so it makes sense to be also. | ||
1257 | 293 | config_path, args = get_example_path_and_components() | ||
1258 | 294 | # Ensure there's a leading slash. | ||
1259 | 295 | config_path = "/" + config_path.lstrip("/") | ||
1260 | 296 | match = re_config_file.match(config_path) | ||
1261 | 297 | self.assertIsNotNone(match, config_path) | ||
1262 | 298 | self.assertEqual(args, match.groupdict()) | ||
1263 | 299 | |||
1264 | 300 | def test_re_config_file_without_leading_slash(self): | ||
1265 | 301 | # The regular expression for extracting components of the file path | ||
1266 | 302 | # doesn't care if there's no leading forward slash; the TFTP server is | ||
1267 | 303 | # easy on this point, so it makes sense to be also. | ||
1268 | 304 | config_path, args = get_example_path_and_components() | ||
1269 | 305 | # Ensure there's no leading slash. | ||
1270 | 306 | config_path = config_path.lstrip("/") | ||
1271 | 307 | match = re_config_file.match(config_path) | ||
1272 | 308 | self.assertIsNotNone(match, config_path) | ||
1273 | 309 | self.assertEqual(args, match.groupdict()) | ||
1274 | 310 | |||
1275 | 311 | def test_re_config_file_matches_classic_pxelinux_cfg(self): | ||
1276 | 312 | # The default config path is simply "pxelinux.cfg" (without | ||
1277 | 313 | # leading slash). The regex matches this. | ||
1278 | 314 | mac = 'aa-bb-cc-dd-ee-ff' | ||
1279 | 315 | match = re_config_file.match('ppc64el/pxelinux.cfg/01-%s' % mac) | ||
1280 | 316 | self.assertIsNotNone(match) | ||
1281 | 317 | self.assertEqual({'mac': mac}, match.groupdict()) | ||
1282 | 318 | |||
1283 | 319 | def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self): | ||
1284 | 320 | mac = 'aa-bb-cc-dd-ee-ff' | ||
1285 | 321 | match = re_config_file.match('/ppc64el/pxelinux.cfg/01-%s' % mac) | ||
1286 | 322 | self.assertIsNotNone(match) | ||
1287 | 323 | self.assertEqual({'mac': mac}, match.groupdict()) | ||
1288 | 324 | |||
1289 | 325 | def test_re_config_file_does_not_match_non_config_file(self): | ||
1290 | 326 | self.assertIsNone(re_config_file.match('ppc64el/pxelinux.cfg/kernel')) | ||
1291 | 327 | |||
1292 | 328 | def test_re_config_file_does_not_match_file_in_root(self): | ||
1293 | 329 | self.assertIsNone(re_config_file.match('01-aa-bb-cc-dd-ee-ff')) | ||
1294 | 330 | |||
1295 | 331 | def test_re_config_file_does_not_match_file_not_in_pxelinux_cfg(self): | ||
1296 | 332 | self.assertIsNone(re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff')) | ||
1297 | 333 | |||
1298 | 334 | def test_re_config_file_with_default(self): | ||
1299 | 335 | match = re_config_file.match('ppc64el/pxelinux.cfg/default') | ||
1300 | 336 | self.assertIsNotNone(match) | ||
1301 | 337 | self.assertEqual({'mac': None}, match.groupdict()) | ||
1302 | 0 | 338 | ||
1303 | === modified file 'src/provisioningserver/dhcp/config.py' | |||
1304 | --- src/provisioningserver/dhcp/config.py 2014-04-03 13:45:02 +0000 | |||
1305 | +++ src/provisioningserver/dhcp/config.py 2014-08-29 19:22:35 +0000 | |||
1306 | @@ -33,16 +33,22 @@ | |||
1307 | 33 | 33 | ||
1308 | 34 | # Used to generate the conditional bootloader behaviour | 34 | # Used to generate the conditional bootloader behaviour |
1309 | 35 | CONDITIONAL_BOOTLOADER = """ | 35 | CONDITIONAL_BOOTLOADER = """ |
1313 | 36 | {behaviour} option arch = {arch_octet} {{ | 36 | {{behaviour}} option arch = {{arch_octet}} { |
1314 | 37 | filename \"{bootloader}\"; | 37 | filename \"{{bootloader}}\"; |
1315 | 38 | }} | 38 | {{if path_prefix}} |
1316 | 39 | option path-prefix \"{{path_prefix}}\"; | ||
1317 | 40 | {{endif}} | ||
1318 | 41 | } | ||
1319 | 39 | """ | 42 | """ |
1320 | 40 | 43 | ||
1321 | 41 | # Used to generate the PXEBootLoader special case | 44 | # Used to generate the PXEBootLoader special case |
1322 | 42 | PXE_BOOTLOADER = """ | 45 | PXE_BOOTLOADER = """ |
1326 | 43 | else {{ | 46 | else { |
1327 | 44 | filename \"{bootloader}\"; | 47 | filename \"{{bootloader}}\"; |
1328 | 45 | }} | 48 | {{if path_prefix}} |
1329 | 49 | option path-prefix \"{{path_prefix}}\"; | ||
1330 | 50 | {{endif}} | ||
1331 | 51 | } | ||
1332 | 46 | """ | 52 | """ |
1333 | 47 | 53 | ||
1334 | 48 | 54 | ||
1335 | @@ -55,9 +61,13 @@ | |||
1336 | 55 | behaviour = chain(["if"], repeat("elsif")) | 61 | behaviour = chain(["if"], repeat("elsif")) |
1337 | 56 | for name, method in BootMethodRegistry: | 62 | for name, method in BootMethodRegistry: |
1338 | 57 | if name != "pxe": | 63 | if name != "pxe": |
1342 | 58 | output += CONDITIONAL_BOOTLOADER.format( | 64 | output += tempita.sub( |
1343 | 59 | behaviour=next(behaviour), arch_octet=method.arch_octet, | 65 | CONDITIONAL_BOOTLOADER, |
1344 | 60 | bootloader=method.bootloader_path).strip() + ' ' | 66 | behaviour=next(behaviour), |
1345 | 67 | arch_octet=method.arch_octet, | ||
1346 | 68 | bootloader=method.bootloader_path, | ||
1347 | 69 | path_prefix=method.path_prefix, | ||
1348 | 70 | ).strip() + ' ' | ||
1349 | 61 | 71 | ||
1350 | 62 | # The PXEBootMethod is used in an else statement for the generated | 72 | # The PXEBootMethod is used in an else statement for the generated |
1351 | 63 | # dhcpd config. This ensures that a booting node that does not | 73 | # dhcpd config. This ensures that a booting node that does not |
1352 | @@ -65,8 +75,11 @@ | |||
1353 | 65 | # pxelinux can still boot. | 75 | # pxelinux can still boot. |
1354 | 66 | pxe_method = BootMethodRegistry.get_item('pxe') | 76 | pxe_method = BootMethodRegistry.get_item('pxe') |
1355 | 67 | if pxe_method is not None: | 77 | if pxe_method is not None: |
1358 | 68 | output += PXE_BOOTLOADER.format( | 78 | output += tempita.sub( |
1359 | 69 | bootloader=pxe_method.bootloader_path).strip() | 79 | PXE_BOOTLOADER, |
1360 | 80 | bootloader=pxe_method.bootloader_path, | ||
1361 | 81 | path_prefix=pxe_method.path_prefix, | ||
1362 | 82 | ).strip() | ||
1363 | 70 | return output.strip() | 83 | return output.strip() |
1364 | 71 | 84 | ||
1365 | 72 | 85 | ||
1366 | 73 | 86 | ||
1367 | === modified file 'src/provisioningserver/driver/__init__.py' | |||
1368 | --- src/provisioningserver/driver/__init__.py 2014-06-02 11:57:58 +0000 | |||
1369 | +++ src/provisioningserver/driver/__init__.py 2014-08-29 19:22:35 +0000 | |||
1370 | @@ -134,12 +134,25 @@ | |||
1371 | 134 | Architecture(name="i386/generic", description="i386"), | 134 | Architecture(name="i386/generic", description="i386"), |
1372 | 135 | Architecture(name="amd64/generic", description="amd64"), | 135 | Architecture(name="amd64/generic", description="amd64"), |
1373 | 136 | Architecture( | 136 | Architecture( |
1374 | 137 | name="arm64/generic", description="arm64/generic", | ||
1375 | 138 | pxealiases=["arm"]), | ||
1376 | 139 | Architecture( | ||
1377 | 140 | name="arm64/xgene-uboot", description="arm64/xgene-uboot", | ||
1378 | 141 | pxealiases=["arm"]), | ||
1379 | 142 | Architecture( | ||
1380 | 137 | name="armhf/highbank", description="armhf/highbank", | 143 | name="armhf/highbank", description="armhf/highbank", |
1381 | 138 | pxealiases=["arm"], kernel_options=["console=ttyAMA0"]), | 144 | pxealiases=["arm"], kernel_options=["console=ttyAMA0"]), |
1382 | 139 | Architecture( | 145 | Architecture( |
1383 | 140 | name="armhf/generic", description="armhf/generic", | 146 | name="armhf/generic", description="armhf/generic", |
1384 | 141 | pxealiases=["arm"], kernel_options=["console=ttyAMA0"]), | 147 | pxealiases=["arm"], kernel_options=["console=ttyAMA0"]), |
1386 | 142 | Architecture(name="ppc64el/generic", description="ppc64el"), | 148 | # PPC64EL needs a rootdelay for PowerNV. The disk controller |
1387 | 149 | # in the hardware, takes a little bit longer to come up then | ||
1388 | 150 | # the initrd wants to wait. Set this to 60 seconds, just to | ||
1389 | 151 | # give the booting machine enough time. This doesn't slow down | ||
1390 | 152 | # the booting process, it just increases the timeout. | ||
1391 | 153 | Architecture( | ||
1392 | 154 | name="ppc64el/generic", description="ppc64el", | ||
1393 | 155 | kernel_options=['rootdelay=60']), | ||
1394 | 143 | ] | 156 | ] |
1395 | 144 | for arch in builtin_architectures: | 157 | for arch in builtin_architectures: |
1396 | 145 | ArchitectureRegistry.register_item(arch.name, arch) | 158 | ArchitectureRegistry.register_item(arch.name, arch) |
1397 | 146 | 159 | ||
1398 | === added directory 'src/provisioningserver/drivers' | |||
1399 | === added file 'src/provisioningserver/drivers/__init__.py' | |||
1400 | === added directory 'src/provisioningserver/drivers/hardware' | |||
1401 | === added file 'src/provisioningserver/drivers/hardware/__init__.py' | |||
1402 | === added file 'src/provisioningserver/drivers/hardware/mscm.py' | |||
1403 | --- src/provisioningserver/drivers/hardware/mscm.py 1970-01-01 00:00:00 +0000 | |||
1404 | +++ src/provisioningserver/drivers/hardware/mscm.py 2014-08-29 19:22:35 +0000 | |||
1405 | @@ -0,0 +1,187 @@ | |||
1406 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
1407 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
1408 | 3 | |||
1409 | 4 | """Support for managing nodes via the Moonshot HP iLO Chassis Manager CLI. | ||
1410 | 5 | |||
1411 | 6 | This module provides support for interacting with HP Moonshot iLO Chassis | ||
1412 | 7 | Management (MSCM) CLI via SSH, and for using that support to allow MAAS to | ||
1413 | 8 | manage systems via iLO. | ||
1414 | 9 | """ | ||
1415 | 10 | |||
1416 | 11 | from __future__ import ( | ||
1417 | 12 | absolute_import, | ||
1418 | 13 | print_function, | ||
1419 | 14 | unicode_literals, | ||
1420 | 15 | ) | ||
1421 | 16 | str = None | ||
1422 | 17 | |||
1423 | 18 | __metaclass__ = type | ||
1424 | 19 | __all__ = [ | ||
1425 | 20 | 'power_control_mscm', | ||
1426 | 21 | 'probe_and_enlist_mscm', | ||
1427 | 22 | ] | ||
1428 | 23 | |||
1429 | 24 | import re | ||
1430 | 25 | |||
1431 | 26 | from paramiko import ( | ||
1432 | 27 | AutoAddPolicy, | ||
1433 | 28 | SSHClient, | ||
1434 | 29 | ) | ||
1435 | 30 | import provisioningserver.custom_hardware.utils as utils | ||
1436 | 31 | |||
1437 | 32 | |||
1438 | 33 | cartridge_mapping = { | ||
1439 | 34 | 'ProLiant Moonshot Cartridge': 'amd64/generic', | ||
1440 | 35 | 'ProLiant m300 Server Cartridge': 'amd64/generic', | ||
1441 | 36 | 'ProLiant m350 Server Cartridge': 'amd64/generic', | ||
1442 | 37 | 'ProLiant m400 Server Cartridge': 'arm64/xgene-uboot', | ||
1443 | 38 | 'ProLiant m500 Server Cartridge': 'amd64/generic', | ||
1444 | 39 | 'ProLiant m710 Server Cartridge': 'amd64/generic', | ||
1445 | 40 | 'ProLiant m800 Server Cartridge': 'armhf/keystone', | ||
1446 | 41 | 'Default': 'arm64/generic', | ||
1447 | 42 | } | ||
1448 | 43 | |||
1449 | 44 | |||
1450 | 45 | class MSCM_CLI_API(object): | ||
1451 | 46 | """An API for interacting with the Moonshot iLO CM CLI.""" | ||
1452 | 47 | |||
1453 | 48 | def __init__(self, host, username, password): | ||
1454 | 49 | """MSCM_CLI_API Constructor.""" | ||
1455 | 50 | self.host = host | ||
1456 | 51 | self.username = username | ||
1457 | 52 | self.password = password | ||
1458 | 53 | self._ssh = SSHClient() | ||
1459 | 54 | self._ssh.set_missing_host_key_policy(AutoAddPolicy()) | ||
1460 | 55 | |||
1461 | 56 | def _run_cli_command(self, command): | ||
1462 | 57 | """Run a single command and return unparsed text from stdout.""" | ||
1463 | 58 | self._ssh.connect( | ||
1464 | 59 | self.host, username=self.username, password=self.password) | ||
1465 | 60 | try: | ||
1466 | 61 | _, stdout, _ = self._ssh.exec_command(command) | ||
1467 | 62 | output = stdout.read() | ||
1468 | 63 | finally: | ||
1469 | 64 | self._ssh.close() | ||
1470 | 65 | |||
1471 | 66 | return output | ||
1472 | 67 | |||
1473 | 68 | def discover_nodes(self): | ||
1474 | 69 | """Discover all available nodes. | ||
1475 | 70 | |||
1476 | 71 | Example of stdout from running "show node list": | ||
1477 | 72 | |||
1478 | 73 | 'show node list\r\r\nSlot ID Proc Manufacturer | ||
1479 | 74 | Architecture Memory Power Health\r\n---- | ||
1480 | 75 | ----- ---------------------- -------------------- | ||
1481 | 76 | ------ ----- ------\r\n 01 c1n1 Intel Corporation | ||
1482 | 77 | x86 Architecture 32 GB On OK \r\n 02 c2n1 | ||
1483 | 78 | N/A No Asset Information \r\n\r\n' | ||
1484 | 79 | |||
1485 | 80 | The regex 'c\d+n\d' is finding the node_id's c1-45n1-8 | ||
1486 | 81 | """ | ||
1487 | 82 | node_list = self._run_cli_command("show node list") | ||
1488 | 83 | return re.findall(r'c\d+n\d', node_list) | ||
1489 | 84 | |||
1490 | 85 | def get_node_macaddr(self, node_id): | ||
1491 | 86 | """Get node MAC address(es). | ||
1492 | 87 | |||
1493 | 88 | Example of stdout from running "show node macaddr <node_id>": | ||
1494 | 89 | |||
1495 | 90 | 'show node macaddr c1n1\r\r\nSlot ID NIC 1 (Switch A) | ||
1496 | 91 | NIC 2 (Switch B) NIC 3 (Switch A) NIC 4 (Switch B)\r\n | ||
1497 | 92 | ---- ----- ----------------- ----------------- ----------------- | ||
1498 | 93 | -----------------\r\n 1 c1n1 a0:1d:48:b5:04:34 a0:1d:48:b5:04:35 | ||
1499 | 94 | a0:1d:48:b5:04:36 a0:1d:48:b5:04:37\r\n\r\n\r\n' | ||
1500 | 95 | |||
1501 | 96 | The regex '[\:]'.join(['[0-9A-F]{1,2}'] * 6) is finding | ||
1502 | 97 | the MAC Addresses for the given node_id. | ||
1503 | 98 | """ | ||
1504 | 99 | macs = self._run_cli_command("show node macaddr %s" % node_id) | ||
1505 | 100 | return re.findall(r':'.join(['[0-9a-f]{2}'] * 6), macs) | ||
1506 | 101 | |||
1507 | 102 | def get_node_arch(self, node_id): | ||
1508 | 103 | """Get node architecture. | ||
1509 | 104 | |||
1510 | 105 | Example of stdout from running "show node info <node_id>": | ||
1511 | 106 | |||
1512 | 107 | 'show node info c1n1\r\r\n\r\nCartridge #1 \r\n Type: Compute\r\n | ||
1513 | 108 | Manufacturer: HP\r\n Product Name: ProLiant m500 Server Cartridge\r\n' | ||
1514 | 109 | |||
1515 | 110 | Parsing this retrieves 'ProLiant m500 Server Cartridge' | ||
1516 | 111 | """ | ||
1517 | 112 | node_detail = self._run_cli_command("show node info %s" % node_id) | ||
1518 | 113 | cartridge = node_detail.split('Product Name: ')[1].splitlines()[0] | ||
1519 | 114 | if cartridge in cartridge_mapping: | ||
1520 | 115 | return cartridge_mapping[cartridge] | ||
1521 | 116 | else: | ||
1522 | 117 | return cartridge_mapping['Default'] | ||
1523 | 118 | |||
1524 | 119 | def get_node_power_status(self, node_id): | ||
1525 | 120 | """Get power state of node (on/off). | ||
1526 | 121 | |||
1527 | 122 | Example of stdout from running "show node power <node_id>": | ||
1528 | 123 | |||
1529 | 124 | 'show node power c1n1\r\r\n\r\nCartridge #1\r\n Node #1\r\n | ||
1530 | 125 | Power State: On\r\n' | ||
1531 | 126 | |||
1532 | 127 | Parsing this retrieves 'On' | ||
1533 | 128 | """ | ||
1534 | 129 | power_state = self._run_cli_command("show node power %s" % node_id) | ||
1535 | 130 | return power_state.split('Power State: ')[1].splitlines()[0] | ||
1536 | 131 | |||
1537 | 132 | def power_node_on(self, node_id): | ||
1538 | 133 | """Power node on.""" | ||
1539 | 134 | return self._run_cli_command("set node power on %s" % node_id) | ||
1540 | 135 | |||
1541 | 136 | def power_node_off(self, node_id): | ||
1542 | 137 | """Power node off.""" | ||
1543 | 138 | return self._run_cli_command("set node power off force %s" % node_id) | ||
1544 | 139 | |||
1545 | 140 | def configure_node_boot_m2(self, node_id): | ||
1546 | 141 | """Configure HDD boot for node.""" | ||
1547 | 142 | return self._run_cli_command("set node boot M.2 %s" % node_id) | ||
1548 | 143 | |||
1549 | 144 | def configure_node_bootonce_pxe(self, node_id): | ||
1550 | 145 | """Configure PXE boot for node once.""" | ||
1551 | 146 | return self._run_cli_command("set node bootonce pxe %s" % node_id) | ||
1552 | 147 | |||
1553 | 148 | |||
1554 | 149 | def power_control_mscm(host, username, password, node_id, power_change): | ||
1555 | 150 | """Handle calls from the power template for nodes with a power type | ||
1556 | 151 | of 'mscm'. | ||
1557 | 152 | """ | ||
1558 | 153 | mscm = MSCM_CLI_API(host, username, password) | ||
1559 | 154 | power_status = mscm.get_node_power_status(node_id) | ||
1560 | 155 | |||
1561 | 156 | if power_change == 'off': | ||
1562 | 157 | mscm.power_node_off(node_id) | ||
1563 | 158 | return | ||
1564 | 159 | |||
1565 | 160 | if power_change != 'on': | ||
1566 | 161 | raise AssertionError('Unexpected maas power mode.') | ||
1567 | 162 | |||
1568 | 163 | if power_status == 'On': | ||
1569 | 164 | mscm.power_node_off(node_id) | ||
1570 | 165 | |||
1571 | 166 | mscm.configure_node_bootonce_pxe(node_id) | ||
1572 | 167 | mscm.power_node_on(node_id) | ||
1573 | 168 | |||
1574 | 169 | |||
1575 | 170 | def probe_and_enlist_mscm(host, username, password): | ||
1576 | 171 | """ Extracts all of nodes from mscm, sets all of them to boot via HDD by, | ||
1577 | 172 | default, sets them to bootonce via PXE, and then enlists them into MAAS. | ||
1578 | 173 | """ | ||
1579 | 174 | mscm = MSCM_CLI_API(host, username, password) | ||
1580 | 175 | nodes = mscm.discover_nodes() | ||
1581 | 176 | for node_id in nodes: | ||
1582 | 177 | # Set default boot to HDD | ||
1583 | 178 | mscm.configure_node_boot_m2(node_id) | ||
1584 | 179 | params = { | ||
1585 | 180 | 'power_address': host, | ||
1586 | 181 | 'power_user': username, | ||
1587 | 182 | 'power_pass': password, | ||
1588 | 183 | 'node_id': node_id, | ||
1589 | 184 | } | ||
1590 | 185 | arch = mscm.get_node_arch(node_id) | ||
1591 | 186 | macs = mscm.get_node_macaddr(node_id) | ||
1592 | 187 | utils.create_node(macs, arch, 'mscm', params) | ||
1593 | 0 | 188 | ||
1594 | === added directory 'src/provisioningserver/drivers/hardware/tests' | |||
1595 | === added file 'src/provisioningserver/drivers/hardware/tests/test_mscm.py' | |||
1596 | --- src/provisioningserver/drivers/hardware/tests/test_mscm.py 1970-01-01 00:00:00 +0000 | |||
1597 | +++ src/provisioningserver/drivers/hardware/tests/test_mscm.py 2014-08-29 19:22:35 +0000 | |||
1598 | @@ -0,0 +1,259 @@ | |||
1599 | 1 | # Copyright 2014 Canonical Ltd. This software is licensed under the | ||
1600 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
1601 | 3 | |||
1602 | 4 | """Tests for ``provisioningserver.drivers.hardware.mscm``.""" | ||
1603 | 5 | |||
1604 | 6 | from __future__ import ( | ||
1605 | 7 | absolute_import, | ||
1606 | 8 | print_function, | ||
1607 | 9 | unicode_literals, | ||
1608 | 10 | ) | ||
1609 | 11 | |||
1610 | 12 | str = None | ||
1611 | 13 | |||
1612 | 14 | __metaclass__ = type | ||
1613 | 15 | __all__ = [] | ||
1614 | 16 | |||
1615 | 17 | from random import randint | ||
1616 | 18 | import re | ||
1617 | 19 | from StringIO import StringIO | ||
1618 | 20 | |||
1619 | 21 | from maastesting.factory import factory | ||
1620 | 22 | from maastesting.matchers import MockCalledOnceWith | ||
1621 | 23 | from maastesting.testcase import MAASTestCase | ||
1622 | 24 | from mock import Mock | ||
1623 | 25 | from provisioningserver.drivers.hardware.mscm import ( | ||
1624 | 26 | cartridge_mapping, | ||
1625 | 27 | MSCM_CLI_API, | ||
1626 | 28 | power_control_mscm, | ||
1627 | 29 | probe_and_enlist_mscm, | ||
1628 | 30 | ) | ||
1629 | 31 | import provisioningserver.custom_hardware.utils as utils | ||
1630 | 32 | |||
1631 | 33 | |||
1632 | 34 | def make_mscm_api(): | ||
1633 | 35 | """Make a MSCM_CLI_API object with randomized parameters.""" | ||
1634 | 36 | host = factory.make_hostname('mscm') | ||
1635 | 37 | username = factory.make_name('user') | ||
1636 | 38 | password = factory.make_name('password') | ||
1637 | 39 | return MSCM_CLI_API(host, username, password) | ||
1638 | 40 | |||
1639 | 41 | |||
1640 | 42 | def make_node_id(): | ||
1641 | 43 | """Make a node_id.""" | ||
1642 | 44 | return 'c%sn%s' % (randint(1, 45), randint(1, 8)) | ||
1643 | 45 | |||
1644 | 46 | |||
1645 | 47 | def make_show_node_list(length=10): | ||
1646 | 48 | """Make a fake return value for discover_nodes.""" | ||
1647 | 49 | return re.findall(r'c\d+n\d', ''.join(make_node_id() | ||
1648 | 50 | for _ in xrange(length))) | ||
1649 | 51 | |||
1650 | 52 | |||
1651 | 53 | def make_show_node_macaddr(length=10): | ||
1652 | 54 | """Make a fake return value for get_node_macaddr.""" | ||
1653 | 55 | return ''.join((factory.getRandomMACAddress() + ' ') | ||
1654 | 56 | for _ in xrange(length)) | ||
1655 | 57 | |||
1656 | 58 | |||
1657 | 59 | class TestRunCliCommand(MAASTestCase): | ||
1658 | 60 | """Tests for ``MSCM_CLI_API.run_cli_command``.""" | ||
1659 | 61 | |||
1660 | 62 | def test_returns_output(self): | ||
1661 | 63 | api = make_mscm_api() | ||
1662 | 64 | ssh_mock = self.patch(api, '_ssh') | ||
1663 | 65 | expected = factory.make_name('output') | ||
1664 | 66 | stdout = StringIO(expected) | ||
1665 | 67 | streams = factory.make_streams(stdout=stdout) | ||
1666 | 68 | ssh_mock.exec_command = Mock(return_value=streams) | ||
1667 | 69 | output = api._run_cli_command(factory.make_name('command')) | ||
1668 | 70 | self.assertEqual(expected, output) | ||
1669 | 71 | |||
1670 | 72 | def test_connects_and_closes_ssh_client(self): | ||
1671 | 73 | api = make_mscm_api() | ||
1672 | 74 | ssh_mock = self.patch(api, '_ssh') | ||
1673 | 75 | ssh_mock.exec_command = Mock(return_value=factory.make_streams()) | ||
1674 | 76 | api._run_cli_command(factory.make_name('command')) | ||
1675 | 77 | self.assertThat( | ||
1676 | 78 | ssh_mock.connect, | ||
1677 | 79 | MockCalledOnceWith( | ||
1678 | 80 | api.host, username=api.username, password=api.password)) | ||
1679 | 81 | self.assertThat(ssh_mock.close, MockCalledOnceWith()) | ||
1680 | 82 | |||
1681 | 83 | def test_closes_when_exception_raised(self): | ||
1682 | 84 | api = make_mscm_api() | ||
1683 | 85 | ssh_mock = self.patch(api, '_ssh') | ||
1684 | 86 | |||
1685 | 87 | def fail(): | ||
1686 | 88 | raise Exception('fail') | ||
1687 | 89 | |||
1688 | 90 | ssh_mock.exec_command = Mock(side_effect=fail) | ||
1689 | 91 | command = factory.make_name('command') | ||
1690 | 92 | self.assertRaises(Exception, api._run_cli_command, command) | ||
1691 | 93 | self.assertThat(ssh_mock.close, MockCalledOnceWith()) | ||
1692 | 94 | |||
1693 | 95 | |||
1694 | 96 | class TestDiscoverNodes(MAASTestCase): | ||
1695 | 97 | """Tests for ``MSCM_CLI_API.discover_nodes``.""" | ||
1696 | 98 | |||
1697 | 99 | def test_discover_nodes(self): | ||
1698 | 100 | api = make_mscm_api() | ||
1699 | 101 | ssh_mock = self.patch(api, '_ssh') | ||
1700 | 102 | expected = make_show_node_list() | ||
1701 | 103 | stdout = StringIO(expected) | ||
1702 | 104 | streams = factory.make_streams(stdout=stdout) | ||
1703 | 105 | ssh_mock.exec_command = Mock(return_value=streams) | ||
1704 | 106 | output = api.discover_nodes() | ||
1705 | 107 | self.assertEqual(expected, output) | ||
1706 | 108 | |||
1707 | 109 | |||
1708 | 110 | class TestNodeMACAddress(MAASTestCase): | ||
1709 | 111 | """Tests for ``MSCM_CLI_API.get_node_macaddr``.""" | ||
1710 | 112 | |||
1711 | 113 | def test_get_node_macaddr(self): | ||
1712 | 114 | api = make_mscm_api() | ||
1713 | 115 | expected = make_show_node_macaddr() | ||
1714 | 116 | cli_mock = self.patch(api, '_run_cli_command') | ||
1715 | 117 | cli_mock.return_value = expected | ||
1716 | 118 | node_id = make_node_id() | ||
1717 | 119 | output = api.get_node_macaddr(node_id) | ||
1718 | 120 | self.assertEqual(re.findall(r':'.join(['[0-9a-f]{2}'] * 6), | ||
1719 | 121 | expected), output) | ||
1720 | 122 | |||
1721 | 123 | |||
1722 | 124 | class TestNodeArch(MAASTestCase): | ||
1723 | 125 | """Tests for ``MSCM_CLI_API.get_node_arch``.""" | ||
1724 | 126 | |||
1725 | 127 | def test_get_node_arch(self): | ||
1726 | 128 | api = make_mscm_api() | ||
1727 | 129 | expected = '\r\n Product Name: ProLiant Moonshot Cartridge\r\n' | ||
1728 | 130 | cli_mock = self.patch(api, '_run_cli_command') | ||
1729 | 131 | cli_mock.return_value = expected | ||
1730 | 132 | node_id = make_node_id() | ||
1731 | 133 | output = api.get_node_arch(node_id) | ||
1732 | 134 | key = expected.split('Product Name: ')[1].splitlines()[0] | ||
1733 | 135 | self.assertEqual(cartridge_mapping[key], output) | ||
1734 | 136 | |||
1735 | 137 | |||
1736 | 138 | class TestGetNodePowerStatus(MAASTestCase): | ||
1737 | 139 | """Tests for ``MSCM_CLI_API.get_node_power_status``.""" | ||
1738 | 140 | |||
1739 | 141 | def test_get_node_power_status(self): | ||
1740 | 142 | api = make_mscm_api() | ||
1741 | 143 | expected = '\r\n Node #1\r\n Power State: On\r\n' | ||
1742 | 144 | cli_mock = self.patch(api, '_run_cli_command') | ||
1743 | 145 | cli_mock.return_value = expected | ||
1744 | 146 | node_id = make_node_id() | ||
1745 | 147 | output = api.get_node_power_status(node_id) | ||
1746 | 148 | self.assertEqual(expected.split('Power State: ')[1].splitlines()[0], | ||
1747 | 149 | output) | ||
1748 | 150 | |||
1749 | 151 | |||
1750 | 152 | class TestPowerAndConfigureNode(MAASTestCase): | ||
1751 | 153 | """Tests for ``MSCM_CLI_API.configure_node_bootonce_pxe, | ||
1752 | 154 | MSCM_CLI_API.power_node_on, and MSCM_CLI_API.power_node_off``. | ||
1753 | 155 | """ | ||
1754 | 156 | |||
1755 | 157 | scenarios = [ | ||
1756 | 158 | ('power_node_on()', | ||
1757 | 159 | dict(method='power_node_on')), | ||
1758 | 160 | ('power_node_off()', | ||
1759 | 161 | dict(method='power_node_off')), | ||
1760 | 162 | ('configure_node_bootonce_pxe()', | ||
1761 | 163 | dict(method='configure_node_bootonce_pxe')), | ||
1762 | 164 | ] | ||
1763 | 165 | |||
1764 | 166 | def test_returns_expected_outout(self): | ||
1765 | 167 | api = make_mscm_api() | ||
1766 | 168 | ssh_mock = self.patch(api, '_ssh') | ||
1767 | 169 | expected = factory.make_name('output') | ||
1768 | 170 | stdout = StringIO(expected) | ||
1769 | 171 | streams = factory.make_streams(stdout=stdout) | ||
1770 | 172 | ssh_mock.exec_command = Mock(return_value=streams) | ||
1771 | 173 | output = getattr(api, self.method)(make_node_id()) | ||
1772 | 174 | self.assertEqual(expected, output) | ||
1773 | 175 | |||
1774 | 176 | |||
1775 | 177 | class TestPowerControlMSCM(MAASTestCase): | ||
1776 | 178 | """Tests for ``power_control_ucsm``.""" | ||
1777 | 179 | |||
1778 | 180 | def test_power_control_mscm_on_on(self): | ||
1779 | 181 | # power_change and power_status are both 'on' | ||
1780 | 182 | host = factory.make_hostname('mscm') | ||
1781 | 183 | username = factory.make_name('user') | ||
1782 | 184 | password = factory.make_name('password') | ||
1783 | 185 | node_id = make_node_id() | ||
1784 | 186 | bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe') | ||
1785 | 187 | power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status') | ||
1786 | 188 | power_status_mock.return_value = 'On' | ||
1787 | 189 | power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on') | ||
1788 | 190 | power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off') | ||
1789 | 191 | |||
1790 | 192 | power_control_mscm(host, username, password, node_id, | ||
1791 | 193 | power_change='on') | ||
1792 | 194 | self.assertThat(bootonce_mock, MockCalledOnceWith(node_id)) | ||
1793 | 195 | self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id)) | ||
1794 | 196 | self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id)) | ||
1795 | 197 | |||
1796 | 198 | def test_power_control_mscm_on_off(self): | ||
1797 | 199 | # power_change is 'on' and power_status is 'off' | ||
1798 | 200 | host = factory.make_hostname('mscm') | ||
1799 | 201 | username = factory.make_name('user') | ||
1800 | 202 | password = factory.make_name('password') | ||
1801 | 203 | node_id = make_node_id() | ||
1802 | 204 | bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe') | ||
1803 | 205 | power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status') | ||
1804 | 206 | power_status_mock.return_value = 'Off' | ||
1805 | 207 | power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on') | ||
1806 | 208 | |||
1807 | 209 | power_control_mscm(host, username, password, node_id, | ||
1808 | 210 | power_change='on') | ||
1809 | 211 | self.assertThat(bootonce_mock, MockCalledOnceWith(node_id)) | ||
1810 | 212 | self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id)) | ||
1811 | 213 | |||
1812 | 214 | def test_power_control_mscm_off_on(self): | ||
1813 | 215 | # power_change is 'off' and power_status is 'on' | ||
1814 | 216 | host = factory.make_hostname('mscm') | ||
1815 | 217 | username = factory.make_name('user') | ||
1816 | 218 | password = factory.make_name('password') | ||
1817 | 219 | node_id = make_node_id() | ||
1818 | 220 | power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status') | ||
1819 | 221 | power_status_mock.return_value = 'On' | ||
1820 | 222 | power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off') | ||
1821 | 223 | |||
1822 | 224 | power_control_mscm(host, username, password, node_id, | ||
1823 | 225 | power_change='off') | ||
1824 | 226 | self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id)) | ||
1825 | 227 | |||
1826 | 228 | |||
1827 | 229 | class TestProbeAndEnlistMSCM(MAASTestCase): | ||
1828 | 230 | """Tests for ``probe_and_enlist_mscm``.""" | ||
1829 | 231 | |||
1830 | 232 | def test_probe_and_enlist(self): | ||
1831 | 233 | host = factory.make_hostname('mscm') | ||
1832 | 234 | username = factory.make_name('user') | ||
1833 | 235 | password = factory.make_name('password') | ||
1834 | 236 | node_id = make_node_id() | ||
1835 | 237 | macs = make_show_node_macaddr(4) | ||
1836 | 238 | arch = 'arm64/xgene-uboot' | ||
1837 | 239 | discover_nodes_mock = self.patch(MSCM_CLI_API, 'discover_nodes') | ||
1838 | 240 | discover_nodes_mock.return_value = [node_id] | ||
1839 | 241 | boot_m2_mock = self.patch(MSCM_CLI_API, 'configure_node_boot_m2') | ||
1840 | 242 | node_arch_mock = self.patch(MSCM_CLI_API, 'get_node_arch') | ||
1841 | 243 | node_arch_mock.return_value = arch | ||
1842 | 244 | node_macs_mock = self.patch(MSCM_CLI_API, 'get_node_macaddr') | ||
1843 | 245 | node_macs_mock.return_value = macs | ||
1844 | 246 | create_node_mock = self.patch(utils, 'create_node') | ||
1845 | 247 | probe_and_enlist_mscm(host, username, password) | ||
1846 | 248 | self.assertThat(discover_nodes_mock, MockCalledOnceWith()) | ||
1847 | 249 | self.assertThat(boot_m2_mock, MockCalledOnceWith(node_id)) | ||
1848 | 250 | self.assertThat(node_arch_mock, MockCalledOnceWith(node_id)) | ||
1849 | 251 | self.assertThat(node_macs_mock, MockCalledOnceWith(node_id)) | ||
1850 | 252 | params = { | ||
1851 | 253 | 'power_address': host, | ||
1852 | 254 | 'power_user': username, | ||
1853 | 255 | 'power_pass': password, | ||
1854 | 256 | 'node_id': node_id, | ||
1855 | 257 | } | ||
1856 | 258 | self.assertThat(create_node_mock, | ||
1857 | 259 | MockCalledOnceWith(macs, arch, 'mscm', params)) | ||
1858 | 0 | 260 | ||
1859 | === modified file 'src/provisioningserver/power/tests/test_poweraction.py' | |||
1860 | --- src/provisioningserver/power/tests/test_poweraction.py 2014-06-02 11:57:58 +0000 | |||
1861 | +++ src/provisioningserver/power/tests/test_poweraction.py 2014-08-29 19:22:35 +0000 | |||
1862 | @@ -220,3 +220,14 @@ | |||
1863 | 220 | power_user='bar', power_pass='baz', | 220 | power_user='bar', power_pass='baz', |
1864 | 221 | uuid=factory.getRandomUUID(), power_change='on') | 221 | uuid=factory.getRandomUUID(), power_change='on') |
1865 | 222 | self.assertIn('power_control_ucsm', script) | 222 | self.assertIn('power_control_ucsm', script) |
1866 | 223 | |||
1867 | 224 | def test_mscm_renders_template(self): | ||
1868 | 225 | # I'd like to assert that escape_py_literal is being used here, | ||
1869 | 226 | # but it's not obvious how to mock things in the template | ||
1870 | 227 | # rendering namespace so I passed on that. | ||
1871 | 228 | action = PowerAction('mscm') | ||
1872 | 229 | script = action.render_template( | ||
1873 | 230 | action.get_template(), power_address='foo', | ||
1874 | 231 | power_user='bar', power_pass='baz', | ||
1875 | 232 | node_id='c1n1', power_change='on') | ||
1876 | 233 | self.assertIn('power_control_mscm', script) | ||
1877 | 223 | 234 | ||
1878 | === modified file 'src/provisioningserver/power_schema.py' | |||
1879 | --- src/provisioningserver/power_schema.py 2014-06-02 11:57:58 +0000 | |||
1880 | +++ src/provisioningserver/power_schema.py 2014-08-29 19:22:35 +0000 | |||
1881 | @@ -255,4 +255,17 @@ | |||
1882 | 255 | make_json_field('power_pass', "API password"), | 255 | make_json_field('power_pass', "API password"), |
1883 | 256 | ], | 256 | ], |
1884 | 257 | }, | 257 | }, |
1885 | 258 | { | ||
1886 | 259 | 'name': 'mscm', | ||
1887 | 260 | 'description': "Moonshot HP iLO Chassis Manager", | ||
1888 | 261 | 'fields': [ | ||
1889 | 262 | make_json_field('power_address', "IP for MSCM CLI API"), | ||
1890 | 263 | make_json_field('power_user', "MSCM CLI API user"), | ||
1891 | 264 | make_json_field('power_pass', "MSCM CLI API password"), | ||
1892 | 265 | make_json_field( | ||
1893 | 266 | 'node_id', | ||
1894 | 267 | "Node ID - Must adhere to cXnY format " | ||
1895 | 268 | "(X=cartridge number, Y=node number)."), | ||
1896 | 269 | ], | ||
1897 | 270 | }, | ||
1898 | 258 | ] | 271 | ] |
1899 | 259 | 272 | ||
1900 | === modified file 'src/provisioningserver/tasks.py' | |||
1901 | --- src/provisioningserver/tasks.py 2014-06-02 11:57:58 +0000 | |||
1902 | +++ src/provisioningserver/tasks.py 2014-08-29 19:22:35 +0000 | |||
1903 | @@ -58,6 +58,7 @@ | |||
1904 | 58 | set_up_options_conf, | 58 | set_up_options_conf, |
1905 | 59 | setup_rndc, | 59 | setup_rndc, |
1906 | 60 | ) | 60 | ) |
1907 | 61 | from provisioningserver.drivers.hardware.mscm import probe_and_enlist_mscm | ||
1908 | 61 | from provisioningserver.omshell import Omshell | 62 | from provisioningserver.omshell import Omshell |
1909 | 62 | from provisioningserver.power.poweraction import ( | 63 | from provisioningserver.power.poweraction import ( |
1910 | 63 | PowerAction, | 64 | PowerAction, |
1911 | @@ -493,5 +494,12 @@ | |||
1912 | 493 | @task | 494 | @task |
1913 | 494 | @log_exception_text | 495 | @log_exception_text |
1914 | 495 | def enlist_nodes_from_ucsm(url, username, password): | 496 | def enlist_nodes_from_ucsm(url, username, password): |
1916 | 496 | """ See `maasserver.api.NodeGroupsHandler.enlist_nodes_from_ucsm`. """ | 497 | """ See `maasserver.api.NodeGroupHandler.enlist_nodes_from_ucsm`. """ |
1917 | 497 | probe_and_enlist_ucsm(url, username, password) | 498 | probe_and_enlist_ucsm(url, username, password) |
1918 | 499 | |||
1919 | 500 | |||
1920 | 501 | @task | ||
1921 | 502 | @log_exception_text | ||
1922 | 503 | def enlist_nodes_from_mscm(host, username, password): | ||
1923 | 504 | """ See `maasserver.api.NodeGroupHandler.enlist_nodes_from_mscm`. """ | ||
1924 | 505 | probe_and_enlist_mscm(host, username, password) | ||
1925 | 498 | 506 | ||
1926 | === modified file 'src/provisioningserver/tests/test_tasks.py' | |||
1927 | --- src/provisioningserver/tests/test_tasks.py 2014-06-02 11:57:58 +0000 | |||
1928 | +++ src/provisioningserver/tests/test_tasks.py 2014-08-29 19:22:35 +0000 | |||
1929 | @@ -73,6 +73,7 @@ | |||
1930 | 73 | from provisioningserver.tags import MissingCredentials | 73 | from provisioningserver.tags import MissingCredentials |
1931 | 74 | from provisioningserver.tasks import ( | 74 | from provisioningserver.tasks import ( |
1932 | 75 | add_new_dhcp_host_map, | 75 | add_new_dhcp_host_map, |
1933 | 76 | enlist_nodes_from_mscm, | ||
1934 | 76 | enlist_nodes_from_ucsm, | 77 | enlist_nodes_from_ucsm, |
1935 | 77 | import_boot_images, | 78 | import_boot_images, |
1936 | 78 | Omshell, | 79 | Omshell, |
1937 | @@ -661,3 +662,14 @@ | |||
1938 | 661 | mock = self.patch(tasks, 'probe_and_enlist_ucsm') | 662 | mock = self.patch(tasks, 'probe_and_enlist_ucsm') |
1939 | 662 | enlist_nodes_from_ucsm(url, username, password) | 663 | enlist_nodes_from_ucsm(url, username, password) |
1940 | 663 | self.assertThat(mock, MockCalledOnceWith(url, username, password)) | 664 | self.assertThat(mock, MockCalledOnceWith(url, username, password)) |
1941 | 665 | |||
1942 | 666 | |||
1943 | 667 | class TestAddMSCM(PservTestCase): | ||
1944 | 668 | |||
1945 | 669 | def test_enlist_nodes_from_mscm(self): | ||
1946 | 670 | host = 'host' | ||
1947 | 671 | username = 'username' | ||
1948 | 672 | password = 'password' | ||
1949 | 673 | mock = self.patch(tasks, 'probe_and_enlist_mscm') | ||
1950 | 674 | enlist_nodes_from_mscm(host, username, password) | ||
1951 | 675 | self.assertThat(mock, MockCalledOnceWith(host, username, password)) | ||
1952 | 664 | 676 | ||
1953 | === modified file 'src/provisioningserver/utils/__init__.py' | |||
1954 | --- src/provisioningserver/utils/__init__.py 2014-04-15 14:41:32 +0000 | |||
1955 | +++ src/provisioningserver/utils/__init__.py 2014-08-29 19:22:35 +0000 | |||
1956 | @@ -829,3 +829,24 @@ | |||
1957 | 829 | if len(columns) == 5 and columns[2] == mac: | 829 | if len(columns) == 5 and columns[2] == mac: |
1958 | 830 | return columns[0] | 830 | return columns[0] |
1959 | 831 | return None | 831 | return None |
1960 | 832 | |||
1961 | 833 | |||
1962 | 834 | def find_mac_via_arp(ip): | ||
1963 | 835 | """Find the MAC address for `ip` by reading the output of arp -n. | ||
1964 | 836 | |||
1965 | 837 | Returns `None` if the IP is not found. | ||
1966 | 838 | |||
1967 | 839 | We do this because we aren't necessarily the only DHCP server on the | ||
1968 | 840 | network, so we can't check our own leases file and be guaranteed to find an | ||
1969 | 841 | IP that matches. | ||
1970 | 842 | |||
1971 | 843 | :param ip: The ip address, e.g. '192.168.1.1'. | ||
1972 | 844 | """ | ||
1973 | 845 | |||
1974 | 846 | output = call_capture_and_check(['arp', '-n']).split('\n') | ||
1975 | 847 | |||
1976 | 848 | for line in sorted(output): | ||
1977 | 849 | columns = line.split() | ||
1978 | 850 | if len(columns) == 5 and columns[0] == ip: | ||
1979 | 851 | return columns[2] | ||
1980 | 852 | return None | ||
1981 | 832 | 853 | ||
1982 | === modified file 'src/provisioningserver/utils/tests/test_utils.py' | |||
1983 | --- src/provisioningserver/utils/tests/test_utils.py 2014-04-15 14:41:32 +0000 | |||
1984 | +++ src/provisioningserver/utils/tests/test_utils.py 2014-08-29 19:22:35 +0000 | |||
1985 | @@ -75,6 +75,7 @@ | |||
1986 | 75 | ExternalProcessError, | 75 | ExternalProcessError, |
1987 | 76 | filter_dict, | 76 | filter_dict, |
1988 | 77 | find_ip_via_arp, | 77 | find_ip_via_arp, |
1989 | 78 | find_mac_via_arp, | ||
1990 | 78 | get_all_interface_addresses, | 79 | get_all_interface_addresses, |
1991 | 79 | get_mtime, | 80 | get_mtime, |
1992 | 80 | incremental_write, | 81 | incremental_write, |
1993 | @@ -1322,6 +1323,50 @@ | |||
1994 | 1322 | self.assertEqual("192.168.0.1", ip_address_observed) | 1323 | self.assertEqual("192.168.0.1", ip_address_observed) |
1995 | 1323 | 1324 | ||
1996 | 1324 | 1325 | ||
1997 | 1326 | class TestFindMACViaARP(MAASTestCase): | ||
1998 | 1327 | |||
1999 | 1328 | def patch_call(self, output): | ||
2000 | 1329 | """Replace `call_capture_and_check` with one that returns `output`.""" | ||
2001 | 1330 | fake = self.patch(provisioningserver.utils, 'call_capture_and_check') | ||
2002 | 1331 | fake.return_value = output | ||
2003 | 1332 | return fake | ||
2004 | 1333 | |||
2005 | 1334 | def test__resolves_IP_address_to_MAC(self): | ||
2006 | 1335 | sample = """\ | ||
2007 | 1336 | Address HWtype HWaddress Flags Mask Iface | ||
2008 | 1337 | 192.168.100.20 (incomplete) virbr1 | ||
2009 | 1338 | 192.168.0.104 (incomplete) eth0 | ||
2010 | 1339 | 192.168.0.5 (incomplete) eth0 | ||
2011 | 1340 | 192.168.0.2 (incomplete) eth0 | ||
2012 | 1341 | 192.168.0.100 (incomplete) eth0 | ||
2013 | 1342 | 192.168.122.20 ether 52:54:00:02:86:4b C virbr0 | ||
2014 | 1343 | 192.168.0.4 (incomplete) eth0 | ||
2015 | 1344 | 192.168.0.1 ether 90:f6:52:f6:17:92 C eth0 | ||
2016 | 1345 | """ | ||
2017 | 1346 | |||
2018 | 1347 | call_capture_and_check = self.patch_call(sample) | ||
2019 | 1348 | mac_address_observed = find_mac_via_arp("192.168.122.20") | ||
2020 | 1349 | self.assertThat( | ||
2021 | 1350 | call_capture_and_check, | ||
2022 | 1351 | MockCalledOnceWith(['arp', '-n'])) | ||
2023 | 1352 | self.assertEqual("52:54:00:02:86:4b", mac_address_observed) | ||
2024 | 1353 | |||
2025 | 1354 | def test__returns_consistent_output(self): | ||
2026 | 1355 | ip = factory.getRandomIPAddress() | ||
2027 | 1356 | macs = [ | ||
2028 | 1357 | '52:54:00:02:86:4b', | ||
2029 | 1358 | '90:f6:52:f6:17:92', | ||
2030 | 1359 | ] | ||
2031 | 1360 | lines = ['%s ether %s C eth0' % (ip, mac) for mac in macs] | ||
2032 | 1361 | self.patch_call('\n'.join(lines)) | ||
2033 | 1362 | one_result = find_mac_via_arp(ip) | ||
2034 | 1363 | self.patch_call('\n'.join(reversed(lines))) | ||
2035 | 1364 | other_result = find_mac_via_arp(ip) | ||
2036 | 1365 | |||
2037 | 1366 | self.assertIn(one_result, macs) | ||
2038 | 1367 | self.assertEqual(one_result, other_result) | ||
2039 | 1368 | |||
2040 | 1369 | |||
2041 | 1325 | class TestAsynchronousDecorator(MAASTestCase): | 1370 | class TestAsynchronousDecorator(MAASTestCase): |
2042 | 1326 | 1371 | ||
2043 | 1327 | run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5) | 1372 | run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5) |
2044 | 1328 | 1373 | ||
2045 | === modified file 'versions.cfg' | |||
2046 | --- versions.cfg 2014-03-28 10:43:53 +0000 | |||
2047 | +++ versions.cfg 2014-08-29 19:22:35 +0000 | |||
2048 | @@ -37,7 +37,7 @@ | |||
2049 | 37 | nose = 1.3 | 37 | nose = 1.3 |
2050 | 38 | nose-subunit = 0.2 | 38 | nose-subunit = 0.2 |
2051 | 39 | python-subunit = 0.0.7 | 39 | python-subunit = 0.0.7 |
2053 | 40 | rabbitfixture = 0.3.4 | 40 | rabbitfixture = 0.3.5 |
2054 | 41 | sst = 0.2.2 | 41 | sst = 0.2.2 |
2055 | 42 | testresources = 0.2.5 | 42 | testresources = 0.2.5 |
2056 | 43 | testscenarios = 0.4 | 43 | testscenarios = 0.4 |
For an SRU team member who may be looking at this, effected bugs fixed in SRU are:
1350235
1338851
1337437
1190986
1325759
1325640
1315154
1352273
1342302
1334401
1331982