Merge lp:~ubuntu-branches/ubuntu/trusty/maas/trusty-proposed-201409031457 into lp:ubuntu/trusty-proposed/maas

Proposed by Ubuntu Package Importer
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
Reviewer Review Type Date Requested Status
Ubuntu Development Team Pending
Review via email: mp+233230@code.launchpad.net

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://bugs.launchpad.net/udd linking to this merge proposal.

(this is an automatically generated message)

To post a comment you must log in.

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-provisioningserver depends on python-paramiko (LP: #1334401)
 * debian/extras/99-maas-sudoers:
  - Add rule 'maas-dhcp-server stop' job.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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))

Subscribers

People subscribed via source and target branches

to all changes: