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
=== modified file 'debian/changelog'
--- debian/changelog 2014-06-20 10:10:47 +0000
+++ debian/changelog 2014-08-29 19:22:35 +0000
@@ -1,3 +1,42 @@
1maas (1.5.4+bzr2294-0ubuntu1) trusty-proposed; urgency=medium
2
3 * New upstream bug fix release:
4 - Package fails to install when the default route is through an
5 aliased/tagged interface (LP: #1350235)
6 - ERROR Nonce already used (LP: #1190986)
7 - Add MAAS arm64/xgene support (LP: #1338851)
8 - Add utopic support (LP: #1337437)
9 - API documentation for nodegroup op=details missing parameter
10 (LP: #1331982)
11 - Reduce number of celery tasks emitted when updating a cluster controller
12 (LP: #1324944)
13 - Fix VirshSSH template which was referencing invalid attributes
14 (LP: #1324966)
15 - Fix a start up problems where a database lock was being taken outside of
16 a transaction (LP: #1325640, LP: #1325759)
17 - Reformat badly formatted Architecture error message (LP: #1301465)
18 - Final changes to support ppc64el (now known as PowerNV) (LP: #1315154)
19 - UI tweak to make navigation elements visible for documentation
20
21 [ Greg Lutostanski ]
22 * debian/control:
23 - maas-provisioningserver not maas-cluster-controller depends on
24 python-pexpect (LP: #1352273)
25
26 [ Gavin Panella ]
27 * debian/maas-cluster-controller.postinst
28 - Allow maas-pserv to bind to all IPv6 addresses too. (LP: #1342302)
29
30 [ Diogo Matsubara ]
31 * debian/control:
32 - python-maas-provisioningserver depends on python-paramiko (LP: #1334401)
33
34 [ Raphaƫl Badin ]
35 * debian/extras/99-maas-sudoers:
36 - Add rule 'maas-dhcp-server stop' job.
37
38 -- Greg Lutostanski <gregory.lutostanski@canonical.com> Fri, 29 Aug 2014 13:27:34 -0500
39
1maas (1.5.2+bzr2282-0ubuntu0.2) trusty-proposed; urgency=medium40maas (1.5.2+bzr2282-0ubuntu0.2) trusty-proposed; urgency=medium
241
3 * debian/control:42 * debian/control:
443
=== modified file 'debian/control'
--- debian/control 2014-06-20 10:10:47 +0000
+++ debian/control 2014-08-29 19:22:35 +0000
@@ -99,6 +99,8 @@
99 python-oops-amqp,99 python-oops-amqp,
100 python-oops-datedir-repo,100 python-oops-datedir-repo,
101 python-oops-twisted,101 python-oops-twisted,
102 python-paramiko,
103 python-pexpect,
102 python-pyparsing,104 python-pyparsing,
103 python-seamicroclient,105 python-seamicroclient,
104 python-simplestreams,106 python-simplestreams,
@@ -159,7 +161,6 @@
159 python-maas-provisioningserver (=${binary:Version}),161 python-maas-provisioningserver (=${binary:Version}),
160 python-netaddr,162 python-netaddr,
161 python-oauth,163 python-oauth,
162 python-pexpect,
163 python-tempita,164 python-tempita,
164 python-twisted,165 python-twisted,
165 python-zope.interface,166 python-zope.interface,
166167
=== modified file 'debian/extras/99-maas-sudoers'
--- debian/extras/99-maas-sudoers 2013-06-03 16:53:14 +0000
+++ debian/extras/99-maas-sudoers 2014-08-29 19:22:35 +0000
@@ -1,3 +1,4 @@
1maas ALL= NOPASSWD: /usr/sbin/service maas-dhcp-server restart1maas ALL= NOPASSWD: /usr/sbin/service maas-dhcp-server restart
2maas ALL= NOPASSWD: /usr/sbin/service maas-dhcp-server stop
2maas ALL= NOPASSWD: /usr/sbin/maas-provision3maas ALL= NOPASSWD: /usr/sbin/maas-provision
3maas ALL= NOPASSWD: SETENV: /usr/sbin/maas-import-pxe-files4maas ALL= NOPASSWD: SETENV: /usr/sbin/maas-import-pxe-files
45
=== modified file 'debian/maas-cluster-controller.postinst'
--- debian/maas-cluster-controller.postinst 2014-04-03 13:45:02 +0000
+++ debian/maas-cluster-controller.postinst 2014-08-29 19:22:35 +0000
@@ -103,6 +103,7 @@
103 fi103 fi
104 fi104 fi
105 echo '0.0.0.0/0:68,69' >/etc/authbind/byuid/$MAAS_UID105 echo '0.0.0.0/0:68,69' >/etc/authbind/byuid/$MAAS_UID
106 echo '::/0,68-69' >>/etc/authbind/byuid/$MAAS_UID
106 chown maas:maas /etc/authbind/byuid/$MAAS_UID107 chown maas:maas /etc/authbind/byuid/$MAAS_UID
107 chmod 700 /etc/authbind/byuid/$MAAS_UID108 chmod 700 /etc/authbind/byuid/$MAAS_UID
108}109}
109110
=== modified file 'docs/_templates/maas/layout.html'
--- docs/_templates/maas/layout.html 2014-04-15 14:41:32 +0000
+++ docs/_templates/maas/layout.html 2014-08-29 19:22:35 +0000
@@ -10,6 +10,12 @@
10<br/>10<br/>
11{% endblock %}11{% endblock %}
1212
13{# Remove 'modules' and 'index' from rellinks: they point to
14 autogenerated code documentation pages that we don't want
15 to advertise too much.
16#}
17{%- set rellinks = rellinks[2:] %}
18
13{%- block footer %}19{%- block footer %}
14<footer class="global clearfix">20<footer class="global clearfix">
15 <div class="legal clearfix">21 <div class="legal clearfix">
1622
=== modified file 'docs/_templates/maas/static/css/main.css'
--- docs/_templates/maas/static/css/main.css 2014-02-15 12:08:23 +0000
+++ docs/_templates/maas/static/css/main.css 2014-08-29 19:22:35 +0000
@@ -9,7 +9,7 @@
99
10div.document {10div.document {
11 width: 984px;11 width: 984px;
12 margin: 30px auto 0 auto;12 margin: 10px auto 0 auto;
13}13}
1414
15div.body h1 {15div.body h1 {
1616
=== modified file 'docs/_templates/maas/static/flasky.css_t'
--- docs/_templates/maas/static/flasky.css_t 2013-10-10 17:07:51 +0000
+++ docs/_templates/maas/static/flasky.css_t 2014-08-29 19:22:35 +0000
@@ -25,7 +25,7 @@
2525
26div.document {26div.document {
27 width: {{ page_width }};27 width: {{ page_width }};
28 margin: 30px auto 0 auto;28 margin: 10px auto 0 auto;
29}29}
3030
31div.documentwrapper {31div.documentwrapper {
@@ -69,6 +69,11 @@
69}69}
7070
71div.related {71div.related {
72 width: {{ page_width }};
73 margin: 10px auto 0 auto;
74}
75
76div.related h3 {
72 display: none;77 display: none;
73}78}
7479
7580
=== modified file 'docs/changelog.rst'
--- docs/changelog.rst 2014-06-02 11:57:58 +0000
+++ docs/changelog.rst 2014-08-29 19:22:35 +0000
@@ -2,6 +2,37 @@
2Changelog2Changelog
3=========3=========
44
51.5.4
6=====
7
8Bug fix update
9--------------
10
11 - Package fails to install when the default route is through an
12 aliased/tagged interface (LP: #1350235)
13 - ERROR Nonce already used (LP: #1190986)
14 - Add MAAS arm64/xgene support (LP: #1338851)
15 - Add utopic support (LP: #1337437)
16 - API documentation for nodegroup op=details missing parameter
17 (LP: #1331982)
18
19
201.5.3
21=====
22
23Bug fix update
24--------------
25
26 - Reduce number of celery tasks emitted when updating a cluster controller
27 (LP: #1324944)
28 - Fix VirshSSH template which was referencing invalid attributes
29 (LP: #1324966)
30 - Fix a start up problem where a database lock was being taken outside of
31 a transaction (LP: #1325759)
32 - Reformat badly formatted Architecture error message (LP: #1301465)
33 - Final changes to support ppc64el (now known as PowerNV) (LP: #1315154)
34
35
51.5.2361.5.2
6=====37=====
738
839
=== modified file 'etc/maas/templates/commissioning-user-data/snippets/maas_api_helper.py'
--- etc/maas/templates/commissioning-user-data/snippets/maas_api_helper.py 2013-10-10 17:07:51 +0000
+++ etc/maas/templates/commissioning-user-data/snippets/maas_api_helper.py 2014-08-29 19:22:35 +0000
@@ -12,6 +12,7 @@
12import sys12import sys
13import time13import time
14import urllib214import urllib2
15import uuid
1516
16import oauth.oauth as oauth17import oauth.oauth as oauth
17import yaml18import yaml
@@ -60,7 +61,7 @@
6061
61 params = {62 params = {
62 'oauth_version': "1.0",63 'oauth_version': "1.0",
63 'oauth_nonce': oauth.generate_nonce(),64 'oauth_nonce': uuid.uuid4().get_hex(),
64 'oauth_timestamp': timestamp,65 'oauth_timestamp': timestamp,
65 'oauth_token': token.key,66 'oauth_token': token.key,
66 'oauth_consumer_key': consumer.key,67 'oauth_consumer_key': consumer.key,
6768
=== modified file 'etc/maas/templates/dhcp/dhcpd.conf.template'
--- etc/maas/templates/dhcp/dhcpd.conf.template 2014-04-09 19:02:00 +0000
+++ etc/maas/templates/dhcp/dhcpd.conf.template 2014-08-29 19:22:35 +0000
@@ -6,6 +6,7 @@
6# the nodegroup's configuration in MAAS to trigger an update.6# the nodegroup's configuration in MAAS to trigger an update.
77
8option arch code 93 = unsigned integer 16; # RFC45788option arch code 93 = unsigned integer 16; # RFC4578
9option path-prefix code 210 = text; #RFC5071
9{{for dhcp_subnet in dhcp_subnets}}10{{for dhcp_subnet in dhcp_subnets}}
10subnet {{dhcp_subnet['subnet']}} netmask {{dhcp_subnet['subnet_mask']}} {11subnet {{dhcp_subnet['subnet']}} netmask {{dhcp_subnet['subnet_mask']}} {
11 {{bootloader}}12 {{bootloader}}
1213
=== added file 'etc/maas/templates/power/mscm.template'
--- etc/maas/templates/power/mscm.template 1970-01-01 00:00:00 +0000
+++ etc/maas/templates/power/mscm.template 2014-08-29 19:22:35 +0000
@@ -0,0 +1,15 @@
1# -*- mode: shell-script -*-
2#
3# Control a system via Moonshot HP iLO Chassis Manager (MSCM).
4
5{{py: from provisioningserver.utils import escape_py_literal}}
6python - << END
7from provisioningserver.drivers.hardware.mscm import power_control_mscm
8power_control_mscm(
9 {{escape_py_literal(power_address) | safe}},
10 {{escape_py_literal(power_user) | safe}},
11 {{escape_py_literal(power_pass) | safe}},
12 {{escape_py_literal(node_id) | safe}},
13 {{escape_py_literal(power_change) | safe}},
14)
15END
016
=== added symlink 'etc/maas/templates/pxe/config.commissioning.arm64.template'
=== target is u'config.commissioning.armhf.template'
=== added file 'etc/maas/templates/pxe/config.commissioning.ppc64el.template'
--- etc/maas/templates/pxe/config.commissioning.ppc64el.template 1970-01-01 00:00:00 +0000
+++ etc/maas/templates/pxe/config.commissioning.ppc64el.template 2014-08-29 19:22:35 +0000
@@ -0,0 +1,6 @@
1DEFAULT execute
2
3LABEL execute
4 KERNEL {{kernel_params | kernel_path }}
5 INITRD {{kernel_params | initrd_path }}
6 APPEND {{kernel_params | kernel_command}}
07
=== added symlink 'etc/maas/templates/pxe/config.install.arm64.template'
=== target is u'config.install.armhf.template'
=== added file 'etc/maas/templates/pxe/config.install.ppc64el.template'
--- etc/maas/templates/pxe/config.install.ppc64el.template 1970-01-01 00:00:00 +0000
+++ etc/maas/templates/pxe/config.install.ppc64el.template 2014-08-29 19:22:35 +0000
@@ -0,0 +1,6 @@
1DEFAULT execute
2
3LABEL execute
4 KERNEL {{kernel_params | kernel_path }}
5 INITRD {{kernel_params | initrd_path }}
6 APPEND {{kernel_params | kernel_command}}
07
=== added symlink 'etc/maas/templates/pxe/config.xinstall.arm64.template'
=== target is u'config.xinstall.armhf.template'
=== added symlink 'etc/maas/templates/pxe/config.xinstall.ppc64el.template'
=== target is u'config.install.ppc64el.template'
=== modified file 'src/apiclient/maas_client.py'
--- src/apiclient/maas_client.py 2014-04-15 14:41:32 +0000
+++ src/apiclient/maas_client.py 2014-08-29 19:22:35 +0000
@@ -21,6 +21,7 @@
21import gzip21import gzip
22from io import BytesIO22from io import BytesIO
23import urllib223import urllib2
24import uuid
2425
25from apiclient.encode_json import encode_json_data26from apiclient.encode_json import encode_json_data
26from apiclient.multipart import encode_multipart_data27from apiclient.multipart import encode_multipart_data
@@ -45,7 +46,8 @@
45 with the signature.46 with the signature.
46 """47 """
47 oauth_request = oauth.OAuthRequest.from_consumer_and_token(48 oauth_request = oauth.OAuthRequest.from_consumer_and_token(
48 self.consumer_token, token=self.resource_token, http_url=url)49 self.consumer_token, token=self.resource_token, http_url=url,
50 parameters={'oauth_nonce': uuid.uuid4().get_hex()})
49 oauth_request.sign_request(51 oauth_request.sign_request(
50 oauth.OAuthSignatureMethod_PLAINTEXT(), self.consumer_token,52 oauth.OAuthSignatureMethod_PLAINTEXT(), self.consumer_token,
51 self.resource_token)53 self.resource_token)
5254
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2014-06-04 14:31:41 +0000
+++ src/maasserver/api.py 2014-08-29 19:22:35 +0000
@@ -1580,6 +1580,8 @@
1580 Returns a ``{system_id: {detail_type: xml, ...}, ...}`` map,1580 Returns a ``{system_id: {detail_type: xml, ...}, ...}`` map,
1581 where ``detail_type`` is something like "lldp" or "lshw".1581 where ``detail_type`` is something like "lldp" or "lshw".
15821582
1583 :param system_ids: System ids of nodes for which to get system details.
1584
1583 Note that this is returned as BSON and not JSON. This is for1585 Note that this is returned as BSON and not JSON. This is for
1584 efficiency, but mainly because JSON can't do binary content1586 efficiency, but mainly because JSON can't do binary content
1585 without applying additional encoding like base-64.1587 without applying additional encoding like base-64.
@@ -1761,6 +1763,30 @@
17611763
1762 return HttpResponse(status=httplib.OK)1764 return HttpResponse(status=httplib.OK)
17631765
1766 @admin_method
1767 @operation(idempotent=False)
1768 def probe_and_enlist_mscm(self, request, uuid):
1769 """Add the nodes from a Moonshot HP iLO Chassis Manager (MSCM).
1770
1771 :param host: IP Address for the MSCM.
1772 :type host: unicode
1773 :param username: The username for the MSCM.
1774 :type username: unicode
1775 :param password: The password for the MSCM.
1776 :type password: unicode
1777
1778 """
1779 nodegroup = get_object_or_404(NodeGroup, uuid=uuid)
1780
1781 host = get_mandatory_param(request.data, 'host')
1782 username = get_mandatory_param(request.data, 'username')
1783 password = get_mandatory_param(request.data, 'password')
1784
1785 nodegroup.enlist_nodes_from_mscm(host, username, password)
1786
1787 return HttpResponse(status=httplib.OK)
1788
1789
1764DISPLAYED_NODEGROUPINTERFACE_FIELDS = (1790DISPLAYED_NODEGROUPINTERFACE_FIELDS = (
1765 'ip', 'management', 'interface', 'subnet_mask',1791 'ip', 'management', 'interface', 'subnet_mask',
1766 'broadcast_ip', 'ip_range_low', 'ip_range_high')1792 'broadcast_ip', 'ip_range_low', 'ip_range_high')
17671793
=== modified file 'src/maasserver/enum.py'
--- src/maasserver/enum.py 2014-03-28 10:43:53 +0000
+++ src/maasserver/enum.py 2014-08-29 19:22:35 +0000
@@ -93,21 +93,19 @@
93 #:93 #:
94 precise = 'precise'94 precise = 'precise'
95 #:95 #:
96 quantal = 'quantal'
97 #:
98 raring = 'raring'
99 #:
100 saucy = 'saucy'96 saucy = 'saucy'
101 #:97 #:
102 trusty = 'trusty'98 trusty = 'trusty'
99 #:
100 utopic = 'utopic'
101
103102
104DISTRO_SERIES_CHOICES = (103DISTRO_SERIES_CHOICES = (
105 (DISTRO_SERIES.default, 'Default Ubuntu Release'),104 (DISTRO_SERIES.default, 'Default Ubuntu Release'),
106 (DISTRO_SERIES.precise, 'Ubuntu 12.04 LTS "Precise Pangolin"'),105 (DISTRO_SERIES.precise, 'Ubuntu 12.04 LTS "Precise Pangolin"'),
107 (DISTRO_SERIES.quantal, 'Ubuntu 12.10 "Quantal Quetzal"'),
108 (DISTRO_SERIES.raring, 'Ubuntu 13.04 "Raring Ringtail"'),
109 (DISTRO_SERIES.saucy, 'Ubuntu 13.10 "Saucy Salamander"'),106 (DISTRO_SERIES.saucy, 'Ubuntu 13.10 "Saucy Salamander"'),
110 (DISTRO_SERIES.trusty, 'Ubuntu 14.04 LTS "Trusty Tahr"'),107 (DISTRO_SERIES.trusty, 'Ubuntu 14.04 LTS "Trusty Tahr"'),
108 (DISTRO_SERIES.utopic, 'Ubuntu 14.10 "Utopic Unicorn"'),
111)109)
112110
113111
114112
=== modified file 'src/maasserver/models/nodegroup.py'
--- src/maasserver/models/nodegroup.py 2014-06-04 14:31:41 +0000
+++ src/maasserver/models/nodegroup.py 2014-08-29 19:22:35 +0000
@@ -42,6 +42,7 @@
42 add_new_dhcp_host_map,42 add_new_dhcp_host_map,
43 add_seamicro15k,43 add_seamicro15k,
44 add_virsh,44 add_virsh,
45 enlist_nodes_from_mscm,
45 enlist_nodes_from_ucsm,46 enlist_nodes_from_ucsm,
46 import_boot_images,47 import_boot_images,
47 report_boot_images,48 report_boot_images,
@@ -307,6 +308,16 @@
307 args = (url, username, password)308 args = (url, username, password)
308 enlist_nodes_from_ucsm.apply_async(queue=self.uuid, args=args)309 enlist_nodes_from_ucsm.apply_async(queue=self.uuid, args=args)
309310
311 def enlist_nodes_from_mscm(self, host, username, password):
312 """ Add the servers from a Moonshot HP iLO Chassis Manager.
313
314 :param host: IP address for the MSCM.
315 :param username: username for MSCM.
316 :param password: password for MSCM.
317 """
318 args = (host, username, password)
319 enlist_nodes_from_mscm.apply_async(queue=self.uuid, args=args)
320
310 def add_dhcp_host_maps(self, new_leases):321 def add_dhcp_host_maps(self, new_leases):
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:
312 # XXX JeroenVermeulen 2012-08-21, bug=1039362: the DHCP323 # XXX JeroenVermeulen 2012-08-21, bug=1039362: the DHCP
313324
=== modified file 'src/maasserver/models/tests/test_node.py'
--- src/maasserver/models/tests/test_node.py 2014-04-15 14:41:32 +0000
+++ src/maasserver/models/tests/test_node.py 2014-08-29 19:22:35 +0000
@@ -162,7 +162,7 @@
162162
163 def test_set_get_distro_series_returns_series(self):163 def test_set_get_distro_series_returns_series(self):
164 node = factory.make_node()164 node = factory.make_node()
165 series = DISTRO_SERIES.quantal165 series = DISTRO_SERIES.utopic
166 node.set_distro_series(series)166 node.set_distro_series(series)
167 self.assertEqual(series, node.get_distro_series())167 self.assertEqual(series, node.get_distro_series())
168168
@@ -474,7 +474,7 @@
474 def test_release_clears_distro_series(self):474 def test_release_clears_distro_series(self):
475 node = factory.make_node(475 node = factory.make_node(
476 status=NODE_STATUS.ALLOCATED, owner=factory.make_user())476 status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
477 node.set_distro_series(series=DISTRO_SERIES.quantal)477 node.set_distro_series(series=DISTRO_SERIES.utopic)
478 node.release()478 node.release()
479 self.assertEqual("", node.distro_series)479 self.assertEqual("", node.distro_series)
480480
481481
=== modified file 'src/maasserver/rpc/regionservice.py'
--- src/maasserver/rpc/regionservice.py 2014-04-03 13:45:02 +0000
+++ src/maasserver/rpc/regionservice.py 2014-08-29 19:22:35 +0000
@@ -325,8 +325,8 @@
325325
326 @synchronous326 @synchronous
327 @synchronised(lock)327 @synchronised(lock)
328 @transactional
328 @synchronised(locks.eventloop)329 @synchronised(locks.eventloop)
329 @transactional
330 def prepare(self):330 def prepare(self):
331 """Ensure that the ``eventloops`` table exists.331 """Ensure that the ``eventloops`` table exists.
332332
333333
=== modified file 'src/maasserver/rpc/tests/test_regionservice.py'
--- src/maasserver/rpc/tests/test_regionservice.py 2014-04-15 14:41:32 +0000
+++ src/maasserver/rpc/tests/test_regionservice.py 2014-08-29 19:22:35 +0000
@@ -49,6 +49,7 @@
49 common,49 common,
50 exceptions,50 exceptions,
51 )51 )
52from provisioningserver.rpc.interfaces import IConnection
52from provisioningserver.rpc.region import (53from provisioningserver.rpc.region import (
53 Identify,54 Identify,
54 ReportBootImages,55 ReportBootImages,
@@ -82,6 +83,7 @@
82from twisted.internet.threads import deferToThread83from twisted.internet.threads import deferToThread
83from twisted.protocols import amp84from twisted.protocols import amp
84from twisted.python import log85from twisted.python import log
86from zope.interface.verify import verifyObject
8587
8688
87class TestRegionProtocol_Identify(MAASTestCase):89class TestRegionProtocol_Identify(MAASTestCase):
@@ -195,10 +197,6 @@
195 return d.addCallback(check)197 return d.addCallback(check)
196198
197199
198from provisioningserver.rpc.interfaces import IConnection
199from zope.interface.verify import verifyObject
200
201
202class TestRegionServer(MAASServerTestCase):200class TestRegionServer(MAASServerTestCase):
203201
204 def test_interfaces(self):202 def test_interfaces(self):
205203
=== modified file 'src/maasserver/start_up.py'
--- src/maasserver/start_up.py 2014-04-09 19:02:00 +0000
+++ src/maasserver/start_up.py 2014-08-29 19:22:35 +0000
@@ -18,7 +18,10 @@
1818
19from textwrap import dedent19from textwrap import dedent
2020
21from django.db import connection21from django.db import (
22 connection,
23 transaction,
24 )
22from maasserver import (25from maasserver import (
23 eventloop,26 eventloop,
24 locks,27 locks,
@@ -51,8 +54,9 @@
51 but this method uses file-based locking to ensure that the methods it calls54 but this method uses file-based locking to ensure that the methods it calls
52 internally are not ran concurrently.55 internally are not ran concurrently.
53 """56 """
54 with locks.startup:57 with transaction.atomic():
55 inner_start_up()58 with locks.startup:
59 inner_start_up()
5660
57 eventloop.start().wait(10)61 eventloop.start().wait(10)
5862
5963
=== modified file 'src/maasserver/testing/tests/test_rabbit.py'
--- src/maasserver/testing/tests/test_rabbit.py 2013-10-04 12:33:05 +0000
+++ src/maasserver/testing/tests/test_rabbit.py 2014-08-29 19:22:35 +0000
@@ -26,7 +26,8 @@
26 def test_patch(self):26 def test_patch(self):
27 config = RabbitServerResources(27 config = RabbitServerResources(
28 hostname=factory.getRandomString(),28 hostname=factory.getRandomString(),
29 port=factory.getRandomPort())29 port=factory.getRandomPort(),
30 dist_port=factory.getRandomPort())
30 self.useFixture(config)31 self.useFixture(config)
31 self.useFixture(RabbitServerSettings(config))32 self.useFixture(RabbitServerSettings(config))
32 self.assertEqual(33 self.assertEqual(
3334
=== modified file 'src/maasserver/tests/test_api_nodegroup.py'
--- src/maasserver/tests/test_api_nodegroup.py 2014-04-15 14:41:32 +0000
+++ src/maasserver/tests/test_api_nodegroup.py 2014-08-29 19:22:35 +0000
@@ -460,6 +460,32 @@
460 matcher = MockCalledOnceWith(queue=nodegroup.uuid, args=args)460 matcher = MockCalledOnceWith(queue=nodegroup.uuid, args=args)
461 self.assertThat(mock.apply_async, matcher)461 self.assertThat(mock.apply_async, matcher)
462462
463 def test_probe_and_enlist_mscm_adds_mscm(self):
464 nodegroup = factory.make_node_group()
465 host = 'http://host'
466 username = factory.make_name('user')
467 password = factory.make_name('password')
468 self.become_admin()
469
470 mock = self.patch(nodegroup_module, 'enlist_nodes_from_mscm')
471
472 response = self.client.post(
473 reverse('nodegroup_handler', args=[nodegroup.uuid]),
474 {
475 'op': 'probe_and_enlist_mscm',
476 'host': host,
477 'username': username,
478 'password': password,
479 })
480
481 self.assertEqual(
482 httplib.OK, response.status_code,
483 explain_unexpected_response(httplib.OK, response))
484
485 args = (host, username, password)
486 matcher = MockCalledOnceWith(queue=nodegroup.uuid, args=args)
487 self.assertThat(mock.apply_async, matcher)
488
463489
464class TestNodeGroupAPIAuth(MAASServerTestCase):490class TestNodeGroupAPIAuth(MAASServerTestCase):
465 """Authorization tests for nodegroup API."""491 """Authorization tests for nodegroup API."""
466492
=== modified file 'src/maasserver/utils/dblocks.py'
--- src/maasserver/utils/dblocks.py 2014-04-03 13:45:02 +0000
+++ src/maasserver/utils/dblocks.py 2014-08-29 19:22:35 +0000
@@ -14,6 +14,8 @@
14__metaclass__ = type14__metaclass__ = type
15__all__ = [15__all__ = [
16 "DatabaseLock",16 "DatabaseLock",
17 "DatabaseLockAttemptOutsideTransaction",
18 "DatabaseLockNotHeld",
17]19]
1820
19from contextlib import closing21from contextlib import closing
@@ -26,6 +28,19 @@
26classid = 2012011628classid = 20120116
2729
2830
31class DatabaseLockAttemptOutsideTransaction(Exception):
32 """A locking attempt was made outside of a transaction.
33
34 :class:`DatabaseLock` should only be used within a transaction.
35 Django agressively closes connections outside of atomic blocks to
36 the extent that session-level locks are rendered unreliable at best.
37 """
38
39
40class DatabaseLockNotHeld(Exception):
41 """A particular lock was not held."""
42
43
29class DatabaseLock(tuple):44class DatabaseLock(tuple):
30 """An advisory lock held in the database.45 """An advisory lock held in the database.
3146
@@ -58,12 +73,16 @@
58 return super(cls, DatabaseLock).__new__(cls, (classid, objid))73 return super(cls, DatabaseLock).__new__(cls, (classid, objid))
5974
60 def __enter__(self):75 def __enter__(self):
76 if not connection.in_atomic_block:
77 raise DatabaseLockAttemptOutsideTransaction(self)
61 with closing(connection.cursor()) as cursor:78 with closing(connection.cursor()) as cursor:
62 cursor.execute("SELECT pg_advisory_lock(%s, %s)", self)79 cursor.execute("SELECT pg_advisory_lock(%s, %s)", self)
6380
64 def __exit__(self, *exc_info):81 def __exit__(self, *exc_info):
65 with closing(connection.cursor()) as cursor:82 with closing(connection.cursor()) as cursor:
66 cursor.execute("SELECT pg_advisory_unlock(%s, %s)", self)83 cursor.execute("SELECT pg_advisory_unlock(%s, %s)", self)
84 if cursor.fetchone() != (True,):
85 raise DatabaseLockNotHeld(self)
6786
68 def __repr__(self):87 def __repr__(self):
69 return b"<%s classid=%d objid=%d>" % (88 return b"<%s classid=%d objid=%d>" % (
@@ -71,8 +90,18 @@
7190
72 def is_locked(self):91 def is_locked(self):
73 stmt = (92 stmt = (
74 "SELECT 1 FROM pg_locks"93 "SELECT 1 FROM pg_locks, pg_database"
75 " WHERE classid = %s AND objid = %s AND granted"94 " WHERE pg_locks.locktype = 'advisory'"
95 " AND pg_locks.classid = %s"
96 " AND pg_locks.objid = %s"
97 # objsubid is 2 when using the 2-argument version of the
98 # pg_advisory_* locking functions.
99 " AND pg_locks.objsubid = 2"
100 " AND pg_locks.granted"
101 # Advisory locks are local to each database so we join to
102 # pg_databases to discover the OID of the currrent database.
103 " AND pg_locks.database = pg_database.oid"
104 " AND pg_database.datname = current_database()"
76 )105 )
77 with closing(connection.cursor()) as cursor:106 with closing(connection.cursor()) as cursor:
78 cursor.execute(stmt, self)107 cursor.execute(stmt, self)
79108
=== modified file 'src/maasserver/utils/tests/test_dblocks.py'
--- src/maasserver/utils/tests/test_dblocks.py 2014-03-28 10:43:53 +0000
+++ src/maasserver/utils/tests/test_dblocks.py 2014-08-29 19:22:35 +0000
@@ -16,7 +16,10 @@
1616
17from contextlib import closing17from contextlib import closing
1818
19from django.db import connection19from django.db import (
20 connection,
21 transaction,
22 )
20from maasserver.utils import dblocks23from maasserver.utils import dblocks
21from maastesting.testcase import MAASTestCase24from maastesting.testcase import MAASTestCase
2225
@@ -40,6 +43,7 @@
40 lock = dblocks.DatabaseLock(self.getUniqueInteger())43 lock = dblocks.DatabaseLock(self.getUniqueInteger())
41 self.assertEqual(lock, (lock.classid, lock.objid))44 self.assertEqual(lock, (lock.classid, lock.objid))
4245
46 @transaction.atomic
43 def test_lock_actually_locked(self):47 def test_lock_actually_locked(self):
44 objid = self.getUniqueInteger()48 objid = self.getUniqueInteger()
45 lock = dblocks.DatabaseLock(objid)49 lock = dblocks.DatabaseLock(objid)
@@ -55,6 +59,7 @@
55 locks_released = locks_held - locks_held_after59 locks_released = locks_held - locks_held_after
56 self.assertEqual({objid}, locks_released)60 self.assertEqual({objid}, locks_released)
5761
62 @transaction.atomic
58 def test_is_locked(self):63 def test_is_locked(self):
59 objid = self.getUniqueInteger()64 objid = self.getUniqueInteger()
60 lock = dblocks.DatabaseLock(objid)65 lock = dblocks.DatabaseLock(objid)
@@ -64,6 +69,18 @@
64 self.assertTrue(lock.is_locked())69 self.assertTrue(lock.is_locked())
65 self.assertFalse(lock.is_locked())70 self.assertFalse(lock.is_locked())
6671
72 def test_obtaining_lock_fails_when_outside_of_transaction(self):
73 objid = self.getUniqueInteger()
74 lock = dblocks.DatabaseLock(objid)
75 self.assertRaises(
76 dblocks.DatabaseLockAttemptOutsideTransaction,
77 lock.__enter__)
78
79 def test_releasing_lock_fails_when_lock_not_held(self):
80 objid = self.getUniqueInteger()
81 lock = dblocks.DatabaseLock(objid)
82 self.assertRaises(dblocks.DatabaseLockNotHeld, lock.__exit__)
83
67 def test_repr(self):84 def test_repr(self):
68 lock = dblocks.DatabaseLock(self.getUniqueInteger())85 lock = dblocks.DatabaseLock(self.getUniqueInteger())
69 self.assertEqual(86 self.assertEqual(
7087
=== modified file 'src/maastesting/factory.py'
--- src/maastesting/factory.py 2014-03-28 10:43:53 +0000
+++ src/maastesting/factory.py 2014-08-29 19:22:35 +0000
@@ -35,6 +35,7 @@
35from uuid import uuid135from uuid import uuid1
3636
37from maastesting.fixtures import TempDirectory37from maastesting.fixtures import TempDirectory
38import mock
38from netaddr import (39from netaddr import (
39 IPAddress,40 IPAddress,
40 IPNetwork,41 IPNetwork,
@@ -264,6 +265,14 @@
264265
265 return tarball266 return tarball
266267
268 def make_streams(self, stdin=None, stdout=None, stderr=None):
269 """Make a fake return value for a SSHClient.exec_command."""
270 # stdout.read() is called so stdout can't be None.
271 if stdout is None:
272 stdout = mock.Mock()
273
274 return (stdin, stdout, stderr)
275
267276
268# Create factory singleton.277# Create factory singleton.
269factory = Factory()278factory = Factory()
270279
=== modified file 'src/metadataserver/address.py'
--- src/metadataserver/address.py 2013-10-10 17:07:51 +0000
+++ src/metadataserver/address.py 2014-08-29 19:22:35 +0000
@@ -63,13 +63,13 @@
63 """63 """
64 route_lines = list(ip_route_output)64 route_lines = list(ip_route_output)
65 for line in route_lines:65 for line in route_lines:
66 match = re.match('default\s+.*\sdev\s+(\w+)', line)66 match = re.match('default\s+.*\sdev\s+([^\s]+)', line)
67 if match is not None:67 if match is not None:
68 return match.groups()[0]68 return match.groups()[0]
6969
70 # Still nothing? Try the first recognizable interface in the list.70 # Still nothing? Try the first recognizable interface in the list.
71 for line in route_lines:71 for line in route_lines:
72 match = re.match('\s*(?:\S+\s+)*dev\s+(\w+)', line)72 match = re.match('\s*(?:\S+\s+)*dev\s+([^\s]+)', line)
73 if match is not None:73 if match is not None:
74 return match.groups()[0]74 return match.groups()[0]
75 return None75 return None
7676
=== modified file 'src/metadataserver/tests/test_address.py'
--- src/metadataserver/tests/test_address.py 2013-10-10 17:07:51 +0000
+++ src/metadataserver/tests/test_address.py 2014-08-29 19:22:35 +0000
@@ -59,6 +59,26 @@
59 self.assertEqual(59 self.assertEqual(
60 'eth1', address.find_default_interface(sample_ip_route))60 'eth1', address.find_default_interface(sample_ip_route))
6161
62 def test_find_default_interface_finds_default_tagged_interface(self):
63 sample_ip_route = [
64 "default via 10.20.64.1 dev eth0.2",
65 "10.14.0.0/16 dev br0 proto kernel scope link src 10.14.4.1",
66 "10.90.90.0/24 dev br0 proto kernel scope link src 10.90.90.1",
67 "169.254.0.0/16 dev br0 scope link metric 1000",
68 ]
69 self.assertEqual(
70 'eth0.2', address.find_default_interface(sample_ip_route))
71
72 def test_find_default_interface_finds_default_aliased_interface(self):
73 sample_ip_route = [
74 "default via 10.20.64.1 dev eth0:2",
75 "10.14.0.0/16 dev br0 proto kernel scope link src 10.14.4.1",
76 "10.90.90.0/24 dev br0 proto kernel scope link src 10.90.90.1",
77 "169.254.0.0/16 dev br0 scope link metric 1000",
78 ]
79 self.assertEqual(
80 'eth0:2', address.find_default_interface(sample_ip_route))
81
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):
63 sample_ip_route = [83 sample_ip_route = [
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 "
@@ -69,6 +89,26 @@
69 self.assertEqual(89 self.assertEqual(
70 'eth2', address.find_default_interface(sample_ip_route))90 'eth2', address.find_default_interface(sample_ip_route))
7191
92 def test_find_default_tagged_interface_makes_a_guess_if_no_default(self):
93 sample_ip_route = [
94 "10.0.0.0/24 dev eth2.4 proto kernel scope link src 10.0.0.11 "
95 "metric 2",
96 "10.1.0.0/24 dev virbr0 proto kernel scope link src 10.1.0.1",
97 "10.1.1.0/24 dev virbr1 proto kernel scope link src 10.1.1.1",
98 ]
99 self.assertEqual(
100 'eth2.4', address.find_default_interface(sample_ip_route))
101
102 def test_find_default_aliased_interface_makes_a_guess_if_no_default(self):
103 sample_ip_route = [
104 "10.0.0.0/24 dev eth2:4 proto kernel scope link src 10.0.0.11 "
105 "metric 2",
106 "10.1.0.0/24 dev virbr0 proto kernel scope link src 10.1.0.1",
107 "10.1.1.0/24 dev virbr1 proto kernel scope link src 10.1.1.1",
108 ]
109 self.assertEqual(
110 'eth2:4', address.find_default_interface(sample_ip_route))
111
72 def test_find_default_interface_returns_None_on_failure(self):112 def test_find_default_interface_returns_None_on_failure(self):
73 self.assertIsNone(address.find_default_interface([]))113 self.assertIsNone(address.find_default_interface([]))
74114
75115
=== modified file 'src/provisioningserver/boot/__init__.py'
--- src/provisioningserver/boot/__init__.py 2014-06-02 11:57:58 +0000
+++ src/provisioningserver/boot/__init__.py 2014-08-29 19:22:35 +0000
@@ -99,6 +99,10 @@
9999
100 __metaclass__ = ABCMeta100 __metaclass__ = ABCMeta
101101
102 # Path prefix that is used for the pxelinux.cfg. Used for
103 # the dhcpd.conf that is generated.
104 path_prefix = None
105
102 @abstractproperty106 @abstractproperty
103 def name(self):107 def name(self):
104 """Name of the boot method."""108 """Name of the boot method."""
@@ -223,12 +227,14 @@
223from provisioningserver.boot.pxe import PXEBootMethod227from provisioningserver.boot.pxe import PXEBootMethod
224from provisioningserver.boot.uefi import UEFIBootMethod228from provisioningserver.boot.uefi import UEFIBootMethod
225from provisioningserver.boot.powerkvm import PowerKVMBootMethod229from provisioningserver.boot.powerkvm import PowerKVMBootMethod
230from provisioningserver.boot.powernv import PowerNVBootMethod
226231
227232
228builtin_boot_methods = [233builtin_boot_methods = [
229 PXEBootMethod(),234 PXEBootMethod(),
230 UEFIBootMethod(),235 UEFIBootMethod(),
231 PowerKVMBootMethod(),236 PowerKVMBootMethod(),
237 PowerNVBootMethod(),
232]238]
233for method in builtin_boot_methods:239for method in builtin_boot_methods:
234 BootMethodRegistry.register_item(method.name, method)240 BootMethodRegistry.register_item(method.name, method)
235241
=== added file 'src/provisioningserver/boot/powernv.py'
--- src/provisioningserver/boot/powernv.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/boot/powernv.py 2014-08-29 19:22:35 +0000
@@ -0,0 +1,158 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""PowerNV Boot Method"""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 'PowerNVBootMethod',
17 ]
18
19import re
20
21from provisioningserver.boot import (
22 BootMethod,
23 BytesReader,
24 get_parameters,
25 )
26from provisioningserver.boot.pxe import (
27 ARP_HTYPE,
28 re_mac_address,
29 )
30from provisioningserver.kernel_opts import compose_kernel_command_line
31from provisioningserver.utils import find_mac_via_arp
32from tftp.backend import FilesystemReader
33from twisted.python.context import get
34
35# The pxelinux.cfg path is prefixed with the architecture for the
36# PowerNV nodes. This prefix is set by the path-prefix dhcpd option.
37# We assume that the ARP HTYPE (hardware type) that PXELINUX sends is
38# always Ethernet.
39re_config_file = r'''
40 # Optional leading slash(es).
41 ^/*
42 ppc64el # PowerNV pxe prefix, set by dhcpd
43 /
44 pxelinux[.]cfg # PXELINUX expects this.
45 /
46 (?: # either a MAC
47 {htype:02x} # ARP HTYPE.
48 -
49 (?P<mac>{re_mac_address.pattern}) # Capture MAC.
50 | # or "default"
51 default
52 )
53 $
54'''
55
56re_config_file = re_config_file.format(
57 htype=ARP_HTYPE.ETHERNET, re_mac_address=re_mac_address)
58re_config_file = re.compile(re_config_file, re.VERBOSE)
59
60
61def format_bootif(mac):
62 """Formats a mac address into the BOOTIF format, expected by
63 the linux kernel."""
64 mac = mac.replace(':', '-')
65 mac = mac.upper()
66 return '%02x-%s' % (ARP_HTYPE.ETHERNET, mac)
67
68
69class PowerNVBootMethod(BootMethod):
70
71 name = "powernv"
72 template_subdir = "pxe"
73 bootloader_path = "pxelinux.0"
74 arch_octet = "00:0E"
75 path_prefix = "ppc64el/"
76
77 def get_remote_mac(self):
78 """Gets the requestors MAC address from arp cache.
79
80 This is used, when the pxelinux.cfg is requested without the mac
81 address appended. This is needed to inject the BOOTIF into the
82 pxelinux.cfg that is returned to the node.
83 """
84 remote_host, remote_port = get("remote", (None, None))
85 return find_mac_via_arp(remote_host)
86
87 def get_params(self, backend, path):
88 """Gets the matching parameters from the requested path."""
89 match = re_config_file.match(path)
90 if match is not None:
91 return get_parameters(match)
92 if path.lstrip('/').startswith(self.path_prefix):
93 return {'path': path}
94 return None
95
96 def match_path(self, backend, path):
97 """Checks path for the configuration file that needs to be
98 generated.
99
100 :param backend: requesting backend
101 :param path: requested path
102 :returns: dict of match params from path, None if no match
103 """
104 params = self.get_params(backend, path)
105 if params is None:
106 return None
107 params['arch'] = "ppc64el"
108 if 'mac' not in params:
109 mac = self.get_remote_mac()
110 if mac is not None:
111 params['mac'] = mac
112 return params
113
114 def get_reader(self, backend, kernel_params, **extra):
115 """Render a configuration file as a unicode string.
116
117 :param backend: requesting backend
118 :param kernel_params: An instance of `KernelParameters`.
119 :param extra: Allow for other arguments. This is a safety valve;
120 parameters generated in another component (for example, see
121 `TFTPBackend.get_config_reader`) won't cause this to break.
122 """
123 # Due to the path prefix, all requested files from the client will
124 # contain that prefix. Removing the prefix from the path will return
125 # the correct path in the tftp root.
126 if 'path' in extra:
127 path = extra['path']
128 path = path.replace(self.path_prefix, '', 1)
129 target_path = backend.base.descendant(path.split('/'))
130 return FilesystemReader(target_path)
131
132 # Return empty config for PowerNV local. PowerNV fails to
133 # support the LOCALBOOT flag. Empty config will allow it
134 # to select the first device.
135 if kernel_params.purpose == 'local':
136 return BytesReader("".encode("utf-8"))
137
138 template = self.get_template(
139 kernel_params.purpose, kernel_params.arch,
140 kernel_params.subarch)
141 namespace = self.compose_template_namespace(kernel_params)
142
143 # Modify the kernel_command to inject the BOOTIF. PowerNV fails to
144 # support the IPAPPEND pxelinux flag.
145 def kernel_command(params):
146 cmd_line = compose_kernel_command_line(params)
147 if 'mac' in extra:
148 mac = extra['mac']
149 mac = format_bootif(mac)
150 return '%s BOOTIF=%s' % (cmd_line, mac)
151 return cmd_line
152
153 namespace['kernel_command'] = kernel_command
154 return BytesReader(template.substitute(namespace).encode("utf-8"))
155
156 def install_bootloader(self, destination):
157 """Does nothing. No extra boot files are required. All of the boot
158 files from PXEBootMethod will suffice."""
0159
=== added file 'src/provisioningserver/boot/tests/test_powernv.py'
--- src/provisioningserver/boot/tests/test_powernv.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/boot/tests/test_powernv.py 2014-08-29 19:22:35 +0000
@@ -0,0 +1,337 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for `provisioningserver.boot.powernv`."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17import os
18import re
19
20from maastesting.factory import factory
21from maastesting.testcase import MAASTestCase
22from provisioningserver.boot import BytesReader
23from provisioningserver.boot.powernv import (
24 ARP_HTYPE,
25 format_bootif,
26 PowerNVBootMethod,
27 re_config_file,
28 )
29from provisioningserver.boot.tests.test_pxe import parse_pxe_config
30from provisioningserver.boot.tftppath import compose_image_path
31from provisioningserver.testing.config import set_tftp_root
32from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
33from provisioningserver.tftp import TFTPBackend
34from testtools.matchers import (
35 IsInstance,
36 MatchesAll,
37 MatchesRegex,
38 Not,
39 StartsWith,
40 )
41
42
43def compose_config_path(mac):
44 """Compose the TFTP path for a PowerNV PXE configuration file.
45
46 The path returned is relative to the TFTP root, as it would be
47 identified by clients on the network.
48
49 :param mac: A MAC address, in IEEE 802 hyphen-separated form,
50 corresponding to the machine for which this configuration is
51 relevant. This relates to PXELINUX's lookup protocol.
52 :return: Path for the corresponding PXE config file as exposed over
53 TFTP.
54 """
55 # Not using os.path.join: this is a TFTP path, not a native path. Yes, in
56 # practice for us they're the same. We always assume that the ARP HTYPE
57 # (hardware type) that PXELINUX sends is Ethernet.
58 return "ppc64el/pxelinux.cfg/{htype:02x}-{mac}".format(
59 htype=ARP_HTYPE.ETHERNET, mac=mac)
60
61
62def get_example_path_and_components():
63 """Return a plausible path and its components.
64
65 The path is intended to match `re_config_file`, and the components are
66 the expected groups from a match.
67 """
68 components = {"mac": factory.getRandomMACAddress("-")}
69 config_path = compose_config_path(components["mac"])
70 return config_path, components
71
72
73class TestPowerNVBootMethod(MAASTestCase):
74
75 def make_tftp_root(self):
76 """Set, and return, a temporary TFTP root directory."""
77 tftproot = self.make_dir()
78 self.useFixture(set_tftp_root(tftproot))
79 return tftproot
80
81 def test_compose_config_path_follows_maas_pxe_directory_layout(self):
82 name = factory.make_name('config')
83 self.assertEqual(
84 'ppc64el/pxelinux.cfg/%02x-%s' % (ARP_HTYPE.ETHERNET, name),
85 compose_config_path(name))
86
87 def test_compose_config_path_does_not_include_tftp_root(self):
88 tftproot = self.make_tftp_root()
89 name = factory.make_name('config')
90 self.assertThat(
91 compose_config_path(name),
92 Not(StartsWith(tftproot)))
93
94 def test_bootloader_path(self):
95 method = PowerNVBootMethod()
96 self.assertEqual('pxelinux.0', method.bootloader_path)
97
98 def test_bootloader_path_does_not_include_tftp_root(self):
99 tftproot = self.make_tftp_root()
100 method = PowerNVBootMethod()
101 self.assertThat(
102 method.bootloader_path,
103 Not(StartsWith(tftproot)))
104
105 def test_name(self):
106 method = PowerNVBootMethod()
107 self.assertEqual('powernv', method.name)
108
109 def test_template_subdir(self):
110 method = PowerNVBootMethod()
111 self.assertEqual('pxe', method.template_subdir)
112
113 def test_arch_octet(self):
114 method = PowerNVBootMethod()
115 self.assertEqual('00:0E', method.arch_octet)
116
117 def test_path_prefix(self):
118 method = PowerNVBootMethod()
119 self.assertEqual('ppc64el/', method.path_prefix)
120
121
122class TestPowerNVBootMethodMatchPath(MAASTestCase):
123 """Tests for
124 `provisioningserver.boot.powernv.PowerNVBootMethod.match_path`.
125 """
126
127 def test_match_path_pxe_config_with_mac(self):
128 method = PowerNVBootMethod()
129 config_path, expected = get_example_path_and_components()
130 params = method.match_path(None, config_path)
131 expected['arch'] = 'ppc64el'
132 self.assertEqual(expected, params)
133
134 def test_match_path_pxe_config_without_mac(self):
135 method = PowerNVBootMethod()
136 fake_mac = factory.getRandomMACAddress()
137 self.patch(method, 'get_remote_mac').return_value = fake_mac
138 config_path = 'ppc64el/pxelinux.cfg/default'
139 params = method.match_path(None, config_path)
140 expected = {
141 'arch': 'ppc64el',
142 'mac': fake_mac,
143 }
144 self.assertEqual(expected, params)
145
146 def test_match_path_pxe_prefix_request(self):
147 method = PowerNVBootMethod()
148 fake_mac = factory.getRandomMACAddress()
149 self.patch(method, 'get_remote_mac').return_value = fake_mac
150 file_path = 'ppc64el/file'
151 params = method.match_path(None, file_path)
152 expected = {
153 'arch': 'ppc64el',
154 'mac': fake_mac,
155 'path': file_path,
156 }
157 self.assertEqual(expected, params)
158
159
160class TestPowerNVBootMethodRenderConfig(MAASTestCase):
161 """Tests for
162 `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`
163 """
164
165 def test_get_reader_install(self):
166 # Given the right configuration options, the PXE configuration is
167 # correctly rendered.
168 method = PowerNVBootMethod()
169 params = make_kernel_parameters(self, purpose="install")
170 output = method.get_reader(backend=None, kernel_params=params)
171 # The output is a BytesReader.
172 self.assertThat(output, IsInstance(BytesReader))
173 output = output.read(10000)
174 # The template has rendered without error. PXELINUX configurations
175 # typically start with a DEFAULT line.
176 self.assertThat(output, StartsWith("DEFAULT "))
177 # The PXE parameters are all set according to the options.
178 image_dir = compose_image_path(
179 arch=params.arch, subarch=params.subarch,
180 release=params.release, label=params.label)
181 self.assertThat(
182 output, MatchesAll(
183 MatchesRegex(
184 r'.*^\s+KERNEL %s/di-kernel$' % re.escape(image_dir),
185 re.MULTILINE | re.DOTALL),
186 MatchesRegex(
187 r'.*^\s+INITRD %s/di-initrd$' % re.escape(image_dir),
188 re.MULTILINE | re.DOTALL),
189 MatchesRegex(
190 r'.*^\s+APPEND .+?$',
191 re.MULTILINE | re.DOTALL)))
192
193 def test_get_reader_with_extra_arguments_does_not_affect_output(self):
194 # get_reader() allows any keyword arguments as a safety valve.
195 method = PowerNVBootMethod()
196 options = {
197 "backend": None,
198 "kernel_params": make_kernel_parameters(self, purpose="install"),
199 }
200 # Capture the output before sprinking in some random options.
201 output_before = method.get_reader(**options).read(10000)
202 # Sprinkle some magic in.
203 options.update(
204 (factory.make_name("name"), factory.make_name("value"))
205 for _ in range(10))
206 # Capture the output after sprinking in some random options.
207 output_after = method.get_reader(**options).read(10000)
208 # The generated template is the same.
209 self.assertEqual(output_before, output_after)
210
211 def test_get_reader_with_local_purpose(self):
212 # If purpose is "local", output should be empty string.
213 method = PowerNVBootMethod()
214 options = {
215 "backend": None,
216 "kernel_params": make_kernel_parameters(purpose="local"),
217 }
218 output = method.get_reader(**options).read(10000)
219 self.assertIn("", output)
220
221 def test_get_reader_appends_bootif(self):
222 method = PowerNVBootMethod()
223 fake_mac = factory.getRandomMACAddress()
224 params = make_kernel_parameters(self, purpose="install")
225 output = method.get_reader(
226 backend=None, kernel_params=params, arch='ppc64el', mac=fake_mac)
227 output = output.read(10000)
228 config = parse_pxe_config(output)
229 expected = 'BOOTIF=%s' % format_bootif(fake_mac)
230 self.assertIn(expected, config['execute']['APPEND'])
231
232
233class TestPowerNVBootMethodPathPrefix(MAASTestCase):
234 """Tests for
235 `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`.
236 """
237
238 def test_get_reader_path_prefix(self):
239 data = factory.getRandomString().encode("ascii")
240 temp_file = self.make_file(name="example", contents=data)
241 temp_dir = os.path.dirname(temp_file)
242 backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
243 method = PowerNVBootMethod()
244 options = {
245 'backend': backend,
246 'kernel_params': make_kernel_parameters(),
247 'path': 'ppc64el/example',
248 }
249 reader = method.get_reader(**options)
250 self.addCleanup(reader.finish)
251 self.assertEqual(len(data), reader.size)
252 self.assertEqual(data, reader.read(len(data)))
253 self.assertEqual(b"", reader.read(1))
254
255 def test_get_reader_path_prefix_only_removes_first_occurrence(self):
256 data = factory.getRandomString().encode("ascii")
257 temp_dir = self.make_dir()
258 temp_subdir = os.path.join(temp_dir, 'ppc64el')
259 os.mkdir(temp_subdir)
260 factory.make_file(temp_subdir, "example", data)
261 backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
262 method = PowerNVBootMethod()
263 options = {
264 'backend': backend,
265 'kernel_params': make_kernel_parameters(),
266 'path': 'ppc64el/ppc64el/example',
267 }
268 reader = method.get_reader(**options)
269 self.addCleanup(reader.finish)
270 self.assertEqual(len(data), reader.size)
271 self.assertEqual(data, reader.read(len(data)))
272 self.assertEqual(b"", reader.read(1))
273
274
275class TestPowerNVBootMethodRegex(MAASTestCase):
276 """Tests for
277 `provisioningserver.boot.powernv.PowerNVBootMethod.re_config_file`.
278 """
279
280 def test_re_config_file_is_compatible_with_config_path_generator(self):
281 # The regular expression for extracting components of the file path is
282 # compatible with the PXE config path generator.
283 for iteration in range(10):
284 config_path, args = get_example_path_and_components()
285 match = re_config_file.match(config_path)
286 self.assertIsNotNone(match, config_path)
287 self.assertEqual(args, match.groupdict())
288
289 def test_re_config_file_with_leading_slash(self):
290 # The regular expression for extracting components of the file path
291 # doesn't care if there's a leading forward slash; the TFTP server is
292 # easy on this point, so it makes sense to be also.
293 config_path, args = get_example_path_and_components()
294 # Ensure there's a leading slash.
295 config_path = "/" + config_path.lstrip("/")
296 match = re_config_file.match(config_path)
297 self.assertIsNotNone(match, config_path)
298 self.assertEqual(args, match.groupdict())
299
300 def test_re_config_file_without_leading_slash(self):
301 # The regular expression for extracting components of the file path
302 # doesn't care if there's no leading forward slash; the TFTP server is
303 # easy on this point, so it makes sense to be also.
304 config_path, args = get_example_path_and_components()
305 # Ensure there's no leading slash.
306 config_path = config_path.lstrip("/")
307 match = re_config_file.match(config_path)
308 self.assertIsNotNone(match, config_path)
309 self.assertEqual(args, match.groupdict())
310
311 def test_re_config_file_matches_classic_pxelinux_cfg(self):
312 # The default config path is simply "pxelinux.cfg" (without
313 # leading slash). The regex matches this.
314 mac = 'aa-bb-cc-dd-ee-ff'
315 match = re_config_file.match('ppc64el/pxelinux.cfg/01-%s' % mac)
316 self.assertIsNotNone(match)
317 self.assertEqual({'mac': mac}, match.groupdict())
318
319 def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self):
320 mac = 'aa-bb-cc-dd-ee-ff'
321 match = re_config_file.match('/ppc64el/pxelinux.cfg/01-%s' % mac)
322 self.assertIsNotNone(match)
323 self.assertEqual({'mac': mac}, match.groupdict())
324
325 def test_re_config_file_does_not_match_non_config_file(self):
326 self.assertIsNone(re_config_file.match('ppc64el/pxelinux.cfg/kernel'))
327
328 def test_re_config_file_does_not_match_file_in_root(self):
329 self.assertIsNone(re_config_file.match('01-aa-bb-cc-dd-ee-ff'))
330
331 def test_re_config_file_does_not_match_file_not_in_pxelinux_cfg(self):
332 self.assertIsNone(re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff'))
333
334 def test_re_config_file_with_default(self):
335 match = re_config_file.match('ppc64el/pxelinux.cfg/default')
336 self.assertIsNotNone(match)
337 self.assertEqual({'mac': None}, match.groupdict())
0338
=== modified file 'src/provisioningserver/dhcp/config.py'
--- src/provisioningserver/dhcp/config.py 2014-04-03 13:45:02 +0000
+++ src/provisioningserver/dhcp/config.py 2014-08-29 19:22:35 +0000
@@ -33,16 +33,22 @@
3333
34# Used to generate the conditional bootloader behaviour34# Used to generate the conditional bootloader behaviour
35CONDITIONAL_BOOTLOADER = """35CONDITIONAL_BOOTLOADER = """
36{behaviour} option arch = {arch_octet} {{36{{behaviour}} option arch = {{arch_octet}} {
37 filename \"{bootloader}\";37 filename \"{{bootloader}}\";
38 }}38 {{if path_prefix}}
39 option path-prefix \"{{path_prefix}}\";
40 {{endif}}
41 }
39"""42"""
4043
41# Used to generate the PXEBootLoader special case44# Used to generate the PXEBootLoader special case
42PXE_BOOTLOADER = """45PXE_BOOTLOADER = """
43else {{46else {
44 filename \"{bootloader}\";47 filename \"{{bootloader}}\";
45 }}48 {{if path_prefix}}
49 option path-prefix \"{{path_prefix}}\";
50 {{endif}}
51 }
46"""52"""
4753
4854
@@ -55,9 +61,13 @@
55 behaviour = chain(["if"], repeat("elsif"))61 behaviour = chain(["if"], repeat("elsif"))
56 for name, method in BootMethodRegistry:62 for name, method in BootMethodRegistry:
57 if name != "pxe":63 if name != "pxe":
58 output += CONDITIONAL_BOOTLOADER.format(64 output += tempita.sub(
59 behaviour=next(behaviour), arch_octet=method.arch_octet,65 CONDITIONAL_BOOTLOADER,
60 bootloader=method.bootloader_path).strip() + ' '66 behaviour=next(behaviour),
67 arch_octet=method.arch_octet,
68 bootloader=method.bootloader_path,
69 path_prefix=method.path_prefix,
70 ).strip() + ' '
6171
62 # The PXEBootMethod is used in an else statement for the generated72 # The PXEBootMethod is used in an else statement for the generated
63 # dhcpd config. This ensures that a booting node that does not73 # dhcpd config. This ensures that a booting node that does not
@@ -65,8 +75,11 @@
65 # pxelinux can still boot.75 # pxelinux can still boot.
66 pxe_method = BootMethodRegistry.get_item('pxe')76 pxe_method = BootMethodRegistry.get_item('pxe')
67 if pxe_method is not None:77 if pxe_method is not None:
68 output += PXE_BOOTLOADER.format(78 output += tempita.sub(
69 bootloader=pxe_method.bootloader_path).strip()79 PXE_BOOTLOADER,
80 bootloader=pxe_method.bootloader_path,
81 path_prefix=pxe_method.path_prefix,
82 ).strip()
70 return output.strip()83 return output.strip()
7184
7285
7386
=== modified file 'src/provisioningserver/driver/__init__.py'
--- src/provisioningserver/driver/__init__.py 2014-06-02 11:57:58 +0000
+++ src/provisioningserver/driver/__init__.py 2014-08-29 19:22:35 +0000
@@ -134,12 +134,25 @@
134 Architecture(name="i386/generic", description="i386"),134 Architecture(name="i386/generic", description="i386"),
135 Architecture(name="amd64/generic", description="amd64"),135 Architecture(name="amd64/generic", description="amd64"),
136 Architecture(136 Architecture(
137 name="arm64/generic", description="arm64/generic",
138 pxealiases=["arm"]),
139 Architecture(
140 name="arm64/xgene-uboot", description="arm64/xgene-uboot",
141 pxealiases=["arm"]),
142 Architecture(
137 name="armhf/highbank", description="armhf/highbank",143 name="armhf/highbank", description="armhf/highbank",
138 pxealiases=["arm"], kernel_options=["console=ttyAMA0"]),144 pxealiases=["arm"], kernel_options=["console=ttyAMA0"]),
139 Architecture(145 Architecture(
140 name="armhf/generic", description="armhf/generic",146 name="armhf/generic", description="armhf/generic",
141 pxealiases=["arm"], kernel_options=["console=ttyAMA0"]),147 pxealiases=["arm"], kernel_options=["console=ttyAMA0"]),
142 Architecture(name="ppc64el/generic", description="ppc64el"),148 # PPC64EL needs a rootdelay for PowerNV. The disk controller
149 # in the hardware, takes a little bit longer to come up then
150 # the initrd wants to wait. Set this to 60 seconds, just to
151 # give the booting machine enough time. This doesn't slow down
152 # the booting process, it just increases the timeout.
153 Architecture(
154 name="ppc64el/generic", description="ppc64el",
155 kernel_options=['rootdelay=60']),
143]156]
144for arch in builtin_architectures:157for arch in builtin_architectures:
145 ArchitectureRegistry.register_item(arch.name, arch)158 ArchitectureRegistry.register_item(arch.name, arch)
146159
=== added directory 'src/provisioningserver/drivers'
=== added file 'src/provisioningserver/drivers/__init__.py'
=== added directory 'src/provisioningserver/drivers/hardware'
=== added file 'src/provisioningserver/drivers/hardware/__init__.py'
=== added file 'src/provisioningserver/drivers/hardware/mscm.py'
--- src/provisioningserver/drivers/hardware/mscm.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/drivers/hardware/mscm.py 2014-08-29 19:22:35 +0000
@@ -0,0 +1,187 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Support for managing nodes via the Moonshot HP iLO Chassis Manager CLI.
5
6This module provides support for interacting with HP Moonshot iLO Chassis
7Management (MSCM) CLI via SSH, and for using that support to allow MAAS to
8manage systems via iLO.
9"""
10
11from __future__ import (
12 absolute_import,
13 print_function,
14 unicode_literals,
15 )
16str = None
17
18__metaclass__ = type
19__all__ = [
20 'power_control_mscm',
21 'probe_and_enlist_mscm',
22]
23
24import re
25
26from paramiko import (
27 AutoAddPolicy,
28 SSHClient,
29 )
30import provisioningserver.custom_hardware.utils as utils
31
32
33cartridge_mapping = {
34 'ProLiant Moonshot Cartridge': 'amd64/generic',
35 'ProLiant m300 Server Cartridge': 'amd64/generic',
36 'ProLiant m350 Server Cartridge': 'amd64/generic',
37 'ProLiant m400 Server Cartridge': 'arm64/xgene-uboot',
38 'ProLiant m500 Server Cartridge': 'amd64/generic',
39 'ProLiant m710 Server Cartridge': 'amd64/generic',
40 'ProLiant m800 Server Cartridge': 'armhf/keystone',
41 'Default': 'arm64/generic',
42}
43
44
45class MSCM_CLI_API(object):
46 """An API for interacting with the Moonshot iLO CM CLI."""
47
48 def __init__(self, host, username, password):
49 """MSCM_CLI_API Constructor."""
50 self.host = host
51 self.username = username
52 self.password = password
53 self._ssh = SSHClient()
54 self._ssh.set_missing_host_key_policy(AutoAddPolicy())
55
56 def _run_cli_command(self, command):
57 """Run a single command and return unparsed text from stdout."""
58 self._ssh.connect(
59 self.host, username=self.username, password=self.password)
60 try:
61 _, stdout, _ = self._ssh.exec_command(command)
62 output = stdout.read()
63 finally:
64 self._ssh.close()
65
66 return output
67
68 def discover_nodes(self):
69 """Discover all available nodes.
70
71 Example of stdout from running "show node list":
72
73 'show node list\r\r\nSlot ID Proc Manufacturer
74 Architecture Memory Power Health\r\n----
75 ----- ---------------------- --------------------
76 ------ ----- ------\r\n 01 c1n1 Intel Corporation
77 x86 Architecture 32 GB On OK \r\n 02 c2n1
78 N/A No Asset Information \r\n\r\n'
79
80 The regex 'c\d+n\d' is finding the node_id's c1-45n1-8
81 """
82 node_list = self._run_cli_command("show node list")
83 return re.findall(r'c\d+n\d', node_list)
84
85 def get_node_macaddr(self, node_id):
86 """Get node MAC address(es).
87
88 Example of stdout from running "show node macaddr <node_id>":
89
90 'show node macaddr c1n1\r\r\nSlot ID NIC 1 (Switch A)
91 NIC 2 (Switch B) NIC 3 (Switch A) NIC 4 (Switch B)\r\n
92 ---- ----- ----------------- ----------------- -----------------
93 -----------------\r\n 1 c1n1 a0:1d:48:b5:04:34 a0:1d:48:b5:04:35
94 a0:1d:48:b5:04:36 a0:1d:48:b5:04:37\r\n\r\n\r\n'
95
96 The regex '[\:]'.join(['[0-9A-F]{1,2}'] * 6) is finding
97 the MAC Addresses for the given node_id.
98 """
99 macs = self._run_cli_command("show node macaddr %s" % node_id)
100 return re.findall(r':'.join(['[0-9a-f]{2}'] * 6), macs)
101
102 def get_node_arch(self, node_id):
103 """Get node architecture.
104
105 Example of stdout from running "show node info <node_id>":
106
107 'show node info c1n1\r\r\n\r\nCartridge #1 \r\n Type: Compute\r\n
108 Manufacturer: HP\r\n Product Name: ProLiant m500 Server Cartridge\r\n'
109
110 Parsing this retrieves 'ProLiant m500 Server Cartridge'
111 """
112 node_detail = self._run_cli_command("show node info %s" % node_id)
113 cartridge = node_detail.split('Product Name: ')[1].splitlines()[0]
114 if cartridge in cartridge_mapping:
115 return cartridge_mapping[cartridge]
116 else:
117 return cartridge_mapping['Default']
118
119 def get_node_power_status(self, node_id):
120 """Get power state of node (on/off).
121
122 Example of stdout from running "show node power <node_id>":
123
124 'show node power c1n1\r\r\n\r\nCartridge #1\r\n Node #1\r\n
125 Power State: On\r\n'
126
127 Parsing this retrieves 'On'
128 """
129 power_state = self._run_cli_command("show node power %s" % node_id)
130 return power_state.split('Power State: ')[1].splitlines()[0]
131
132 def power_node_on(self, node_id):
133 """Power node on."""
134 return self._run_cli_command("set node power on %s" % node_id)
135
136 def power_node_off(self, node_id):
137 """Power node off."""
138 return self._run_cli_command("set node power off force %s" % node_id)
139
140 def configure_node_boot_m2(self, node_id):
141 """Configure HDD boot for node."""
142 return self._run_cli_command("set node boot M.2 %s" % node_id)
143
144 def configure_node_bootonce_pxe(self, node_id):
145 """Configure PXE boot for node once."""
146 return self._run_cli_command("set node bootonce pxe %s" % node_id)
147
148
149def power_control_mscm(host, username, password, node_id, power_change):
150 """Handle calls from the power template for nodes with a power type
151 of 'mscm'.
152 """
153 mscm = MSCM_CLI_API(host, username, password)
154 power_status = mscm.get_node_power_status(node_id)
155
156 if power_change == 'off':
157 mscm.power_node_off(node_id)
158 return
159
160 if power_change != 'on':
161 raise AssertionError('Unexpected maas power mode.')
162
163 if power_status == 'On':
164 mscm.power_node_off(node_id)
165
166 mscm.configure_node_bootonce_pxe(node_id)
167 mscm.power_node_on(node_id)
168
169
170def probe_and_enlist_mscm(host, username, password):
171 """ Extracts all of nodes from mscm, sets all of them to boot via HDD by,
172 default, sets them to bootonce via PXE, and then enlists them into MAAS.
173 """
174 mscm = MSCM_CLI_API(host, username, password)
175 nodes = mscm.discover_nodes()
176 for node_id in nodes:
177 # Set default boot to HDD
178 mscm.configure_node_boot_m2(node_id)
179 params = {
180 'power_address': host,
181 'power_user': username,
182 'power_pass': password,
183 'node_id': node_id,
184 }
185 arch = mscm.get_node_arch(node_id)
186 macs = mscm.get_node_macaddr(node_id)
187 utils.create_node(macs, arch, 'mscm', params)
0188
=== added directory 'src/provisioningserver/drivers/hardware/tests'
=== added file 'src/provisioningserver/drivers/hardware/tests/test_mscm.py'
--- src/provisioningserver/drivers/hardware/tests/test_mscm.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/drivers/hardware/tests/test_mscm.py 2014-08-29 19:22:35 +0000
@@ -0,0 +1,259 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for ``provisioningserver.drivers.hardware.mscm``."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17from random import randint
18import re
19from StringIO import StringIO
20
21from maastesting.factory import factory
22from maastesting.matchers import MockCalledOnceWith
23from maastesting.testcase import MAASTestCase
24from mock import Mock
25from provisioningserver.drivers.hardware.mscm import (
26 cartridge_mapping,
27 MSCM_CLI_API,
28 power_control_mscm,
29 probe_and_enlist_mscm,
30 )
31import provisioningserver.custom_hardware.utils as utils
32
33
34def make_mscm_api():
35 """Make a MSCM_CLI_API object with randomized parameters."""
36 host = factory.make_hostname('mscm')
37 username = factory.make_name('user')
38 password = factory.make_name('password')
39 return MSCM_CLI_API(host, username, password)
40
41
42def make_node_id():
43 """Make a node_id."""
44 return 'c%sn%s' % (randint(1, 45), randint(1, 8))
45
46
47def make_show_node_list(length=10):
48 """Make a fake return value for discover_nodes."""
49 return re.findall(r'c\d+n\d', ''.join(make_node_id()
50 for _ in xrange(length)))
51
52
53def make_show_node_macaddr(length=10):
54 """Make a fake return value for get_node_macaddr."""
55 return ''.join((factory.getRandomMACAddress() + ' ')
56 for _ in xrange(length))
57
58
59class TestRunCliCommand(MAASTestCase):
60 """Tests for ``MSCM_CLI_API.run_cli_command``."""
61
62 def test_returns_output(self):
63 api = make_mscm_api()
64 ssh_mock = self.patch(api, '_ssh')
65 expected = factory.make_name('output')
66 stdout = StringIO(expected)
67 streams = factory.make_streams(stdout=stdout)
68 ssh_mock.exec_command = Mock(return_value=streams)
69 output = api._run_cli_command(factory.make_name('command'))
70 self.assertEqual(expected, output)
71
72 def test_connects_and_closes_ssh_client(self):
73 api = make_mscm_api()
74 ssh_mock = self.patch(api, '_ssh')
75 ssh_mock.exec_command = Mock(return_value=factory.make_streams())
76 api._run_cli_command(factory.make_name('command'))
77 self.assertThat(
78 ssh_mock.connect,
79 MockCalledOnceWith(
80 api.host, username=api.username, password=api.password))
81 self.assertThat(ssh_mock.close, MockCalledOnceWith())
82
83 def test_closes_when_exception_raised(self):
84 api = make_mscm_api()
85 ssh_mock = self.patch(api, '_ssh')
86
87 def fail():
88 raise Exception('fail')
89
90 ssh_mock.exec_command = Mock(side_effect=fail)
91 command = factory.make_name('command')
92 self.assertRaises(Exception, api._run_cli_command, command)
93 self.assertThat(ssh_mock.close, MockCalledOnceWith())
94
95
96class TestDiscoverNodes(MAASTestCase):
97 """Tests for ``MSCM_CLI_API.discover_nodes``."""
98
99 def test_discover_nodes(self):
100 api = make_mscm_api()
101 ssh_mock = self.patch(api, '_ssh')
102 expected = make_show_node_list()
103 stdout = StringIO(expected)
104 streams = factory.make_streams(stdout=stdout)
105 ssh_mock.exec_command = Mock(return_value=streams)
106 output = api.discover_nodes()
107 self.assertEqual(expected, output)
108
109
110class TestNodeMACAddress(MAASTestCase):
111 """Tests for ``MSCM_CLI_API.get_node_macaddr``."""
112
113 def test_get_node_macaddr(self):
114 api = make_mscm_api()
115 expected = make_show_node_macaddr()
116 cli_mock = self.patch(api, '_run_cli_command')
117 cli_mock.return_value = expected
118 node_id = make_node_id()
119 output = api.get_node_macaddr(node_id)
120 self.assertEqual(re.findall(r':'.join(['[0-9a-f]{2}'] * 6),
121 expected), output)
122
123
124class TestNodeArch(MAASTestCase):
125 """Tests for ``MSCM_CLI_API.get_node_arch``."""
126
127 def test_get_node_arch(self):
128 api = make_mscm_api()
129 expected = '\r\n Product Name: ProLiant Moonshot Cartridge\r\n'
130 cli_mock = self.patch(api, '_run_cli_command')
131 cli_mock.return_value = expected
132 node_id = make_node_id()
133 output = api.get_node_arch(node_id)
134 key = expected.split('Product Name: ')[1].splitlines()[0]
135 self.assertEqual(cartridge_mapping[key], output)
136
137
138class TestGetNodePowerStatus(MAASTestCase):
139 """Tests for ``MSCM_CLI_API.get_node_power_status``."""
140
141 def test_get_node_power_status(self):
142 api = make_mscm_api()
143 expected = '\r\n Node #1\r\n Power State: On\r\n'
144 cli_mock = self.patch(api, '_run_cli_command')
145 cli_mock.return_value = expected
146 node_id = make_node_id()
147 output = api.get_node_power_status(node_id)
148 self.assertEqual(expected.split('Power State: ')[1].splitlines()[0],
149 output)
150
151
152class TestPowerAndConfigureNode(MAASTestCase):
153 """Tests for ``MSCM_CLI_API.configure_node_bootonce_pxe,
154 MSCM_CLI_API.power_node_on, and MSCM_CLI_API.power_node_off``.
155 """
156
157 scenarios = [
158 ('power_node_on()',
159 dict(method='power_node_on')),
160 ('power_node_off()',
161 dict(method='power_node_off')),
162 ('configure_node_bootonce_pxe()',
163 dict(method='configure_node_bootonce_pxe')),
164 ]
165
166 def test_returns_expected_outout(self):
167 api = make_mscm_api()
168 ssh_mock = self.patch(api, '_ssh')
169 expected = factory.make_name('output')
170 stdout = StringIO(expected)
171 streams = factory.make_streams(stdout=stdout)
172 ssh_mock.exec_command = Mock(return_value=streams)
173 output = getattr(api, self.method)(make_node_id())
174 self.assertEqual(expected, output)
175
176
177class TestPowerControlMSCM(MAASTestCase):
178 """Tests for ``power_control_ucsm``."""
179
180 def test_power_control_mscm_on_on(self):
181 # power_change and power_status are both 'on'
182 host = factory.make_hostname('mscm')
183 username = factory.make_name('user')
184 password = factory.make_name('password')
185 node_id = make_node_id()
186 bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
187 power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
188 power_status_mock.return_value = 'On'
189 power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
190 power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
191
192 power_control_mscm(host, username, password, node_id,
193 power_change='on')
194 self.assertThat(bootonce_mock, MockCalledOnceWith(node_id))
195 self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id))
196 self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id))
197
198 def test_power_control_mscm_on_off(self):
199 # power_change is 'on' and power_status is 'off'
200 host = factory.make_hostname('mscm')
201 username = factory.make_name('user')
202 password = factory.make_name('password')
203 node_id = make_node_id()
204 bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
205 power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
206 power_status_mock.return_value = 'Off'
207 power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
208
209 power_control_mscm(host, username, password, node_id,
210 power_change='on')
211 self.assertThat(bootonce_mock, MockCalledOnceWith(node_id))
212 self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id))
213
214 def test_power_control_mscm_off_on(self):
215 # power_change is 'off' and power_status is 'on'
216 host = factory.make_hostname('mscm')
217 username = factory.make_name('user')
218 password = factory.make_name('password')
219 node_id = make_node_id()
220 power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
221 power_status_mock.return_value = 'On'
222 power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
223
224 power_control_mscm(host, username, password, node_id,
225 power_change='off')
226 self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id))
227
228
229class TestProbeAndEnlistMSCM(MAASTestCase):
230 """Tests for ``probe_and_enlist_mscm``."""
231
232 def test_probe_and_enlist(self):
233 host = factory.make_hostname('mscm')
234 username = factory.make_name('user')
235 password = factory.make_name('password')
236 node_id = make_node_id()
237 macs = make_show_node_macaddr(4)
238 arch = 'arm64/xgene-uboot'
239 discover_nodes_mock = self.patch(MSCM_CLI_API, 'discover_nodes')
240 discover_nodes_mock.return_value = [node_id]
241 boot_m2_mock = self.patch(MSCM_CLI_API, 'configure_node_boot_m2')
242 node_arch_mock = self.patch(MSCM_CLI_API, 'get_node_arch')
243 node_arch_mock.return_value = arch
244 node_macs_mock = self.patch(MSCM_CLI_API, 'get_node_macaddr')
245 node_macs_mock.return_value = macs
246 create_node_mock = self.patch(utils, 'create_node')
247 probe_and_enlist_mscm(host, username, password)
248 self.assertThat(discover_nodes_mock, MockCalledOnceWith())
249 self.assertThat(boot_m2_mock, MockCalledOnceWith(node_id))
250 self.assertThat(node_arch_mock, MockCalledOnceWith(node_id))
251 self.assertThat(node_macs_mock, MockCalledOnceWith(node_id))
252 params = {
253 'power_address': host,
254 'power_user': username,
255 'power_pass': password,
256 'node_id': node_id,
257 }
258 self.assertThat(create_node_mock,
259 MockCalledOnceWith(macs, arch, 'mscm', params))
0260
=== modified file 'src/provisioningserver/power/tests/test_poweraction.py'
--- src/provisioningserver/power/tests/test_poweraction.py 2014-06-02 11:57:58 +0000
+++ src/provisioningserver/power/tests/test_poweraction.py 2014-08-29 19:22:35 +0000
@@ -220,3 +220,14 @@
220 power_user='bar', power_pass='baz',220 power_user='bar', power_pass='baz',
221 uuid=factory.getRandomUUID(), power_change='on')221 uuid=factory.getRandomUUID(), power_change='on')
222 self.assertIn('power_control_ucsm', script)222 self.assertIn('power_control_ucsm', script)
223
224 def test_mscm_renders_template(self):
225 # I'd like to assert that escape_py_literal is being used here,
226 # but it's not obvious how to mock things in the template
227 # rendering namespace so I passed on that.
228 action = PowerAction('mscm')
229 script = action.render_template(
230 action.get_template(), power_address='foo',
231 power_user='bar', power_pass='baz',
232 node_id='c1n1', power_change='on')
233 self.assertIn('power_control_mscm', script)
223234
=== modified file 'src/provisioningserver/power_schema.py'
--- src/provisioningserver/power_schema.py 2014-06-02 11:57:58 +0000
+++ src/provisioningserver/power_schema.py 2014-08-29 19:22:35 +0000
@@ -255,4 +255,17 @@
255 make_json_field('power_pass', "API password"),255 make_json_field('power_pass', "API password"),
256 ],256 ],
257 },257 },
258 {
259 'name': 'mscm',
260 'description': "Moonshot HP iLO Chassis Manager",
261 'fields': [
262 make_json_field('power_address', "IP for MSCM CLI API"),
263 make_json_field('power_user', "MSCM CLI API user"),
264 make_json_field('power_pass', "MSCM CLI API password"),
265 make_json_field(
266 'node_id',
267 "Node ID - Must adhere to cXnY format "
268 "(X=cartridge number, Y=node number)."),
269 ],
270 },
258]271]
259272
=== modified file 'src/provisioningserver/tasks.py'
--- src/provisioningserver/tasks.py 2014-06-02 11:57:58 +0000
+++ src/provisioningserver/tasks.py 2014-08-29 19:22:35 +0000
@@ -58,6 +58,7 @@
58 set_up_options_conf,58 set_up_options_conf,
59 setup_rndc,59 setup_rndc,
60 )60 )
61from provisioningserver.drivers.hardware.mscm import probe_and_enlist_mscm
61from provisioningserver.omshell import Omshell62from provisioningserver.omshell import Omshell
62from provisioningserver.power.poweraction import (63from provisioningserver.power.poweraction import (
63 PowerAction,64 PowerAction,
@@ -493,5 +494,12 @@
493@task494@task
494@log_exception_text495@log_exception_text
495def enlist_nodes_from_ucsm(url, username, password):496def enlist_nodes_from_ucsm(url, username, password):
496 """ See `maasserver.api.NodeGroupsHandler.enlist_nodes_from_ucsm`. """497 """ See `maasserver.api.NodeGroupHandler.enlist_nodes_from_ucsm`. """
497 probe_and_enlist_ucsm(url, username, password)498 probe_and_enlist_ucsm(url, username, password)
499
500
501@task
502@log_exception_text
503def enlist_nodes_from_mscm(host, username, password):
504 """ See `maasserver.api.NodeGroupHandler.enlist_nodes_from_mscm`. """
505 probe_and_enlist_mscm(host, username, password)
498506
=== modified file 'src/provisioningserver/tests/test_tasks.py'
--- src/provisioningserver/tests/test_tasks.py 2014-06-02 11:57:58 +0000
+++ src/provisioningserver/tests/test_tasks.py 2014-08-29 19:22:35 +0000
@@ -73,6 +73,7 @@
73from provisioningserver.tags import MissingCredentials73from provisioningserver.tags import MissingCredentials
74from provisioningserver.tasks import (74from provisioningserver.tasks import (
75 add_new_dhcp_host_map,75 add_new_dhcp_host_map,
76 enlist_nodes_from_mscm,
76 enlist_nodes_from_ucsm,77 enlist_nodes_from_ucsm,
77 import_boot_images,78 import_boot_images,
78 Omshell,79 Omshell,
@@ -661,3 +662,14 @@
661 mock = self.patch(tasks, 'probe_and_enlist_ucsm')662 mock = self.patch(tasks, 'probe_and_enlist_ucsm')
662 enlist_nodes_from_ucsm(url, username, password)663 enlist_nodes_from_ucsm(url, username, password)
663 self.assertThat(mock, MockCalledOnceWith(url, username, password))664 self.assertThat(mock, MockCalledOnceWith(url, username, password))
665
666
667class TestAddMSCM(PservTestCase):
668
669 def test_enlist_nodes_from_mscm(self):
670 host = 'host'
671 username = 'username'
672 password = 'password'
673 mock = self.patch(tasks, 'probe_and_enlist_mscm')
674 enlist_nodes_from_mscm(host, username, password)
675 self.assertThat(mock, MockCalledOnceWith(host, username, password))
664676
=== modified file 'src/provisioningserver/utils/__init__.py'
--- src/provisioningserver/utils/__init__.py 2014-04-15 14:41:32 +0000
+++ src/provisioningserver/utils/__init__.py 2014-08-29 19:22:35 +0000
@@ -829,3 +829,24 @@
829 if len(columns) == 5 and columns[2] == mac:829 if len(columns) == 5 and columns[2] == mac:
830 return columns[0]830 return columns[0]
831 return None831 return None
832
833
834def find_mac_via_arp(ip):
835 """Find the MAC address for `ip` by reading the output of arp -n.
836
837 Returns `None` if the IP is not found.
838
839 We do this because we aren't necessarily the only DHCP server on the
840 network, so we can't check our own leases file and be guaranteed to find an
841 IP that matches.
842
843 :param ip: The ip address, e.g. '192.168.1.1'.
844 """
845
846 output = call_capture_and_check(['arp', '-n']).split('\n')
847
848 for line in sorted(output):
849 columns = line.split()
850 if len(columns) == 5 and columns[0] == ip:
851 return columns[2]
852 return None
832853
=== modified file 'src/provisioningserver/utils/tests/test_utils.py'
--- src/provisioningserver/utils/tests/test_utils.py 2014-04-15 14:41:32 +0000
+++ src/provisioningserver/utils/tests/test_utils.py 2014-08-29 19:22:35 +0000
@@ -75,6 +75,7 @@
75 ExternalProcessError,75 ExternalProcessError,
76 filter_dict,76 filter_dict,
77 find_ip_via_arp,77 find_ip_via_arp,
78 find_mac_via_arp,
78 get_all_interface_addresses,79 get_all_interface_addresses,
79 get_mtime,80 get_mtime,
80 incremental_write,81 incremental_write,
@@ -1322,6 +1323,50 @@
1322 self.assertEqual("192.168.0.1", ip_address_observed)1323 self.assertEqual("192.168.0.1", ip_address_observed)
13231324
13241325
1326class TestFindMACViaARP(MAASTestCase):
1327
1328 def patch_call(self, output):
1329 """Replace `call_capture_and_check` with one that returns `output`."""
1330 fake = self.patch(provisioningserver.utils, 'call_capture_and_check')
1331 fake.return_value = output
1332 return fake
1333
1334 def test__resolves_IP_address_to_MAC(self):
1335 sample = """\
1336 Address HWtype HWaddress Flags Mask Iface
1337 192.168.100.20 (incomplete) virbr1
1338 192.168.0.104 (incomplete) eth0
1339 192.168.0.5 (incomplete) eth0
1340 192.168.0.2 (incomplete) eth0
1341 192.168.0.100 (incomplete) eth0
1342 192.168.122.20 ether 52:54:00:02:86:4b C virbr0
1343 192.168.0.4 (incomplete) eth0
1344 192.168.0.1 ether 90:f6:52:f6:17:92 C eth0
1345 """
1346
1347 call_capture_and_check = self.patch_call(sample)
1348 mac_address_observed = find_mac_via_arp("192.168.122.20")
1349 self.assertThat(
1350 call_capture_and_check,
1351 MockCalledOnceWith(['arp', '-n']))
1352 self.assertEqual("52:54:00:02:86:4b", mac_address_observed)
1353
1354 def test__returns_consistent_output(self):
1355 ip = factory.getRandomIPAddress()
1356 macs = [
1357 '52:54:00:02:86:4b',
1358 '90:f6:52:f6:17:92',
1359 ]
1360 lines = ['%s ether %s C eth0' % (ip, mac) for mac in macs]
1361 self.patch_call('\n'.join(lines))
1362 one_result = find_mac_via_arp(ip)
1363 self.patch_call('\n'.join(reversed(lines)))
1364 other_result = find_mac_via_arp(ip)
1365
1366 self.assertIn(one_result, macs)
1367 self.assertEqual(one_result, other_result)
1368
1369
1325class TestAsynchronousDecorator(MAASTestCase):1370class TestAsynchronousDecorator(MAASTestCase):
13261371
1327 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)1372 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
13281373
=== modified file 'versions.cfg'
--- versions.cfg 2014-03-28 10:43:53 +0000
+++ versions.cfg 2014-08-29 19:22:35 +0000
@@ -37,7 +37,7 @@
37nose = 1.337nose = 1.3
38nose-subunit = 0.238nose-subunit = 0.2
39python-subunit = 0.0.739python-subunit = 0.0.7
40rabbitfixture = 0.3.440rabbitfixture = 0.3.5
41sst = 0.2.241sst = 0.2.2
42testresources = 0.2.542testresources = 0.2.5
43testscenarios = 0.443testscenarios = 0.4

Subscribers

People subscribed via source and target branches