Merge lp:~ubuntu-branches/ubuntu/trusty/maas/trusty-proposed-201409031457 into lp:ubuntu/trusty-proposed/maas
- Trusty (14.04)
- trusty-proposed-201409031457
- Merge into trusty-proposed
Status: | Needs review |
---|---|
Proposed branch: | lp:~ubuntu-branches/ubuntu/trusty/maas/trusty-proposed-201409031457 |
Merge into: | lp:ubuntu/trusty-proposed/maas |
Diff against target: |
1318 lines (+983/-188) (has conflicts) 14 files modified
.pc/01-fix-database-settings.patch/contrib/maas_local_settings.py (+0/-89) .pc/02-pserv-config.patch/etc/maas/pserv.yaml (+0/-45) .pc/03-txlongpoll-config.patch/etc/txlongpoll.yaml (+0/-38) .pc/applied-patches (+0/-3) contrib/maas_local_settings.py (+4/-4) etc/maas/pserv.yaml (+4/-2) 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) etc/txlongpoll.yaml (+7/-7) src/provisioningserver/boot/powernv.py (+158/-0) src/provisioningserver/boot/tests/test_powernv.py (+337/-0) src/provisioningserver/drivers/hardware/mscm.py (+187/-0) src/provisioningserver/drivers/hardware/tests/test_mscm.py (+259/-0) Conflict adding file etc/maas/templates/power/mscm.template. Moved existing file to etc/maas/templates/power/mscm.template.moved. Conflict adding file etc/maas/templates/pxe/config.commissioning.arm64.template. Moved existing file to etc/maas/templates/pxe/config.commissioning.arm64.template.moved. Conflict adding file etc/maas/templates/pxe/config.commissioning.ppc64el.template. Moved existing file to etc/maas/templates/pxe/config.commissioning.ppc64el.template.moved. Conflict adding file etc/maas/templates/pxe/config.install.arm64.template. Moved existing file to etc/maas/templates/pxe/config.install.arm64.template.moved. Conflict adding file etc/maas/templates/pxe/config.install.ppc64el.template. Moved existing file to etc/maas/templates/pxe/config.install.ppc64el.template.moved. Conflict adding file etc/maas/templates/pxe/config.xinstall.arm64.template. Moved existing file to etc/maas/templates/pxe/config.xinstall.arm64.template.moved. Conflict adding file etc/maas/templates/pxe/config.xinstall.ppc64el.template. Moved existing file to etc/maas/templates/pxe/config.xinstall.ppc64el.template.moved. Conflict adding file src/provisioningserver/boot/powernv.py. Moved existing file to src/provisioningserver/boot/powernv.py.moved. Conflict adding file src/provisioningserver/boot/tests/test_powernv.py. Moved existing file to src/provisioningserver/boot/tests/test_powernv.py.moved. Conflict adding file src/provisioningserver/drivers. Moved existing file to src/provisioningserver/drivers.moved. |
To merge this branch: | bzr merge lp:~ubuntu-branches/ubuntu/trusty/maas/trusty-proposed-201409031457 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu Development Team | Pending | ||
Review via email: mp+233230@code.launchpad.net |
Commit message
Description of the change
The package importer has detected a possible inconsistency between the package history in the archive and the history in bzr. As the archive is authoritative the importer has made lp:ubuntu/trusty-proposed/maas reflect what is in the archive and the old bzr branch has been pushed to lp:~ubuntu-branches/ubuntu/trusty/maas/trusty-proposed-201409031457. This merge proposal was created so that an Ubuntu developer can review the situations and perform a merge/upload if necessary. There are three typical cases where this can happen.
1. Where someone pushes a change to bzr and someone else uploads the package without that change. This is the reason that this check is done by the importer. If this appears to be the case then a merge/upload should be done if the changes that were in bzr are still desirable.
2. The importer incorrectly detected the above situation when someone made a change in bzr and then uploaded it.
3. The importer incorrectly detected the above situation when someone just uploaded a package and didn't touch bzr.
If this case doesn't appear to be the first situation then set the status of the merge proposal to "Rejected" and help avoid the problem in future by filing a bug at https:/
(this is an automatically generated message)
Unmerged revisions
- 66. By Chuck Short
-
Change supported releases for install to Precise, Saucy, Trusty, Utopic
(Add Utopic; Remove Quantal/Raring) -- will still only be able to install
releases with streams available to maas (LP: #1337437) - 65. By Chuck Short
-
* New upstream bug fix release:
- Package fails to install when the default route is through an
aliased/tagged interface (LP: #1350235)
- ERROR Nonce already used (LP: #1190986)
- Add MAAS arm64/xgene support (LP: #1338851)
- Add utopic support (LP: #1337437)
- API documentation for nodegroup op=details missing parameter
(LP: #1331982)
- Reduce number of celery tasks emitted when updating a cluster controller
(LP: #1324944)
- Fix VirshSSH template which was referencing invalid attributes
(LP: #1324966)
- Fix a start up problems where a database lock was being taken outside of
a transaction (LP: #1325640, LP: #1325759)
- Reformat badly formatted Architecture error message (LP: #1301465)
- Final changes to support ppc64el (now known as PowerNV) (LP: #1315154)
- UI tweak to make navigation elements visible for documentation
* debian/control:
- maas-provisioningserver not maas-cluster- controller depends on
python-pexpect (LP: #1352273)
* debian/maas-cluster- controller. postinst
- Allow maas-pserv to bind to all IPv6 addresses too. (LP: #1342302)
* debian/control:
- python-maas-provisioni ngserver depends on python-paramiko (LP: #1334401)
* debian/extras/ 99-maas- sudoers:
- Add rule 'maas-dhcp-server stop' job.
Preview Diff
1 | === removed directory '.pc/01-fix-database-settings.patch' |
2 | === removed directory '.pc/01-fix-database-settings.patch/contrib' |
3 | === removed file '.pc/01-fix-database-settings.patch/contrib/maas_local_settings.py' |
4 | --- .pc/01-fix-database-settings.patch/contrib/maas_local_settings.py 2014-01-31 09:38:51 +0000 |
5 | +++ .pc/01-fix-database-settings.patch/contrib/maas_local_settings.py 1970-01-01 00:00:00 +0000 |
6 | @@ -1,89 +0,0 @@ |
7 | -# Debug/Production mode. |
8 | -DEBUG = False |
9 | - |
10 | -# Default URL specifying protocol, host, and (if necessary) port where |
11 | -# systems in this MAAS can find the MAAS server. Configuration can, and |
12 | -# probably should, override this. |
13 | -DEFAULT_MAAS_URL = "http://maas.internal.example.com/" |
14 | - |
15 | -# Absolute path to the directory static files should be collected to. |
16 | -STATIC_ROOT = '/var/lib/maas/static/' |
17 | - |
18 | -# Prefix to use for MAAS's urls. |
19 | -# If FORCE_SCRIPT_NAME is None (the default), all the urls will start with |
20 | -# '/'. |
21 | -FORCE_SCRIPT_NAME = '/MAAS' |
22 | - |
23 | -# Where to store the user uploaded files. |
24 | -MEDIA_ROOT = '/var/lib/maas/media/' |
25 | - |
26 | -# Use the (libjs-yui) package's files to serve YUI3. |
27 | -YUI_LOCATION = '/usr/share/javascript/yui3/' |
28 | - |
29 | -# Use the package's files to serve RaphaelJS. |
30 | -RAPHAELJS_LOCATION = '/usr/share/javascript/raphael/' |
31 | - |
32 | -# RabbitMQ settings. |
33 | -RABBITMQ_HOST = 'localhost' |
34 | -RABBITMQ_USERID = 'maas_longpoll' |
35 | -RABBITMQ_PASSWORD = '' |
36 | -RABBITMQ_VIRTUAL_HOST = '/maas_longpoll' |
37 | - |
38 | -# See http://docs.djangoproject.com/en/dev/topics/logging for |
39 | -# more details on how to customize the logging configuration. |
40 | -LOGGING_LEVEL = 'INFO' |
41 | -LOGGING = { |
42 | - 'version': 1, |
43 | - 'disable_existing_loggers': False, |
44 | - 'formatters': { |
45 | - 'simple': { |
46 | - 'format': '%(levelname)s %(asctime)s %(name)s %(message)s' |
47 | - }, |
48 | - }, |
49 | - 'handlers': { |
50 | - 'log': { |
51 | - 'class': 'logging.handlers.RotatingFileHandler', |
52 | - 'filename': '/var/log/maas/maas.log', |
53 | - 'formatter': 'simple', |
54 | - }, |
55 | - }, |
56 | - 'loggers': { |
57 | - 'maasserver': { |
58 | - 'level': LOGGING_LEVEL, |
59 | - 'handlers': ['log'], |
60 | - 'propagate': True, |
61 | - }, |
62 | - 'metadataserver': { |
63 | - 'level': LOGGING_LEVEL, |
64 | - 'handlers': ['log'], |
65 | - 'propagate': True, |
66 | - }, |
67 | - 'django.request': { |
68 | - 'level': LOGGING_LEVEL, |
69 | - 'handlers': ['log'], |
70 | - 'propagate': True, |
71 | - }, |
72 | - 'django.db.backends': { |
73 | - 'level': LOGGING_LEVEL, |
74 | - 'handlers': ['log'], |
75 | - 'propagate': True, |
76 | - }, |
77 | - 'twisted': { |
78 | - 'level': LOGGING_LEVEL, |
79 | - 'handlers': ['log'], |
80 | - 'propagate': True, |
81 | - }, |
82 | - }, |
83 | -} |
84 | - |
85 | -# Database access configuration. |
86 | -DATABASES = { |
87 | - 'default': { |
88 | - # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' etc. |
89 | - 'ENGINE': 'django.db.backends.postgresql_psycopg2', |
90 | - 'NAME': '', |
91 | - 'USER': '', |
92 | - 'PASSWORD': '', |
93 | - 'HOST': 'localhost', |
94 | - } |
95 | -} |
96 | |
97 | === removed directory '.pc/02-pserv-config.patch' |
98 | === removed directory '.pc/02-pserv-config.patch/etc' |
99 | === removed directory '.pc/02-pserv-config.patch/etc/maas' |
100 | === removed file '.pc/02-pserv-config.patch/etc/maas/pserv.yaml' |
101 | --- .pc/02-pserv-config.patch/etc/maas/pserv.yaml 2014-03-28 10:43:53 +0000 |
102 | +++ .pc/02-pserv-config.patch/etc/maas/pserv.yaml 1970-01-01 00:00:00 +0000 |
103 | @@ -1,45 +0,0 @@ |
104 | -## |
105 | -## Provisioning Server (pserv) configuration. |
106 | -## |
107 | - |
108 | -## Where to log. This log can be rotated by sending SIGUSR1 to the |
109 | -## running server. |
110 | -# |
111 | -# logfile: "pserv.log" |
112 | -logfile: "/dev/null" |
113 | - |
114 | -## OOPS configuration (optional). |
115 | -# |
116 | -oops: |
117 | - ## Directory in which to place OOPS reports. Must not contain any files |
118 | - # or directories other than what the oops machinery creates there. |
119 | - # |
120 | - # directory: |
121 | - directory: "logs/oops" |
122 | - # reporter: |
123 | - reporter: "maas-pserv" |
124 | - |
125 | -## Message broker configuration (optional, not currently used). |
126 | -# |
127 | -broker: |
128 | - # host: "localhost" |
129 | - # port: 5673 |
130 | - # username: <current user> |
131 | - # password: "test" |
132 | - # vhost: "/" |
133 | - |
134 | -## TFTP configuration. |
135 | -# |
136 | -tftp: |
137 | - # The "root" setting has been replaced by "resource_root". The old setting |
138 | - # is used one final time when upgrading a pre-14.04 cluster controller to a |
139 | - # 14.04 version. After that upgrade, it can be removed. |
140 | - # |
141 | - # resource_root: /var/lib/maas/boot-resources/current/ |
142 | - |
143 | - # port: 69 |
144 | - port: 5244 |
145 | - ## The URL to be contacted to generate PXE configurations. |
146 | - # generator: http://localhost/MAAS/api/1.0/pxeconfig/ |
147 | - generator: http://localhost:5243/api/1.0/pxeconfig/ |
148 | - |
149 | |
150 | === removed directory '.pc/03-txlongpoll-config.patch' |
151 | === removed directory '.pc/03-txlongpoll-config.patch/etc' |
152 | === removed file '.pc/03-txlongpoll-config.patch/etc/txlongpoll.yaml' |
153 | --- .pc/03-txlongpoll-config.patch/etc/txlongpoll.yaml 2012-07-03 17:42:37 +0000 |
154 | +++ .pc/03-txlongpoll-config.patch/etc/txlongpoll.yaml 1970-01-01 00:00:00 +0000 |
155 | @@ -1,38 +0,0 @@ |
156 | -## |
157 | -## txlongpoll configuration. |
158 | -## |
159 | - |
160 | -## The front-end service. |
161 | -# |
162 | -frontend: |
163 | - ## The port on which to serve. |
164 | - port: 5242 |
165 | - ## If specified, queue names requested must have the given prefix. |
166 | - # prefix: |
167 | - |
168 | -## OOPS configuration. |
169 | -# |
170 | -oops: |
171 | - ## Directory in which to place OOPS reports. Must not contain any files |
172 | - # or directories other than what the oops machinery creates there. |
173 | - # |
174 | - # directory: "" |
175 | - directory: "logs/oops" |
176 | - ## The reporter used when generating OOPS reports. |
177 | - # reporter: "LONGPOLL" |
178 | - reporter: "maas-txlongpoll" |
179 | - |
180 | -## Message broker configuration. |
181 | -# |
182 | -broker: |
183 | - # host: "localhost" |
184 | - # port: 5672 |
185 | - # username: "guest" |
186 | - # password: "guest" |
187 | - # vhost: "/" |
188 | - |
189 | -## Where to log. This log can be rotated by sending SIGUSR1 to the |
190 | -## running server. |
191 | -# |
192 | -# logfile: "txlongpoll.log" |
193 | -logfile: "/dev/null" |
194 | |
195 | === removed file '.pc/applied-patches' |
196 | --- .pc/applied-patches 2014-03-28 10:43:53 +0000 |
197 | +++ .pc/applied-patches 1970-01-01 00:00:00 +0000 |
198 | @@ -1,3 +0,0 @@ |
199 | -01-fix-database-settings.patch |
200 | -02-pserv-config.patch |
201 | -03-txlongpoll-config.patch |
202 | |
203 | === modified file 'contrib/maas_local_settings.py' |
204 | --- contrib/maas_local_settings.py 2014-01-31 09:38:51 +0000 |
205 | +++ contrib/maas_local_settings.py 2014-09-03 15:03:36 +0000 |
206 | @@ -7,7 +7,7 @@ |
207 | DEFAULT_MAAS_URL = "http://maas.internal.example.com/" |
208 | |
209 | # Absolute path to the directory static files should be collected to. |
210 | -STATIC_ROOT = '/usr/share/maas/web/static/' |
211 | +STATIC_ROOT = '/var/lib/maas/static/' |
212 | |
213 | # Prefix to use for MAAS's urls. |
214 | # If FORCE_SCRIPT_NAME is None (the default), all the urls will start with |
215 | @@ -81,9 +81,9 @@ |
216 | 'default': { |
217 | # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' etc. |
218 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', |
219 | - 'NAME': 'maasdb', |
220 | - 'USER': 'maas', |
221 | - 'PASSWORD': 'maas', |
222 | + 'NAME': '', |
223 | + 'USER': '', |
224 | + 'PASSWORD': '', |
225 | 'HOST': 'localhost', |
226 | } |
227 | } |
228 | |
229 | === modified file 'etc/maas/pserv.yaml' |
230 | --- etc/maas/pserv.yaml 2014-03-28 10:43:53 +0000 |
231 | +++ etc/maas/pserv.yaml 2014-09-03 15:03:36 +0000 |
232 | @@ -6,7 +6,7 @@ |
233 | ## running server. |
234 | # |
235 | # logfile: "pserv.log" |
236 | -logfile: "/var/log/maas/pserv.log" |
237 | +logfile: "/dev/null" |
238 | |
239 | ## OOPS configuration (optional). |
240 | # |
241 | @@ -15,7 +15,7 @@ |
242 | # or directories other than what the oops machinery creates there. |
243 | # |
244 | # directory: |
245 | - directory: "/var/log/maas/oops" |
246 | + directory: "logs/oops" |
247 | # reporter: |
248 | reporter: "maas-pserv" |
249 | |
250 | @@ -38,6 +38,8 @@ |
251 | # resource_root: /var/lib/maas/boot-resources/current/ |
252 | |
253 | # port: 69 |
254 | + port: 5244 |
255 | ## The URL to be contacted to generate PXE configurations. |
256 | # generator: http://localhost/MAAS/api/1.0/pxeconfig/ |
257 | + generator: http://localhost:5243/api/1.0/pxeconfig/ |
258 | |
259 | |
260 | === added file 'etc/maas/templates/power/mscm.template' |
261 | --- etc/maas/templates/power/mscm.template 1970-01-01 00:00:00 +0000 |
262 | +++ etc/maas/templates/power/mscm.template 2014-09-03 15:03:36 +0000 |
263 | @@ -0,0 +1,15 @@ |
264 | +# -*- mode: shell-script -*- |
265 | +# |
266 | +# Control a system via Moonshot HP iLO Chassis Manager (MSCM). |
267 | + |
268 | +{{py: from provisioningserver.utils import escape_py_literal}} |
269 | +python - << END |
270 | +from provisioningserver.drivers.hardware.mscm import power_control_mscm |
271 | +power_control_mscm( |
272 | + {{escape_py_literal(power_address) | safe}}, |
273 | + {{escape_py_literal(power_user) | safe}}, |
274 | + {{escape_py_literal(power_pass) | safe}}, |
275 | + {{escape_py_literal(node_id) | safe}}, |
276 | + {{escape_py_literal(power_change) | safe}}, |
277 | +) |
278 | +END |
279 | |
280 | === renamed file 'etc/maas/templates/power/mscm.template' => 'etc/maas/templates/power/mscm.template.moved' |
281 | === added symlink 'etc/maas/templates/pxe/config.commissioning.arm64.template' |
282 | === target is u'config.commissioning.armhf.template' |
283 | === renamed symlink 'etc/maas/templates/pxe/config.commissioning.arm64.template' => 'etc/maas/templates/pxe/config.commissioning.arm64.template.moved' |
284 | === added file 'etc/maas/templates/pxe/config.commissioning.ppc64el.template' |
285 | --- etc/maas/templates/pxe/config.commissioning.ppc64el.template 1970-01-01 00:00:00 +0000 |
286 | +++ etc/maas/templates/pxe/config.commissioning.ppc64el.template 2014-09-03 15:03:36 +0000 |
287 | @@ -0,0 +1,6 @@ |
288 | +DEFAULT execute |
289 | + |
290 | +LABEL execute |
291 | + KERNEL {{kernel_params | kernel_path }} |
292 | + INITRD {{kernel_params | initrd_path }} |
293 | + APPEND {{kernel_params | kernel_command}} |
294 | |
295 | === renamed file 'etc/maas/templates/pxe/config.commissioning.ppc64el.template' => 'etc/maas/templates/pxe/config.commissioning.ppc64el.template.moved' |
296 | === added symlink 'etc/maas/templates/pxe/config.install.arm64.template' |
297 | === target is u'config.install.armhf.template' |
298 | === renamed symlink 'etc/maas/templates/pxe/config.install.arm64.template' => 'etc/maas/templates/pxe/config.install.arm64.template.moved' |
299 | === added file 'etc/maas/templates/pxe/config.install.ppc64el.template' |
300 | --- etc/maas/templates/pxe/config.install.ppc64el.template 1970-01-01 00:00:00 +0000 |
301 | +++ etc/maas/templates/pxe/config.install.ppc64el.template 2014-09-03 15:03:36 +0000 |
302 | @@ -0,0 +1,6 @@ |
303 | +DEFAULT execute |
304 | + |
305 | +LABEL execute |
306 | + KERNEL {{kernel_params | kernel_path }} |
307 | + INITRD {{kernel_params | initrd_path }} |
308 | + APPEND {{kernel_params | kernel_command}} |
309 | |
310 | === renamed file 'etc/maas/templates/pxe/config.install.ppc64el.template' => 'etc/maas/templates/pxe/config.install.ppc64el.template.moved' |
311 | === added symlink 'etc/maas/templates/pxe/config.xinstall.arm64.template' |
312 | === target is u'config.xinstall.armhf.template' |
313 | === renamed symlink 'etc/maas/templates/pxe/config.xinstall.arm64.template' => 'etc/maas/templates/pxe/config.xinstall.arm64.template.moved' |
314 | === added symlink 'etc/maas/templates/pxe/config.xinstall.ppc64el.template' |
315 | === target is u'config.install.ppc64el.template' |
316 | === renamed symlink 'etc/maas/templates/pxe/config.xinstall.ppc64el.template' => 'etc/maas/templates/pxe/config.xinstall.ppc64el.template.moved' |
317 | === modified file 'etc/txlongpoll.yaml' |
318 | --- etc/txlongpoll.yaml 2012-07-03 17:42:37 +0000 |
319 | +++ etc/txlongpoll.yaml 2014-09-03 15:03:36 +0000 |
320 | @@ -17,7 +17,7 @@ |
321 | # or directories other than what the oops machinery creates there. |
322 | # |
323 | # directory: "" |
324 | - directory: "/var/log/maas/oops" |
325 | + directory: "logs/oops" |
326 | ## The reporter used when generating OOPS reports. |
327 | # reporter: "LONGPOLL" |
328 | reporter: "maas-txlongpoll" |
329 | @@ -25,14 +25,14 @@ |
330 | ## Message broker configuration. |
331 | # |
332 | broker: |
333 | - host: "localhost" |
334 | - port: 5672 |
335 | - username: "maas_longpoll" |
336 | - password: "maaslongpoll" |
337 | - vhost: "/maas_longpoll" |
338 | + # host: "localhost" |
339 | + # port: 5672 |
340 | + # username: "guest" |
341 | + # password: "guest" |
342 | + # vhost: "/" |
343 | |
344 | ## Where to log. This log can be rotated by sending SIGUSR1 to the |
345 | ## running server. |
346 | # |
347 | # logfile: "txlongpoll.log" |
348 | -logfile: "/var/log/maas/txlongpoll.log" |
349 | +logfile: "/dev/null" |
350 | |
351 | === added file 'src/provisioningserver/boot/powernv.py' |
352 | --- src/provisioningserver/boot/powernv.py 1970-01-01 00:00:00 +0000 |
353 | +++ src/provisioningserver/boot/powernv.py 2014-09-03 15:03:36 +0000 |
354 | @@ -0,0 +1,158 @@ |
355 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
356 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
357 | + |
358 | +"""PowerNV Boot Method""" |
359 | + |
360 | +from __future__ import ( |
361 | + absolute_import, |
362 | + print_function, |
363 | + unicode_literals, |
364 | + ) |
365 | + |
366 | +str = None |
367 | + |
368 | +__metaclass__ = type |
369 | +__all__ = [ |
370 | + 'PowerNVBootMethod', |
371 | + ] |
372 | + |
373 | +import re |
374 | + |
375 | +from provisioningserver.boot import ( |
376 | + BootMethod, |
377 | + BytesReader, |
378 | + get_parameters, |
379 | + ) |
380 | +from provisioningserver.boot.pxe import ( |
381 | + ARP_HTYPE, |
382 | + re_mac_address, |
383 | + ) |
384 | +from provisioningserver.kernel_opts import compose_kernel_command_line |
385 | +from provisioningserver.utils import find_mac_via_arp |
386 | +from tftp.backend import FilesystemReader |
387 | +from twisted.python.context import get |
388 | + |
389 | +# The pxelinux.cfg path is prefixed with the architecture for the |
390 | +# PowerNV nodes. This prefix is set by the path-prefix dhcpd option. |
391 | +# We assume that the ARP HTYPE (hardware type) that PXELINUX sends is |
392 | +# always Ethernet. |
393 | +re_config_file = r''' |
394 | + # Optional leading slash(es). |
395 | + ^/* |
396 | + ppc64el # PowerNV pxe prefix, set by dhcpd |
397 | + / |
398 | + pxelinux[.]cfg # PXELINUX expects this. |
399 | + / |
400 | + (?: # either a MAC |
401 | + {htype:02x} # ARP HTYPE. |
402 | + - |
403 | + (?P<mac>{re_mac_address.pattern}) # Capture MAC. |
404 | + | # or "default" |
405 | + default |
406 | + ) |
407 | + $ |
408 | +''' |
409 | + |
410 | +re_config_file = re_config_file.format( |
411 | + htype=ARP_HTYPE.ETHERNET, re_mac_address=re_mac_address) |
412 | +re_config_file = re.compile(re_config_file, re.VERBOSE) |
413 | + |
414 | + |
415 | +def format_bootif(mac): |
416 | + """Formats a mac address into the BOOTIF format, expected by |
417 | + the linux kernel.""" |
418 | + mac = mac.replace(':', '-') |
419 | + mac = mac.upper() |
420 | + return '%02x-%s' % (ARP_HTYPE.ETHERNET, mac) |
421 | + |
422 | + |
423 | +class PowerNVBootMethod(BootMethod): |
424 | + |
425 | + name = "powernv" |
426 | + template_subdir = "pxe" |
427 | + bootloader_path = "pxelinux.0" |
428 | + arch_octet = "00:0E" |
429 | + path_prefix = "ppc64el/" |
430 | + |
431 | + def get_remote_mac(self): |
432 | + """Gets the requestors MAC address from arp cache. |
433 | + |
434 | + This is used, when the pxelinux.cfg is requested without the mac |
435 | + address appended. This is needed to inject the BOOTIF into the |
436 | + pxelinux.cfg that is returned to the node. |
437 | + """ |
438 | + remote_host, remote_port = get("remote", (None, None)) |
439 | + return find_mac_via_arp(remote_host) |
440 | + |
441 | + def get_params(self, backend, path): |
442 | + """Gets the matching parameters from the requested path.""" |
443 | + match = re_config_file.match(path) |
444 | + if match is not None: |
445 | + return get_parameters(match) |
446 | + if path.lstrip('/').startswith(self.path_prefix): |
447 | + return {'path': path} |
448 | + return None |
449 | + |
450 | + def match_path(self, backend, path): |
451 | + """Checks path for the configuration file that needs to be |
452 | + generated. |
453 | + |
454 | + :param backend: requesting backend |
455 | + :param path: requested path |
456 | + :returns: dict of match params from path, None if no match |
457 | + """ |
458 | + params = self.get_params(backend, path) |
459 | + if params is None: |
460 | + return None |
461 | + params['arch'] = "ppc64el" |
462 | + if 'mac' not in params: |
463 | + mac = self.get_remote_mac() |
464 | + if mac is not None: |
465 | + params['mac'] = mac |
466 | + return params |
467 | + |
468 | + def get_reader(self, backend, kernel_params, **extra): |
469 | + """Render a configuration file as a unicode string. |
470 | + |
471 | + :param backend: requesting backend |
472 | + :param kernel_params: An instance of `KernelParameters`. |
473 | + :param extra: Allow for other arguments. This is a safety valve; |
474 | + parameters generated in another component (for example, see |
475 | + `TFTPBackend.get_config_reader`) won't cause this to break. |
476 | + """ |
477 | + # Due to the path prefix, all requested files from the client will |
478 | + # contain that prefix. Removing the prefix from the path will return |
479 | + # the correct path in the tftp root. |
480 | + if 'path' in extra: |
481 | + path = extra['path'] |
482 | + path = path.replace(self.path_prefix, '', 1) |
483 | + target_path = backend.base.descendant(path.split('/')) |
484 | + return FilesystemReader(target_path) |
485 | + |
486 | + # Return empty config for PowerNV local. PowerNV fails to |
487 | + # support the LOCALBOOT flag. Empty config will allow it |
488 | + # to select the first device. |
489 | + if kernel_params.purpose == 'local': |
490 | + return BytesReader("".encode("utf-8")) |
491 | + |
492 | + template = self.get_template( |
493 | + kernel_params.purpose, kernel_params.arch, |
494 | + kernel_params.subarch) |
495 | + namespace = self.compose_template_namespace(kernel_params) |
496 | + |
497 | + # Modify the kernel_command to inject the BOOTIF. PowerNV fails to |
498 | + # support the IPAPPEND pxelinux flag. |
499 | + def kernel_command(params): |
500 | + cmd_line = compose_kernel_command_line(params) |
501 | + if 'mac' in extra: |
502 | + mac = extra['mac'] |
503 | + mac = format_bootif(mac) |
504 | + return '%s BOOTIF=%s' % (cmd_line, mac) |
505 | + return cmd_line |
506 | + |
507 | + namespace['kernel_command'] = kernel_command |
508 | + return BytesReader(template.substitute(namespace).encode("utf-8")) |
509 | + |
510 | + def install_bootloader(self, destination): |
511 | + """Does nothing. No extra boot files are required. All of the boot |
512 | + files from PXEBootMethod will suffice.""" |
513 | |
514 | === renamed file 'src/provisioningserver/boot/powernv.py' => 'src/provisioningserver/boot/powernv.py.moved' |
515 | === added file 'src/provisioningserver/boot/tests/test_powernv.py' |
516 | --- src/provisioningserver/boot/tests/test_powernv.py 1970-01-01 00:00:00 +0000 |
517 | +++ src/provisioningserver/boot/tests/test_powernv.py 2014-09-03 15:03:36 +0000 |
518 | @@ -0,0 +1,337 @@ |
519 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
520 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
521 | + |
522 | +"""Tests for `provisioningserver.boot.powernv`.""" |
523 | + |
524 | +from __future__ import ( |
525 | + absolute_import, |
526 | + print_function, |
527 | + unicode_literals, |
528 | + ) |
529 | + |
530 | +str = None |
531 | + |
532 | +__metaclass__ = type |
533 | +__all__ = [] |
534 | + |
535 | +import os |
536 | +import re |
537 | + |
538 | +from maastesting.factory import factory |
539 | +from maastesting.testcase import MAASTestCase |
540 | +from provisioningserver.boot import BytesReader |
541 | +from provisioningserver.boot.powernv import ( |
542 | + ARP_HTYPE, |
543 | + format_bootif, |
544 | + PowerNVBootMethod, |
545 | + re_config_file, |
546 | + ) |
547 | +from provisioningserver.boot.tests.test_pxe import parse_pxe_config |
548 | +from provisioningserver.boot.tftppath import compose_image_path |
549 | +from provisioningserver.testing.config import set_tftp_root |
550 | +from provisioningserver.tests.test_kernel_opts import make_kernel_parameters |
551 | +from provisioningserver.tftp import TFTPBackend |
552 | +from testtools.matchers import ( |
553 | + IsInstance, |
554 | + MatchesAll, |
555 | + MatchesRegex, |
556 | + Not, |
557 | + StartsWith, |
558 | + ) |
559 | + |
560 | + |
561 | +def compose_config_path(mac): |
562 | + """Compose the TFTP path for a PowerNV PXE configuration file. |
563 | + |
564 | + The path returned is relative to the TFTP root, as it would be |
565 | + identified by clients on the network. |
566 | + |
567 | + :param mac: A MAC address, in IEEE 802 hyphen-separated form, |
568 | + corresponding to the machine for which this configuration is |
569 | + relevant. This relates to PXELINUX's lookup protocol. |
570 | + :return: Path for the corresponding PXE config file as exposed over |
571 | + TFTP. |
572 | + """ |
573 | + # Not using os.path.join: this is a TFTP path, not a native path. Yes, in |
574 | + # practice for us they're the same. We always assume that the ARP HTYPE |
575 | + # (hardware type) that PXELINUX sends is Ethernet. |
576 | + return "ppc64el/pxelinux.cfg/{htype:02x}-{mac}".format( |
577 | + htype=ARP_HTYPE.ETHERNET, mac=mac) |
578 | + |
579 | + |
580 | +def get_example_path_and_components(): |
581 | + """Return a plausible path and its components. |
582 | + |
583 | + The path is intended to match `re_config_file`, and the components are |
584 | + the expected groups from a match. |
585 | + """ |
586 | + components = {"mac": factory.getRandomMACAddress("-")} |
587 | + config_path = compose_config_path(components["mac"]) |
588 | + return config_path, components |
589 | + |
590 | + |
591 | +class TestPowerNVBootMethod(MAASTestCase): |
592 | + |
593 | + def make_tftp_root(self): |
594 | + """Set, and return, a temporary TFTP root directory.""" |
595 | + tftproot = self.make_dir() |
596 | + self.useFixture(set_tftp_root(tftproot)) |
597 | + return tftproot |
598 | + |
599 | + def test_compose_config_path_follows_maas_pxe_directory_layout(self): |
600 | + name = factory.make_name('config') |
601 | + self.assertEqual( |
602 | + 'ppc64el/pxelinux.cfg/%02x-%s' % (ARP_HTYPE.ETHERNET, name), |
603 | + compose_config_path(name)) |
604 | + |
605 | + def test_compose_config_path_does_not_include_tftp_root(self): |
606 | + tftproot = self.make_tftp_root() |
607 | + name = factory.make_name('config') |
608 | + self.assertThat( |
609 | + compose_config_path(name), |
610 | + Not(StartsWith(tftproot))) |
611 | + |
612 | + def test_bootloader_path(self): |
613 | + method = PowerNVBootMethod() |
614 | + self.assertEqual('pxelinux.0', method.bootloader_path) |
615 | + |
616 | + def test_bootloader_path_does_not_include_tftp_root(self): |
617 | + tftproot = self.make_tftp_root() |
618 | + method = PowerNVBootMethod() |
619 | + self.assertThat( |
620 | + method.bootloader_path, |
621 | + Not(StartsWith(tftproot))) |
622 | + |
623 | + def test_name(self): |
624 | + method = PowerNVBootMethod() |
625 | + self.assertEqual('powernv', method.name) |
626 | + |
627 | + def test_template_subdir(self): |
628 | + method = PowerNVBootMethod() |
629 | + self.assertEqual('pxe', method.template_subdir) |
630 | + |
631 | + def test_arch_octet(self): |
632 | + method = PowerNVBootMethod() |
633 | + self.assertEqual('00:0E', method.arch_octet) |
634 | + |
635 | + def test_path_prefix(self): |
636 | + method = PowerNVBootMethod() |
637 | + self.assertEqual('ppc64el/', method.path_prefix) |
638 | + |
639 | + |
640 | +class TestPowerNVBootMethodMatchPath(MAASTestCase): |
641 | + """Tests for |
642 | + `provisioningserver.boot.powernv.PowerNVBootMethod.match_path`. |
643 | + """ |
644 | + |
645 | + def test_match_path_pxe_config_with_mac(self): |
646 | + method = PowerNVBootMethod() |
647 | + config_path, expected = get_example_path_and_components() |
648 | + params = method.match_path(None, config_path) |
649 | + expected['arch'] = 'ppc64el' |
650 | + self.assertEqual(expected, params) |
651 | + |
652 | + def test_match_path_pxe_config_without_mac(self): |
653 | + method = PowerNVBootMethod() |
654 | + fake_mac = factory.getRandomMACAddress() |
655 | + self.patch(method, 'get_remote_mac').return_value = fake_mac |
656 | + config_path = 'ppc64el/pxelinux.cfg/default' |
657 | + params = method.match_path(None, config_path) |
658 | + expected = { |
659 | + 'arch': 'ppc64el', |
660 | + 'mac': fake_mac, |
661 | + } |
662 | + self.assertEqual(expected, params) |
663 | + |
664 | + def test_match_path_pxe_prefix_request(self): |
665 | + method = PowerNVBootMethod() |
666 | + fake_mac = factory.getRandomMACAddress() |
667 | + self.patch(method, 'get_remote_mac').return_value = fake_mac |
668 | + file_path = 'ppc64el/file' |
669 | + params = method.match_path(None, file_path) |
670 | + expected = { |
671 | + 'arch': 'ppc64el', |
672 | + 'mac': fake_mac, |
673 | + 'path': file_path, |
674 | + } |
675 | + self.assertEqual(expected, params) |
676 | + |
677 | + |
678 | +class TestPowerNVBootMethodRenderConfig(MAASTestCase): |
679 | + """Tests for |
680 | + `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader` |
681 | + """ |
682 | + |
683 | + def test_get_reader_install(self): |
684 | + # Given the right configuration options, the PXE configuration is |
685 | + # correctly rendered. |
686 | + method = PowerNVBootMethod() |
687 | + params = make_kernel_parameters(self, purpose="install") |
688 | + output = method.get_reader(backend=None, kernel_params=params) |
689 | + # The output is a BytesReader. |
690 | + self.assertThat(output, IsInstance(BytesReader)) |
691 | + output = output.read(10000) |
692 | + # The template has rendered without error. PXELINUX configurations |
693 | + # typically start with a DEFAULT line. |
694 | + self.assertThat(output, StartsWith("DEFAULT ")) |
695 | + # The PXE parameters are all set according to the options. |
696 | + image_dir = compose_image_path( |
697 | + arch=params.arch, subarch=params.subarch, |
698 | + release=params.release, label=params.label) |
699 | + self.assertThat( |
700 | + output, MatchesAll( |
701 | + MatchesRegex( |
702 | + r'.*^\s+KERNEL %s/di-kernel$' % re.escape(image_dir), |
703 | + re.MULTILINE | re.DOTALL), |
704 | + MatchesRegex( |
705 | + r'.*^\s+INITRD %s/di-initrd$' % re.escape(image_dir), |
706 | + re.MULTILINE | re.DOTALL), |
707 | + MatchesRegex( |
708 | + r'.*^\s+APPEND .+?$', |
709 | + re.MULTILINE | re.DOTALL))) |
710 | + |
711 | + def test_get_reader_with_extra_arguments_does_not_affect_output(self): |
712 | + # get_reader() allows any keyword arguments as a safety valve. |
713 | + method = PowerNVBootMethod() |
714 | + options = { |
715 | + "backend": None, |
716 | + "kernel_params": make_kernel_parameters(self, purpose="install"), |
717 | + } |
718 | + # Capture the output before sprinking in some random options. |
719 | + output_before = method.get_reader(**options).read(10000) |
720 | + # Sprinkle some magic in. |
721 | + options.update( |
722 | + (factory.make_name("name"), factory.make_name("value")) |
723 | + for _ in range(10)) |
724 | + # Capture the output after sprinking in some random options. |
725 | + output_after = method.get_reader(**options).read(10000) |
726 | + # The generated template is the same. |
727 | + self.assertEqual(output_before, output_after) |
728 | + |
729 | + def test_get_reader_with_local_purpose(self): |
730 | + # If purpose is "local", output should be empty string. |
731 | + method = PowerNVBootMethod() |
732 | + options = { |
733 | + "backend": None, |
734 | + "kernel_params": make_kernel_parameters(purpose="local"), |
735 | + } |
736 | + output = method.get_reader(**options).read(10000) |
737 | + self.assertIn("", output) |
738 | + |
739 | + def test_get_reader_appends_bootif(self): |
740 | + method = PowerNVBootMethod() |
741 | + fake_mac = factory.getRandomMACAddress() |
742 | + params = make_kernel_parameters(self, purpose="install") |
743 | + output = method.get_reader( |
744 | + backend=None, kernel_params=params, arch='ppc64el', mac=fake_mac) |
745 | + output = output.read(10000) |
746 | + config = parse_pxe_config(output) |
747 | + expected = 'BOOTIF=%s' % format_bootif(fake_mac) |
748 | + self.assertIn(expected, config['execute']['APPEND']) |
749 | + |
750 | + |
751 | +class TestPowerNVBootMethodPathPrefix(MAASTestCase): |
752 | + """Tests for |
753 | + `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`. |
754 | + """ |
755 | + |
756 | + def test_get_reader_path_prefix(self): |
757 | + data = factory.getRandomString().encode("ascii") |
758 | + temp_file = self.make_file(name="example", contents=data) |
759 | + temp_dir = os.path.dirname(temp_file) |
760 | + backend = TFTPBackend(temp_dir, "http://nowhere.example.com/") |
761 | + method = PowerNVBootMethod() |
762 | + options = { |
763 | + 'backend': backend, |
764 | + 'kernel_params': make_kernel_parameters(), |
765 | + 'path': 'ppc64el/example', |
766 | + } |
767 | + reader = method.get_reader(**options) |
768 | + self.addCleanup(reader.finish) |
769 | + self.assertEqual(len(data), reader.size) |
770 | + self.assertEqual(data, reader.read(len(data))) |
771 | + self.assertEqual(b"", reader.read(1)) |
772 | + |
773 | + def test_get_reader_path_prefix_only_removes_first_occurrence(self): |
774 | + data = factory.getRandomString().encode("ascii") |
775 | + temp_dir = self.make_dir() |
776 | + temp_subdir = os.path.join(temp_dir, 'ppc64el') |
777 | + os.mkdir(temp_subdir) |
778 | + factory.make_file(temp_subdir, "example", data) |
779 | + backend = TFTPBackend(temp_dir, "http://nowhere.example.com/") |
780 | + method = PowerNVBootMethod() |
781 | + options = { |
782 | + 'backend': backend, |
783 | + 'kernel_params': make_kernel_parameters(), |
784 | + 'path': 'ppc64el/ppc64el/example', |
785 | + } |
786 | + reader = method.get_reader(**options) |
787 | + self.addCleanup(reader.finish) |
788 | + self.assertEqual(len(data), reader.size) |
789 | + self.assertEqual(data, reader.read(len(data))) |
790 | + self.assertEqual(b"", reader.read(1)) |
791 | + |
792 | + |
793 | +class TestPowerNVBootMethodRegex(MAASTestCase): |
794 | + """Tests for |
795 | + `provisioningserver.boot.powernv.PowerNVBootMethod.re_config_file`. |
796 | + """ |
797 | + |
798 | + def test_re_config_file_is_compatible_with_config_path_generator(self): |
799 | + # The regular expression for extracting components of the file path is |
800 | + # compatible with the PXE config path generator. |
801 | + for iteration in range(10): |
802 | + config_path, args = get_example_path_and_components() |
803 | + match = re_config_file.match(config_path) |
804 | + self.assertIsNotNone(match, config_path) |
805 | + self.assertEqual(args, match.groupdict()) |
806 | + |
807 | + def test_re_config_file_with_leading_slash(self): |
808 | + # The regular expression for extracting components of the file path |
809 | + # doesn't care if there's a leading forward slash; the TFTP server is |
810 | + # easy on this point, so it makes sense to be also. |
811 | + config_path, args = get_example_path_and_components() |
812 | + # Ensure there's a leading slash. |
813 | + config_path = "/" + config_path.lstrip("/") |
814 | + match = re_config_file.match(config_path) |
815 | + self.assertIsNotNone(match, config_path) |
816 | + self.assertEqual(args, match.groupdict()) |
817 | + |
818 | + def test_re_config_file_without_leading_slash(self): |
819 | + # The regular expression for extracting components of the file path |
820 | + # doesn't care if there's no leading forward slash; the TFTP server is |
821 | + # easy on this point, so it makes sense to be also. |
822 | + config_path, args = get_example_path_and_components() |
823 | + # Ensure there's no leading slash. |
824 | + config_path = config_path.lstrip("/") |
825 | + match = re_config_file.match(config_path) |
826 | + self.assertIsNotNone(match, config_path) |
827 | + self.assertEqual(args, match.groupdict()) |
828 | + |
829 | + def test_re_config_file_matches_classic_pxelinux_cfg(self): |
830 | + # The default config path is simply "pxelinux.cfg" (without |
831 | + # leading slash). The regex matches this. |
832 | + mac = 'aa-bb-cc-dd-ee-ff' |
833 | + match = re_config_file.match('ppc64el/pxelinux.cfg/01-%s' % mac) |
834 | + self.assertIsNotNone(match) |
835 | + self.assertEqual({'mac': mac}, match.groupdict()) |
836 | + |
837 | + def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self): |
838 | + mac = 'aa-bb-cc-dd-ee-ff' |
839 | + match = re_config_file.match('/ppc64el/pxelinux.cfg/01-%s' % mac) |
840 | + self.assertIsNotNone(match) |
841 | + self.assertEqual({'mac': mac}, match.groupdict()) |
842 | + |
843 | + def test_re_config_file_does_not_match_non_config_file(self): |
844 | + self.assertIsNone(re_config_file.match('ppc64el/pxelinux.cfg/kernel')) |
845 | + |
846 | + def test_re_config_file_does_not_match_file_in_root(self): |
847 | + self.assertIsNone(re_config_file.match('01-aa-bb-cc-dd-ee-ff')) |
848 | + |
849 | + def test_re_config_file_does_not_match_file_not_in_pxelinux_cfg(self): |
850 | + self.assertIsNone(re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff')) |
851 | + |
852 | + def test_re_config_file_with_default(self): |
853 | + match = re_config_file.match('ppc64el/pxelinux.cfg/default') |
854 | + self.assertIsNotNone(match) |
855 | + self.assertEqual({'mac': None}, match.groupdict()) |
856 | |
857 | === renamed file 'src/provisioningserver/boot/tests/test_powernv.py' => 'src/provisioningserver/boot/tests/test_powernv.py.moved' |
858 | === added directory 'src/provisioningserver/drivers' |
859 | === renamed directory 'src/provisioningserver/drivers' => 'src/provisioningserver/drivers.moved' |
860 | === added file 'src/provisioningserver/drivers/__init__.py' |
861 | === added directory 'src/provisioningserver/drivers/hardware' |
862 | === added file 'src/provisioningserver/drivers/hardware/__init__.py' |
863 | === added file 'src/provisioningserver/drivers/hardware/mscm.py' |
864 | --- src/provisioningserver/drivers/hardware/mscm.py 1970-01-01 00:00:00 +0000 |
865 | +++ src/provisioningserver/drivers/hardware/mscm.py 2014-09-03 15:03:36 +0000 |
866 | @@ -0,0 +1,187 @@ |
867 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
868 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
869 | + |
870 | +"""Support for managing nodes via the Moonshot HP iLO Chassis Manager CLI. |
871 | + |
872 | +This module provides support for interacting with HP Moonshot iLO Chassis |
873 | +Management (MSCM) CLI via SSH, and for using that support to allow MAAS to |
874 | +manage systems via iLO. |
875 | +""" |
876 | + |
877 | +from __future__ import ( |
878 | + absolute_import, |
879 | + print_function, |
880 | + unicode_literals, |
881 | + ) |
882 | +str = None |
883 | + |
884 | +__metaclass__ = type |
885 | +__all__ = [ |
886 | + 'power_control_mscm', |
887 | + 'probe_and_enlist_mscm', |
888 | +] |
889 | + |
890 | +import re |
891 | + |
892 | +from paramiko import ( |
893 | + AutoAddPolicy, |
894 | + SSHClient, |
895 | + ) |
896 | +import provisioningserver.custom_hardware.utils as utils |
897 | + |
898 | + |
899 | +cartridge_mapping = { |
900 | + 'ProLiant Moonshot Cartridge': 'amd64/generic', |
901 | + 'ProLiant m300 Server Cartridge': 'amd64/generic', |
902 | + 'ProLiant m350 Server Cartridge': 'amd64/generic', |
903 | + 'ProLiant m400 Server Cartridge': 'arm64/xgene-uboot', |
904 | + 'ProLiant m500 Server Cartridge': 'amd64/generic', |
905 | + 'ProLiant m710 Server Cartridge': 'amd64/generic', |
906 | + 'ProLiant m800 Server Cartridge': 'armhf/keystone', |
907 | + 'Default': 'arm64/generic', |
908 | +} |
909 | + |
910 | + |
911 | +class MSCM_CLI_API(object): |
912 | + """An API for interacting with the Moonshot iLO CM CLI.""" |
913 | + |
914 | + def __init__(self, host, username, password): |
915 | + """MSCM_CLI_API Constructor.""" |
916 | + self.host = host |
917 | + self.username = username |
918 | + self.password = password |
919 | + self._ssh = SSHClient() |
920 | + self._ssh.set_missing_host_key_policy(AutoAddPolicy()) |
921 | + |
922 | + def _run_cli_command(self, command): |
923 | + """Run a single command and return unparsed text from stdout.""" |
924 | + self._ssh.connect( |
925 | + self.host, username=self.username, password=self.password) |
926 | + try: |
927 | + _, stdout, _ = self._ssh.exec_command(command) |
928 | + output = stdout.read() |
929 | + finally: |
930 | + self._ssh.close() |
931 | + |
932 | + return output |
933 | + |
934 | + def discover_nodes(self): |
935 | + """Discover all available nodes. |
936 | + |
937 | + Example of stdout from running "show node list": |
938 | + |
939 | + 'show node list\r\r\nSlot ID Proc Manufacturer |
940 | + Architecture Memory Power Health\r\n---- |
941 | + ----- ---------------------- -------------------- |
942 | + ------ ----- ------\r\n 01 c1n1 Intel Corporation |
943 | + x86 Architecture 32 GB On OK \r\n 02 c2n1 |
944 | + N/A No Asset Information \r\n\r\n' |
945 | + |
946 | + The regex 'c\d+n\d' is finding the node_id's c1-45n1-8 |
947 | + """ |
948 | + node_list = self._run_cli_command("show node list") |
949 | + return re.findall(r'c\d+n\d', node_list) |
950 | + |
951 | + def get_node_macaddr(self, node_id): |
952 | + """Get node MAC address(es). |
953 | + |
954 | + Example of stdout from running "show node macaddr <node_id>": |
955 | + |
956 | + 'show node macaddr c1n1\r\r\nSlot ID NIC 1 (Switch A) |
957 | + NIC 2 (Switch B) NIC 3 (Switch A) NIC 4 (Switch B)\r\n |
958 | + ---- ----- ----------------- ----------------- ----------------- |
959 | + -----------------\r\n 1 c1n1 a0:1d:48:b5:04:34 a0:1d:48:b5:04:35 |
960 | + a0:1d:48:b5:04:36 a0:1d:48:b5:04:37\r\n\r\n\r\n' |
961 | + |
962 | + The regex '[\:]'.join(['[0-9A-F]{1,2}'] * 6) is finding |
963 | + the MAC Addresses for the given node_id. |
964 | + """ |
965 | + macs = self._run_cli_command("show node macaddr %s" % node_id) |
966 | + return re.findall(r':'.join(['[0-9a-f]{2}'] * 6), macs) |
967 | + |
968 | + def get_node_arch(self, node_id): |
969 | + """Get node architecture. |
970 | + |
971 | + Example of stdout from running "show node info <node_id>": |
972 | + |
973 | + 'show node info c1n1\r\r\n\r\nCartridge #1 \r\n Type: Compute\r\n |
974 | + Manufacturer: HP\r\n Product Name: ProLiant m500 Server Cartridge\r\n' |
975 | + |
976 | + Parsing this retrieves 'ProLiant m500 Server Cartridge' |
977 | + """ |
978 | + node_detail = self._run_cli_command("show node info %s" % node_id) |
979 | + cartridge = node_detail.split('Product Name: ')[1].splitlines()[0] |
980 | + if cartridge in cartridge_mapping: |
981 | + return cartridge_mapping[cartridge] |
982 | + else: |
983 | + return cartridge_mapping['Default'] |
984 | + |
985 | + def get_node_power_status(self, node_id): |
986 | + """Get power state of node (on/off). |
987 | + |
988 | + Example of stdout from running "show node power <node_id>": |
989 | + |
990 | + 'show node power c1n1\r\r\n\r\nCartridge #1\r\n Node #1\r\n |
991 | + Power State: On\r\n' |
992 | + |
993 | + Parsing this retrieves 'On' |
994 | + """ |
995 | + power_state = self._run_cli_command("show node power %s" % node_id) |
996 | + return power_state.split('Power State: ')[1].splitlines()[0] |
997 | + |
998 | + def power_node_on(self, node_id): |
999 | + """Power node on.""" |
1000 | + return self._run_cli_command("set node power on %s" % node_id) |
1001 | + |
1002 | + def power_node_off(self, node_id): |
1003 | + """Power node off.""" |
1004 | + return self._run_cli_command("set node power off force %s" % node_id) |
1005 | + |
1006 | + def configure_node_boot_m2(self, node_id): |
1007 | + """Configure HDD boot for node.""" |
1008 | + return self._run_cli_command("set node boot M.2 %s" % node_id) |
1009 | + |
1010 | + def configure_node_bootonce_pxe(self, node_id): |
1011 | + """Configure PXE boot for node once.""" |
1012 | + return self._run_cli_command("set node bootonce pxe %s" % node_id) |
1013 | + |
1014 | + |
1015 | +def power_control_mscm(host, username, password, node_id, power_change): |
1016 | + """Handle calls from the power template for nodes with a power type |
1017 | + of 'mscm'. |
1018 | + """ |
1019 | + mscm = MSCM_CLI_API(host, username, password) |
1020 | + power_status = mscm.get_node_power_status(node_id) |
1021 | + |
1022 | + if power_change == 'off': |
1023 | + mscm.power_node_off(node_id) |
1024 | + return |
1025 | + |
1026 | + if power_change != 'on': |
1027 | + raise AssertionError('Unexpected maas power mode.') |
1028 | + |
1029 | + if power_status == 'On': |
1030 | + mscm.power_node_off(node_id) |
1031 | + |
1032 | + mscm.configure_node_bootonce_pxe(node_id) |
1033 | + mscm.power_node_on(node_id) |
1034 | + |
1035 | + |
1036 | +def probe_and_enlist_mscm(host, username, password): |
1037 | + """ Extracts all of nodes from mscm, sets all of them to boot via HDD by, |
1038 | + default, sets them to bootonce via PXE, and then enlists them into MAAS. |
1039 | + """ |
1040 | + mscm = MSCM_CLI_API(host, username, password) |
1041 | + nodes = mscm.discover_nodes() |
1042 | + for node_id in nodes: |
1043 | + # Set default boot to HDD |
1044 | + mscm.configure_node_boot_m2(node_id) |
1045 | + params = { |
1046 | + 'power_address': host, |
1047 | + 'power_user': username, |
1048 | + 'power_pass': password, |
1049 | + 'node_id': node_id, |
1050 | + } |
1051 | + arch = mscm.get_node_arch(node_id) |
1052 | + macs = mscm.get_node_macaddr(node_id) |
1053 | + utils.create_node(macs, arch, 'mscm', params) |
1054 | |
1055 | === added directory 'src/provisioningserver/drivers/hardware/tests' |
1056 | === added file 'src/provisioningserver/drivers/hardware/tests/test_mscm.py' |
1057 | --- src/provisioningserver/drivers/hardware/tests/test_mscm.py 1970-01-01 00:00:00 +0000 |
1058 | +++ src/provisioningserver/drivers/hardware/tests/test_mscm.py 2014-09-03 15:03:36 +0000 |
1059 | @@ -0,0 +1,259 @@ |
1060 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
1061 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
1062 | + |
1063 | +"""Tests for ``provisioningserver.drivers.hardware.mscm``.""" |
1064 | + |
1065 | +from __future__ import ( |
1066 | + absolute_import, |
1067 | + print_function, |
1068 | + unicode_literals, |
1069 | + ) |
1070 | + |
1071 | +str = None |
1072 | + |
1073 | +__metaclass__ = type |
1074 | +__all__ = [] |
1075 | + |
1076 | +from random import randint |
1077 | +import re |
1078 | +from StringIO import StringIO |
1079 | + |
1080 | +from maastesting.factory import factory |
1081 | +from maastesting.matchers import MockCalledOnceWith |
1082 | +from maastesting.testcase import MAASTestCase |
1083 | +from mock import Mock |
1084 | +from provisioningserver.drivers.hardware.mscm import ( |
1085 | + cartridge_mapping, |
1086 | + MSCM_CLI_API, |
1087 | + power_control_mscm, |
1088 | + probe_and_enlist_mscm, |
1089 | + ) |
1090 | +import provisioningserver.custom_hardware.utils as utils |
1091 | + |
1092 | + |
1093 | +def make_mscm_api(): |
1094 | + """Make a MSCM_CLI_API object with randomized parameters.""" |
1095 | + host = factory.make_hostname('mscm') |
1096 | + username = factory.make_name('user') |
1097 | + password = factory.make_name('password') |
1098 | + return MSCM_CLI_API(host, username, password) |
1099 | + |
1100 | + |
1101 | +def make_node_id(): |
1102 | + """Make a node_id.""" |
1103 | + return 'c%sn%s' % (randint(1, 45), randint(1, 8)) |
1104 | + |
1105 | + |
1106 | +def make_show_node_list(length=10): |
1107 | + """Make a fake return value for discover_nodes.""" |
1108 | + return re.findall(r'c\d+n\d', ''.join(make_node_id() |
1109 | + for _ in xrange(length))) |
1110 | + |
1111 | + |
1112 | +def make_show_node_macaddr(length=10): |
1113 | + """Make a fake return value for get_node_macaddr.""" |
1114 | + return ''.join((factory.getRandomMACAddress() + ' ') |
1115 | + for _ in xrange(length)) |
1116 | + |
1117 | + |
1118 | +class TestRunCliCommand(MAASTestCase): |
1119 | + """Tests for ``MSCM_CLI_API.run_cli_command``.""" |
1120 | + |
1121 | + def test_returns_output(self): |
1122 | + api = make_mscm_api() |
1123 | + ssh_mock = self.patch(api, '_ssh') |
1124 | + expected = factory.make_name('output') |
1125 | + stdout = StringIO(expected) |
1126 | + streams = factory.make_streams(stdout=stdout) |
1127 | + ssh_mock.exec_command = Mock(return_value=streams) |
1128 | + output = api._run_cli_command(factory.make_name('command')) |
1129 | + self.assertEqual(expected, output) |
1130 | + |
1131 | + def test_connects_and_closes_ssh_client(self): |
1132 | + api = make_mscm_api() |
1133 | + ssh_mock = self.patch(api, '_ssh') |
1134 | + ssh_mock.exec_command = Mock(return_value=factory.make_streams()) |
1135 | + api._run_cli_command(factory.make_name('command')) |
1136 | + self.assertThat( |
1137 | + ssh_mock.connect, |
1138 | + MockCalledOnceWith( |
1139 | + api.host, username=api.username, password=api.password)) |
1140 | + self.assertThat(ssh_mock.close, MockCalledOnceWith()) |
1141 | + |
1142 | + def test_closes_when_exception_raised(self): |
1143 | + api = make_mscm_api() |
1144 | + ssh_mock = self.patch(api, '_ssh') |
1145 | + |
1146 | + def fail(): |
1147 | + raise Exception('fail') |
1148 | + |
1149 | + ssh_mock.exec_command = Mock(side_effect=fail) |
1150 | + command = factory.make_name('command') |
1151 | + self.assertRaises(Exception, api._run_cli_command, command) |
1152 | + self.assertThat(ssh_mock.close, MockCalledOnceWith()) |
1153 | + |
1154 | + |
1155 | +class TestDiscoverNodes(MAASTestCase): |
1156 | + """Tests for ``MSCM_CLI_API.discover_nodes``.""" |
1157 | + |
1158 | + def test_discover_nodes(self): |
1159 | + api = make_mscm_api() |
1160 | + ssh_mock = self.patch(api, '_ssh') |
1161 | + expected = make_show_node_list() |
1162 | + stdout = StringIO(expected) |
1163 | + streams = factory.make_streams(stdout=stdout) |
1164 | + ssh_mock.exec_command = Mock(return_value=streams) |
1165 | + output = api.discover_nodes() |
1166 | + self.assertEqual(expected, output) |
1167 | + |
1168 | + |
1169 | +class TestNodeMACAddress(MAASTestCase): |
1170 | + """Tests for ``MSCM_CLI_API.get_node_macaddr``.""" |
1171 | + |
1172 | + def test_get_node_macaddr(self): |
1173 | + api = make_mscm_api() |
1174 | + expected = make_show_node_macaddr() |
1175 | + cli_mock = self.patch(api, '_run_cli_command') |
1176 | + cli_mock.return_value = expected |
1177 | + node_id = make_node_id() |
1178 | + output = api.get_node_macaddr(node_id) |
1179 | + self.assertEqual(re.findall(r':'.join(['[0-9a-f]{2}'] * 6), |
1180 | + expected), output) |
1181 | + |
1182 | + |
1183 | +class TestNodeArch(MAASTestCase): |
1184 | + """Tests for ``MSCM_CLI_API.get_node_arch``.""" |
1185 | + |
1186 | + def test_get_node_arch(self): |
1187 | + api = make_mscm_api() |
1188 | + expected = '\r\n Product Name: ProLiant Moonshot Cartridge\r\n' |
1189 | + cli_mock = self.patch(api, '_run_cli_command') |
1190 | + cli_mock.return_value = expected |
1191 | + node_id = make_node_id() |
1192 | + output = api.get_node_arch(node_id) |
1193 | + key = expected.split('Product Name: ')[1].splitlines()[0] |
1194 | + self.assertEqual(cartridge_mapping[key], output) |
1195 | + |
1196 | + |
1197 | +class TestGetNodePowerStatus(MAASTestCase): |
1198 | + """Tests for ``MSCM_CLI_API.get_node_power_status``.""" |
1199 | + |
1200 | + def test_get_node_power_status(self): |
1201 | + api = make_mscm_api() |
1202 | + expected = '\r\n Node #1\r\n Power State: On\r\n' |
1203 | + cli_mock = self.patch(api, '_run_cli_command') |
1204 | + cli_mock.return_value = expected |
1205 | + node_id = make_node_id() |
1206 | + output = api.get_node_power_status(node_id) |
1207 | + self.assertEqual(expected.split('Power State: ')[1].splitlines()[0], |
1208 | + output) |
1209 | + |
1210 | + |
1211 | +class TestPowerAndConfigureNode(MAASTestCase): |
1212 | + """Tests for ``MSCM_CLI_API.configure_node_bootonce_pxe, |
1213 | + MSCM_CLI_API.power_node_on, and MSCM_CLI_API.power_node_off``. |
1214 | + """ |
1215 | + |
1216 | + scenarios = [ |
1217 | + ('power_node_on()', |
1218 | + dict(method='power_node_on')), |
1219 | + ('power_node_off()', |
1220 | + dict(method='power_node_off')), |
1221 | + ('configure_node_bootonce_pxe()', |
1222 | + dict(method='configure_node_bootonce_pxe')), |
1223 | + ] |
1224 | + |
1225 | + def test_returns_expected_outout(self): |
1226 | + api = make_mscm_api() |
1227 | + ssh_mock = self.patch(api, '_ssh') |
1228 | + expected = factory.make_name('output') |
1229 | + stdout = StringIO(expected) |
1230 | + streams = factory.make_streams(stdout=stdout) |
1231 | + ssh_mock.exec_command = Mock(return_value=streams) |
1232 | + output = getattr(api, self.method)(make_node_id()) |
1233 | + self.assertEqual(expected, output) |
1234 | + |
1235 | + |
1236 | +class TestPowerControlMSCM(MAASTestCase): |
1237 | + """Tests for ``power_control_ucsm``.""" |
1238 | + |
1239 | + def test_power_control_mscm_on_on(self): |
1240 | + # power_change and power_status are both 'on' |
1241 | + host = factory.make_hostname('mscm') |
1242 | + username = factory.make_name('user') |
1243 | + password = factory.make_name('password') |
1244 | + node_id = make_node_id() |
1245 | + bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe') |
1246 | + power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status') |
1247 | + power_status_mock.return_value = 'On' |
1248 | + power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on') |
1249 | + power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off') |
1250 | + |
1251 | + power_control_mscm(host, username, password, node_id, |
1252 | + power_change='on') |
1253 | + self.assertThat(bootonce_mock, MockCalledOnceWith(node_id)) |
1254 | + self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id)) |
1255 | + self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id)) |
1256 | + |
1257 | + def test_power_control_mscm_on_off(self): |
1258 | + # power_change is 'on' and power_status is 'off' |
1259 | + host = factory.make_hostname('mscm') |
1260 | + username = factory.make_name('user') |
1261 | + password = factory.make_name('password') |
1262 | + node_id = make_node_id() |
1263 | + bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe') |
1264 | + power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status') |
1265 | + power_status_mock.return_value = 'Off' |
1266 | + power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on') |
1267 | + |
1268 | + power_control_mscm(host, username, password, node_id, |
1269 | + power_change='on') |
1270 | + self.assertThat(bootonce_mock, MockCalledOnceWith(node_id)) |
1271 | + self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id)) |
1272 | + |
1273 | + def test_power_control_mscm_off_on(self): |
1274 | + # power_change is 'off' and power_status is 'on' |
1275 | + host = factory.make_hostname('mscm') |
1276 | + username = factory.make_name('user') |
1277 | + password = factory.make_name('password') |
1278 | + node_id = make_node_id() |
1279 | + power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status') |
1280 | + power_status_mock.return_value = 'On' |
1281 | + power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off') |
1282 | + |
1283 | + power_control_mscm(host, username, password, node_id, |
1284 | + power_change='off') |
1285 | + self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id)) |
1286 | + |
1287 | + |
1288 | +class TestProbeAndEnlistMSCM(MAASTestCase): |
1289 | + """Tests for ``probe_and_enlist_mscm``.""" |
1290 | + |
1291 | + def test_probe_and_enlist(self): |
1292 | + host = factory.make_hostname('mscm') |
1293 | + username = factory.make_name('user') |
1294 | + password = factory.make_name('password') |
1295 | + node_id = make_node_id() |
1296 | + macs = make_show_node_macaddr(4) |
1297 | + arch = 'arm64/xgene-uboot' |
1298 | + discover_nodes_mock = self.patch(MSCM_CLI_API, 'discover_nodes') |
1299 | + discover_nodes_mock.return_value = [node_id] |
1300 | + boot_m2_mock = self.patch(MSCM_CLI_API, 'configure_node_boot_m2') |
1301 | + node_arch_mock = self.patch(MSCM_CLI_API, 'get_node_arch') |
1302 | + node_arch_mock.return_value = arch |
1303 | + node_macs_mock = self.patch(MSCM_CLI_API, 'get_node_macaddr') |
1304 | + node_macs_mock.return_value = macs |
1305 | + create_node_mock = self.patch(utils, 'create_node') |
1306 | + probe_and_enlist_mscm(host, username, password) |
1307 | + self.assertThat(discover_nodes_mock, MockCalledOnceWith()) |
1308 | + self.assertThat(boot_m2_mock, MockCalledOnceWith(node_id)) |
1309 | + self.assertThat(node_arch_mock, MockCalledOnceWith(node_id)) |
1310 | + self.assertThat(node_macs_mock, MockCalledOnceWith(node_id)) |
1311 | + params = { |
1312 | + 'power_address': host, |
1313 | + 'power_user': username, |
1314 | + 'power_pass': password, |
1315 | + 'node_id': node_id, |
1316 | + } |
1317 | + self.assertThat(create_node_mock, |
1318 | + MockCalledOnceWith(macs, arch, 'mscm', params)) |