Merge lp:~lutostag/ubuntu/trusty/maas/1.5.4 into lp:ubuntu/trusty-proposed/maas

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
Reviewer Review Type Date Requested Status
Chuck Short (community) Approve
Review via email: mp+232757@code.launchpad.net

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 :

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

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

Subscribers

People subscribed via source and target branches