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
=== removed directory '.pc/01-fix-database-settings.patch'
=== removed directory '.pc/01-fix-database-settings.patch/contrib'
=== removed file '.pc/01-fix-database-settings.patch/contrib/maas_local_settings.py'
--- .pc/01-fix-database-settings.patch/contrib/maas_local_settings.py 2014-01-31 09:38:51 +0000
+++ .pc/01-fix-database-settings.patch/contrib/maas_local_settings.py 1970-01-01 00:00:00 +0000
@@ -1,89 +0,0 @@
1# Debug/Production mode.
2DEBUG = False
3
4# Default URL specifying protocol, host, and (if necessary) port where
5# systems in this MAAS can find the MAAS server. Configuration can, and
6# probably should, override this.
7DEFAULT_MAAS_URL = "http://maas.internal.example.com/"
8
9# Absolute path to the directory static files should be collected to.
10STATIC_ROOT = '/var/lib/maas/static/'
11
12# Prefix to use for MAAS's urls.
13# If FORCE_SCRIPT_NAME is None (the default), all the urls will start with
14# '/'.
15FORCE_SCRIPT_NAME = '/MAAS'
16
17# Where to store the user uploaded files.
18MEDIA_ROOT = '/var/lib/maas/media/'
19
20# Use the (libjs-yui) package's files to serve YUI3.
21YUI_LOCATION = '/usr/share/javascript/yui3/'
22
23# Use the package's files to serve RaphaelJS.
24RAPHAELJS_LOCATION = '/usr/share/javascript/raphael/'
25
26# RabbitMQ settings.
27RABBITMQ_HOST = 'localhost'
28RABBITMQ_USERID = 'maas_longpoll'
29RABBITMQ_PASSWORD = ''
30RABBITMQ_VIRTUAL_HOST = '/maas_longpoll'
31
32# See http://docs.djangoproject.com/en/dev/topics/logging for
33# more details on how to customize the logging configuration.
34LOGGING_LEVEL = 'INFO'
35LOGGING = {
36 'version': 1,
37 'disable_existing_loggers': False,
38 'formatters': {
39 'simple': {
40 'format': '%(levelname)s %(asctime)s %(name)s %(message)s'
41 },
42 },
43 'handlers': {
44 'log': {
45 'class': 'logging.handlers.RotatingFileHandler',
46 'filename': '/var/log/maas/maas.log',
47 'formatter': 'simple',
48 },
49 },
50 'loggers': {
51 'maasserver': {
52 'level': LOGGING_LEVEL,
53 'handlers': ['log'],
54 'propagate': True,
55 },
56 'metadataserver': {
57 'level': LOGGING_LEVEL,
58 'handlers': ['log'],
59 'propagate': True,
60 },
61 'django.request': {
62 'level': LOGGING_LEVEL,
63 'handlers': ['log'],
64 'propagate': True,
65 },
66 'django.db.backends': {
67 'level': LOGGING_LEVEL,
68 'handlers': ['log'],
69 'propagate': True,
70 },
71 'twisted': {
72 'level': LOGGING_LEVEL,
73 'handlers': ['log'],
74 'propagate': True,
75 },
76 },
77}
78
79# Database access configuration.
80DATABASES = {
81 'default': {
82 # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' etc.
83 'ENGINE': 'django.db.backends.postgresql_psycopg2',
84 'NAME': '',
85 'USER': '',
86 'PASSWORD': '',
87 'HOST': 'localhost',
88 }
89}
900
=== removed directory '.pc/02-pserv-config.patch'
=== removed directory '.pc/02-pserv-config.patch/etc'
=== removed directory '.pc/02-pserv-config.patch/etc/maas'
=== removed file '.pc/02-pserv-config.patch/etc/maas/pserv.yaml'
--- .pc/02-pserv-config.patch/etc/maas/pserv.yaml 2014-03-28 10:43:53 +0000
+++ .pc/02-pserv-config.patch/etc/maas/pserv.yaml 1970-01-01 00:00:00 +0000
@@ -1,45 +0,0 @@
1##
2## Provisioning Server (pserv) configuration.
3##
4
5## Where to log. This log can be rotated by sending SIGUSR1 to the
6## running server.
7#
8# logfile: "pserv.log"
9logfile: "/dev/null"
10
11## OOPS configuration (optional).
12#
13oops:
14 ## Directory in which to place OOPS reports. Must not contain any files
15 # or directories other than what the oops machinery creates there.
16 #
17 # directory:
18 directory: "logs/oops"
19 # reporter:
20 reporter: "maas-pserv"
21
22## Message broker configuration (optional, not currently used).
23#
24broker:
25 # host: "localhost"
26 # port: 5673
27 # username: <current user>
28 # password: "test"
29 # vhost: "/"
30
31## TFTP configuration.
32#
33tftp:
34 # The "root" setting has been replaced by "resource_root". The old setting
35 # is used one final time when upgrading a pre-14.04 cluster controller to a
36 # 14.04 version. After that upgrade, it can be removed.
37 #
38 # resource_root: /var/lib/maas/boot-resources/current/
39
40 # port: 69
41 port: 5244
42 ## The URL to be contacted to generate PXE configurations.
43 # generator: http://localhost/MAAS/api/1.0/pxeconfig/
44 generator: http://localhost:5243/api/1.0/pxeconfig/
45
460
=== removed directory '.pc/03-txlongpoll-config.patch'
=== removed directory '.pc/03-txlongpoll-config.patch/etc'
=== removed file '.pc/03-txlongpoll-config.patch/etc/txlongpoll.yaml'
--- .pc/03-txlongpoll-config.patch/etc/txlongpoll.yaml 2012-07-03 17:42:37 +0000
+++ .pc/03-txlongpoll-config.patch/etc/txlongpoll.yaml 1970-01-01 00:00:00 +0000
@@ -1,38 +0,0 @@
1##
2## txlongpoll configuration.
3##
4
5## The front-end service.
6#
7frontend:
8 ## The port on which to serve.
9 port: 5242
10 ## If specified, queue names requested must have the given prefix.
11 # prefix:
12
13## OOPS configuration.
14#
15oops:
16 ## Directory in which to place OOPS reports. Must not contain any files
17 # or directories other than what the oops machinery creates there.
18 #
19 # directory: ""
20 directory: "logs/oops"
21 ## The reporter used when generating OOPS reports.
22 # reporter: "LONGPOLL"
23 reporter: "maas-txlongpoll"
24
25## Message broker configuration.
26#
27broker:
28 # host: "localhost"
29 # port: 5672
30 # username: "guest"
31 # password: "guest"
32 # vhost: "/"
33
34## Where to log. This log can be rotated by sending SIGUSR1 to the
35## running server.
36#
37# logfile: "txlongpoll.log"
38logfile: "/dev/null"
390
=== removed file '.pc/applied-patches'
--- .pc/applied-patches 2014-03-28 10:43:53 +0000
+++ .pc/applied-patches 1970-01-01 00:00:00 +0000
@@ -1,3 +0,0 @@
101-fix-database-settings.patch
202-pserv-config.patch
303-txlongpoll-config.patch
40
=== modified file 'contrib/maas_local_settings.py'
--- contrib/maas_local_settings.py 2014-01-31 09:38:51 +0000
+++ contrib/maas_local_settings.py 2014-09-03 15:03:36 +0000
@@ -7,7 +7,7 @@
7DEFAULT_MAAS_URL = "http://maas.internal.example.com/"7DEFAULT_MAAS_URL = "http://maas.internal.example.com/"
88
9# Absolute path to the directory static files should be collected to.9# Absolute path to the directory static files should be collected to.
10STATIC_ROOT = '/usr/share/maas/web/static/'10STATIC_ROOT = '/var/lib/maas/static/'
1111
12# Prefix to use for MAAS's urls.12# Prefix to use for MAAS's urls.
13# If FORCE_SCRIPT_NAME is None (the default), all the urls will start with13# If FORCE_SCRIPT_NAME is None (the default), all the urls will start with
@@ -81,9 +81,9 @@
81 'default': {81 'default': {
82 # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' etc.82 # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' etc.
83 'ENGINE': 'django.db.backends.postgresql_psycopg2',83 'ENGINE': 'django.db.backends.postgresql_psycopg2',
84 'NAME': 'maasdb',84 'NAME': '',
85 'USER': 'maas',85 'USER': '',
86 'PASSWORD': 'maas',86 'PASSWORD': '',
87 'HOST': 'localhost',87 'HOST': 'localhost',
88 }88 }
89}89}
9090
=== modified file 'etc/maas/pserv.yaml'
--- etc/maas/pserv.yaml 2014-03-28 10:43:53 +0000
+++ etc/maas/pserv.yaml 2014-09-03 15:03:36 +0000
@@ -6,7 +6,7 @@
6## running server.6## running server.
7#7#
8# logfile: "pserv.log"8# logfile: "pserv.log"
9logfile: "/var/log/maas/pserv.log"9logfile: "/dev/null"
1010
11## OOPS configuration (optional).11## OOPS configuration (optional).
12#12#
@@ -15,7 +15,7 @@
15 # or directories other than what the oops machinery creates there.15 # or directories other than what the oops machinery creates there.
16 #16 #
17 # directory:17 # directory:
18 directory: "/var/log/maas/oops"18 directory: "logs/oops"
19 # reporter:19 # reporter:
20 reporter: "maas-pserv"20 reporter: "maas-pserv"
2121
@@ -38,6 +38,8 @@
38 # resource_root: /var/lib/maas/boot-resources/current/38 # resource_root: /var/lib/maas/boot-resources/current/
3939
40 # port: 6940 # port: 69
41 port: 5244
41 ## The URL to be contacted to generate PXE configurations.42 ## The URL to be contacted to generate PXE configurations.
42 # generator: http://localhost/MAAS/api/1.0/pxeconfig/43 # generator: http://localhost/MAAS/api/1.0/pxeconfig/
44 generator: http://localhost:5243/api/1.0/pxeconfig/
4345
4446
=== added file 'etc/maas/templates/power/mscm.template'
--- etc/maas/templates/power/mscm.template 1970-01-01 00:00:00 +0000
+++ etc/maas/templates/power/mscm.template 2014-09-03 15:03:36 +0000
@@ -0,0 +1,15 @@
1# -*- mode: shell-script -*-
2#
3# Control a system via Moonshot HP iLO Chassis Manager (MSCM).
4
5{{py: from provisioningserver.utils import escape_py_literal}}
6python - << END
7from provisioningserver.drivers.hardware.mscm import power_control_mscm
8power_control_mscm(
9 {{escape_py_literal(power_address) | safe}},
10 {{escape_py_literal(power_user) | safe}},
11 {{escape_py_literal(power_pass) | safe}},
12 {{escape_py_literal(node_id) | safe}},
13 {{escape_py_literal(power_change) | safe}},
14)
15END
016
=== renamed file 'etc/maas/templates/power/mscm.template' => 'etc/maas/templates/power/mscm.template.moved'
=== added symlink 'etc/maas/templates/pxe/config.commissioning.arm64.template'
=== target is u'config.commissioning.armhf.template'
=== renamed symlink 'etc/maas/templates/pxe/config.commissioning.arm64.template' => 'etc/maas/templates/pxe/config.commissioning.arm64.template.moved'
=== added file 'etc/maas/templates/pxe/config.commissioning.ppc64el.template'
--- etc/maas/templates/pxe/config.commissioning.ppc64el.template 1970-01-01 00:00:00 +0000
+++ etc/maas/templates/pxe/config.commissioning.ppc64el.template 2014-09-03 15:03:36 +0000
@@ -0,0 +1,6 @@
1DEFAULT execute
2
3LABEL execute
4 KERNEL {{kernel_params | kernel_path }}
5 INITRD {{kernel_params | initrd_path }}
6 APPEND {{kernel_params | kernel_command}}
07
=== renamed file 'etc/maas/templates/pxe/config.commissioning.ppc64el.template' => 'etc/maas/templates/pxe/config.commissioning.ppc64el.template.moved'
=== added symlink 'etc/maas/templates/pxe/config.install.arm64.template'
=== target is u'config.install.armhf.template'
=== renamed symlink 'etc/maas/templates/pxe/config.install.arm64.template' => 'etc/maas/templates/pxe/config.install.arm64.template.moved'
=== added file 'etc/maas/templates/pxe/config.install.ppc64el.template'
--- etc/maas/templates/pxe/config.install.ppc64el.template 1970-01-01 00:00:00 +0000
+++ etc/maas/templates/pxe/config.install.ppc64el.template 2014-09-03 15:03:36 +0000
@@ -0,0 +1,6 @@
1DEFAULT execute
2
3LABEL execute
4 KERNEL {{kernel_params | kernel_path }}
5 INITRD {{kernel_params | initrd_path }}
6 APPEND {{kernel_params | kernel_command}}
07
=== renamed file 'etc/maas/templates/pxe/config.install.ppc64el.template' => 'etc/maas/templates/pxe/config.install.ppc64el.template.moved'
=== added symlink 'etc/maas/templates/pxe/config.xinstall.arm64.template'
=== target is u'config.xinstall.armhf.template'
=== renamed symlink 'etc/maas/templates/pxe/config.xinstall.arm64.template' => 'etc/maas/templates/pxe/config.xinstall.arm64.template.moved'
=== added symlink 'etc/maas/templates/pxe/config.xinstall.ppc64el.template'
=== target is u'config.install.ppc64el.template'
=== renamed symlink 'etc/maas/templates/pxe/config.xinstall.ppc64el.template' => 'etc/maas/templates/pxe/config.xinstall.ppc64el.template.moved'
=== modified file 'etc/txlongpoll.yaml'
--- etc/txlongpoll.yaml 2012-07-03 17:42:37 +0000
+++ etc/txlongpoll.yaml 2014-09-03 15:03:36 +0000
@@ -17,7 +17,7 @@
17 # or directories other than what the oops machinery creates there.17 # or directories other than what the oops machinery creates there.
18 #18 #
19 # directory: ""19 # directory: ""
20 directory: "/var/log/maas/oops"20 directory: "logs/oops"
21 ## The reporter used when generating OOPS reports.21 ## The reporter used when generating OOPS reports.
22 # reporter: "LONGPOLL"22 # reporter: "LONGPOLL"
23 reporter: "maas-txlongpoll"23 reporter: "maas-txlongpoll"
@@ -25,14 +25,14 @@
25## Message broker configuration.25## Message broker configuration.
26#26#
27broker:27broker:
28 host: "localhost"28 # host: "localhost"
29 port: 567229 # port: 5672
30 username: "maas_longpoll"30 # username: "guest"
31 password: "maaslongpoll"31 # password: "guest"
32 vhost: "/maas_longpoll"32 # vhost: "/"
3333
34## Where to log. This log can be rotated by sending SIGUSR1 to the34## Where to log. This log can be rotated by sending SIGUSR1 to the
35## running server.35## running server.
36#36#
37# logfile: "txlongpoll.log"37# logfile: "txlongpoll.log"
38logfile: "/var/log/maas/txlongpoll.log"38logfile: "/dev/null"
3939
=== added file 'src/provisioningserver/boot/powernv.py'
--- src/provisioningserver/boot/powernv.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/boot/powernv.py 2014-09-03 15:03:36 +0000
@@ -0,0 +1,158 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""PowerNV Boot Method"""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 'PowerNVBootMethod',
17 ]
18
19import re
20
21from provisioningserver.boot import (
22 BootMethod,
23 BytesReader,
24 get_parameters,
25 )
26from provisioningserver.boot.pxe import (
27 ARP_HTYPE,
28 re_mac_address,
29 )
30from provisioningserver.kernel_opts import compose_kernel_command_line
31from provisioningserver.utils import find_mac_via_arp
32from tftp.backend import FilesystemReader
33from twisted.python.context import get
34
35# The pxelinux.cfg path is prefixed with the architecture for the
36# PowerNV nodes. This prefix is set by the path-prefix dhcpd option.
37# We assume that the ARP HTYPE (hardware type) that PXELINUX sends is
38# always Ethernet.
39re_config_file = r'''
40 # Optional leading slash(es).
41 ^/*
42 ppc64el # PowerNV pxe prefix, set by dhcpd
43 /
44 pxelinux[.]cfg # PXELINUX expects this.
45 /
46 (?: # either a MAC
47 {htype:02x} # ARP HTYPE.
48 -
49 (?P<mac>{re_mac_address.pattern}) # Capture MAC.
50 | # or "default"
51 default
52 )
53 $
54'''
55
56re_config_file = re_config_file.format(
57 htype=ARP_HTYPE.ETHERNET, re_mac_address=re_mac_address)
58re_config_file = re.compile(re_config_file, re.VERBOSE)
59
60
61def format_bootif(mac):
62 """Formats a mac address into the BOOTIF format, expected by
63 the linux kernel."""
64 mac = mac.replace(':', '-')
65 mac = mac.upper()
66 return '%02x-%s' % (ARP_HTYPE.ETHERNET, mac)
67
68
69class PowerNVBootMethod(BootMethod):
70
71 name = "powernv"
72 template_subdir = "pxe"
73 bootloader_path = "pxelinux.0"
74 arch_octet = "00:0E"
75 path_prefix = "ppc64el/"
76
77 def get_remote_mac(self):
78 """Gets the requestors MAC address from arp cache.
79
80 This is used, when the pxelinux.cfg is requested without the mac
81 address appended. This is needed to inject the BOOTIF into the
82 pxelinux.cfg that is returned to the node.
83 """
84 remote_host, remote_port = get("remote", (None, None))
85 return find_mac_via_arp(remote_host)
86
87 def get_params(self, backend, path):
88 """Gets the matching parameters from the requested path."""
89 match = re_config_file.match(path)
90 if match is not None:
91 return get_parameters(match)
92 if path.lstrip('/').startswith(self.path_prefix):
93 return {'path': path}
94 return None
95
96 def match_path(self, backend, path):
97 """Checks path for the configuration file that needs to be
98 generated.
99
100 :param backend: requesting backend
101 :param path: requested path
102 :returns: dict of match params from path, None if no match
103 """
104 params = self.get_params(backend, path)
105 if params is None:
106 return None
107 params['arch'] = "ppc64el"
108 if 'mac' not in params:
109 mac = self.get_remote_mac()
110 if mac is not None:
111 params['mac'] = mac
112 return params
113
114 def get_reader(self, backend, kernel_params, **extra):
115 """Render a configuration file as a unicode string.
116
117 :param backend: requesting backend
118 :param kernel_params: An instance of `KernelParameters`.
119 :param extra: Allow for other arguments. This is a safety valve;
120 parameters generated in another component (for example, see
121 `TFTPBackend.get_config_reader`) won't cause this to break.
122 """
123 # Due to the path prefix, all requested files from the client will
124 # contain that prefix. Removing the prefix from the path will return
125 # the correct path in the tftp root.
126 if 'path' in extra:
127 path = extra['path']
128 path = path.replace(self.path_prefix, '', 1)
129 target_path = backend.base.descendant(path.split('/'))
130 return FilesystemReader(target_path)
131
132 # Return empty config for PowerNV local. PowerNV fails to
133 # support the LOCALBOOT flag. Empty config will allow it
134 # to select the first device.
135 if kernel_params.purpose == 'local':
136 return BytesReader("".encode("utf-8"))
137
138 template = self.get_template(
139 kernel_params.purpose, kernel_params.arch,
140 kernel_params.subarch)
141 namespace = self.compose_template_namespace(kernel_params)
142
143 # Modify the kernel_command to inject the BOOTIF. PowerNV fails to
144 # support the IPAPPEND pxelinux flag.
145 def kernel_command(params):
146 cmd_line = compose_kernel_command_line(params)
147 if 'mac' in extra:
148 mac = extra['mac']
149 mac = format_bootif(mac)
150 return '%s BOOTIF=%s' % (cmd_line, mac)
151 return cmd_line
152
153 namespace['kernel_command'] = kernel_command
154 return BytesReader(template.substitute(namespace).encode("utf-8"))
155
156 def install_bootloader(self, destination):
157 """Does nothing. No extra boot files are required. All of the boot
158 files from PXEBootMethod will suffice."""
0159
=== renamed file 'src/provisioningserver/boot/powernv.py' => 'src/provisioningserver/boot/powernv.py.moved'
=== added file 'src/provisioningserver/boot/tests/test_powernv.py'
--- src/provisioningserver/boot/tests/test_powernv.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/boot/tests/test_powernv.py 2014-09-03 15:03:36 +0000
@@ -0,0 +1,337 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for `provisioningserver.boot.powernv`."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17import os
18import re
19
20from maastesting.factory import factory
21from maastesting.testcase import MAASTestCase
22from provisioningserver.boot import BytesReader
23from provisioningserver.boot.powernv import (
24 ARP_HTYPE,
25 format_bootif,
26 PowerNVBootMethod,
27 re_config_file,
28 )
29from provisioningserver.boot.tests.test_pxe import parse_pxe_config
30from provisioningserver.boot.tftppath import compose_image_path
31from provisioningserver.testing.config import set_tftp_root
32from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
33from provisioningserver.tftp import TFTPBackend
34from testtools.matchers import (
35 IsInstance,
36 MatchesAll,
37 MatchesRegex,
38 Not,
39 StartsWith,
40 )
41
42
43def compose_config_path(mac):
44 """Compose the TFTP path for a PowerNV PXE configuration file.
45
46 The path returned is relative to the TFTP root, as it would be
47 identified by clients on the network.
48
49 :param mac: A MAC address, in IEEE 802 hyphen-separated form,
50 corresponding to the machine for which this configuration is
51 relevant. This relates to PXELINUX's lookup protocol.
52 :return: Path for the corresponding PXE config file as exposed over
53 TFTP.
54 """
55 # Not using os.path.join: this is a TFTP path, not a native path. Yes, in
56 # practice for us they're the same. We always assume that the ARP HTYPE
57 # (hardware type) that PXELINUX sends is Ethernet.
58 return "ppc64el/pxelinux.cfg/{htype:02x}-{mac}".format(
59 htype=ARP_HTYPE.ETHERNET, mac=mac)
60
61
62def get_example_path_and_components():
63 """Return a plausible path and its components.
64
65 The path is intended to match `re_config_file`, and the components are
66 the expected groups from a match.
67 """
68 components = {"mac": factory.getRandomMACAddress("-")}
69 config_path = compose_config_path(components["mac"])
70 return config_path, components
71
72
73class TestPowerNVBootMethod(MAASTestCase):
74
75 def make_tftp_root(self):
76 """Set, and return, a temporary TFTP root directory."""
77 tftproot = self.make_dir()
78 self.useFixture(set_tftp_root(tftproot))
79 return tftproot
80
81 def test_compose_config_path_follows_maas_pxe_directory_layout(self):
82 name = factory.make_name('config')
83 self.assertEqual(
84 'ppc64el/pxelinux.cfg/%02x-%s' % (ARP_HTYPE.ETHERNET, name),
85 compose_config_path(name))
86
87 def test_compose_config_path_does_not_include_tftp_root(self):
88 tftproot = self.make_tftp_root()
89 name = factory.make_name('config')
90 self.assertThat(
91 compose_config_path(name),
92 Not(StartsWith(tftproot)))
93
94 def test_bootloader_path(self):
95 method = PowerNVBootMethod()
96 self.assertEqual('pxelinux.0', method.bootloader_path)
97
98 def test_bootloader_path_does_not_include_tftp_root(self):
99 tftproot = self.make_tftp_root()
100 method = PowerNVBootMethod()
101 self.assertThat(
102 method.bootloader_path,
103 Not(StartsWith(tftproot)))
104
105 def test_name(self):
106 method = PowerNVBootMethod()
107 self.assertEqual('powernv', method.name)
108
109 def test_template_subdir(self):
110 method = PowerNVBootMethod()
111 self.assertEqual('pxe', method.template_subdir)
112
113 def test_arch_octet(self):
114 method = PowerNVBootMethod()
115 self.assertEqual('00:0E', method.arch_octet)
116
117 def test_path_prefix(self):
118 method = PowerNVBootMethod()
119 self.assertEqual('ppc64el/', method.path_prefix)
120
121
122class TestPowerNVBootMethodMatchPath(MAASTestCase):
123 """Tests for
124 `provisioningserver.boot.powernv.PowerNVBootMethod.match_path`.
125 """
126
127 def test_match_path_pxe_config_with_mac(self):
128 method = PowerNVBootMethod()
129 config_path, expected = get_example_path_and_components()
130 params = method.match_path(None, config_path)
131 expected['arch'] = 'ppc64el'
132 self.assertEqual(expected, params)
133
134 def test_match_path_pxe_config_without_mac(self):
135 method = PowerNVBootMethod()
136 fake_mac = factory.getRandomMACAddress()
137 self.patch(method, 'get_remote_mac').return_value = fake_mac
138 config_path = 'ppc64el/pxelinux.cfg/default'
139 params = method.match_path(None, config_path)
140 expected = {
141 'arch': 'ppc64el',
142 'mac': fake_mac,
143 }
144 self.assertEqual(expected, params)
145
146 def test_match_path_pxe_prefix_request(self):
147 method = PowerNVBootMethod()
148 fake_mac = factory.getRandomMACAddress()
149 self.patch(method, 'get_remote_mac').return_value = fake_mac
150 file_path = 'ppc64el/file'
151 params = method.match_path(None, file_path)
152 expected = {
153 'arch': 'ppc64el',
154 'mac': fake_mac,
155 'path': file_path,
156 }
157 self.assertEqual(expected, params)
158
159
160class TestPowerNVBootMethodRenderConfig(MAASTestCase):
161 """Tests for
162 `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`
163 """
164
165 def test_get_reader_install(self):
166 # Given the right configuration options, the PXE configuration is
167 # correctly rendered.
168 method = PowerNVBootMethod()
169 params = make_kernel_parameters(self, purpose="install")
170 output = method.get_reader(backend=None, kernel_params=params)
171 # The output is a BytesReader.
172 self.assertThat(output, IsInstance(BytesReader))
173 output = output.read(10000)
174 # The template has rendered without error. PXELINUX configurations
175 # typically start with a DEFAULT line.
176 self.assertThat(output, StartsWith("DEFAULT "))
177 # The PXE parameters are all set according to the options.
178 image_dir = compose_image_path(
179 arch=params.arch, subarch=params.subarch,
180 release=params.release, label=params.label)
181 self.assertThat(
182 output, MatchesAll(
183 MatchesRegex(
184 r'.*^\s+KERNEL %s/di-kernel$' % re.escape(image_dir),
185 re.MULTILINE | re.DOTALL),
186 MatchesRegex(
187 r'.*^\s+INITRD %s/di-initrd$' % re.escape(image_dir),
188 re.MULTILINE | re.DOTALL),
189 MatchesRegex(
190 r'.*^\s+APPEND .+?$',
191 re.MULTILINE | re.DOTALL)))
192
193 def test_get_reader_with_extra_arguments_does_not_affect_output(self):
194 # get_reader() allows any keyword arguments as a safety valve.
195 method = PowerNVBootMethod()
196 options = {
197 "backend": None,
198 "kernel_params": make_kernel_parameters(self, purpose="install"),
199 }
200 # Capture the output before sprinking in some random options.
201 output_before = method.get_reader(**options).read(10000)
202 # Sprinkle some magic in.
203 options.update(
204 (factory.make_name("name"), factory.make_name("value"))
205 for _ in range(10))
206 # Capture the output after sprinking in some random options.
207 output_after = method.get_reader(**options).read(10000)
208 # The generated template is the same.
209 self.assertEqual(output_before, output_after)
210
211 def test_get_reader_with_local_purpose(self):
212 # If purpose is "local", output should be empty string.
213 method = PowerNVBootMethod()
214 options = {
215 "backend": None,
216 "kernel_params": make_kernel_parameters(purpose="local"),
217 }
218 output = method.get_reader(**options).read(10000)
219 self.assertIn("", output)
220
221 def test_get_reader_appends_bootif(self):
222 method = PowerNVBootMethod()
223 fake_mac = factory.getRandomMACAddress()
224 params = make_kernel_parameters(self, purpose="install")
225 output = method.get_reader(
226 backend=None, kernel_params=params, arch='ppc64el', mac=fake_mac)
227 output = output.read(10000)
228 config = parse_pxe_config(output)
229 expected = 'BOOTIF=%s' % format_bootif(fake_mac)
230 self.assertIn(expected, config['execute']['APPEND'])
231
232
233class TestPowerNVBootMethodPathPrefix(MAASTestCase):
234 """Tests for
235 `provisioningserver.boot.powernv.PowerNVBootMethod.get_reader`.
236 """
237
238 def test_get_reader_path_prefix(self):
239 data = factory.getRandomString().encode("ascii")
240 temp_file = self.make_file(name="example", contents=data)
241 temp_dir = os.path.dirname(temp_file)
242 backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
243 method = PowerNVBootMethod()
244 options = {
245 'backend': backend,
246 'kernel_params': make_kernel_parameters(),
247 'path': 'ppc64el/example',
248 }
249 reader = method.get_reader(**options)
250 self.addCleanup(reader.finish)
251 self.assertEqual(len(data), reader.size)
252 self.assertEqual(data, reader.read(len(data)))
253 self.assertEqual(b"", reader.read(1))
254
255 def test_get_reader_path_prefix_only_removes_first_occurrence(self):
256 data = factory.getRandomString().encode("ascii")
257 temp_dir = self.make_dir()
258 temp_subdir = os.path.join(temp_dir, 'ppc64el')
259 os.mkdir(temp_subdir)
260 factory.make_file(temp_subdir, "example", data)
261 backend = TFTPBackend(temp_dir, "http://nowhere.example.com/")
262 method = PowerNVBootMethod()
263 options = {
264 'backend': backend,
265 'kernel_params': make_kernel_parameters(),
266 'path': 'ppc64el/ppc64el/example',
267 }
268 reader = method.get_reader(**options)
269 self.addCleanup(reader.finish)
270 self.assertEqual(len(data), reader.size)
271 self.assertEqual(data, reader.read(len(data)))
272 self.assertEqual(b"", reader.read(1))
273
274
275class TestPowerNVBootMethodRegex(MAASTestCase):
276 """Tests for
277 `provisioningserver.boot.powernv.PowerNVBootMethod.re_config_file`.
278 """
279
280 def test_re_config_file_is_compatible_with_config_path_generator(self):
281 # The regular expression for extracting components of the file path is
282 # compatible with the PXE config path generator.
283 for iteration in range(10):
284 config_path, args = get_example_path_and_components()
285 match = re_config_file.match(config_path)
286 self.assertIsNotNone(match, config_path)
287 self.assertEqual(args, match.groupdict())
288
289 def test_re_config_file_with_leading_slash(self):
290 # The regular expression for extracting components of the file path
291 # doesn't care if there's a leading forward slash; the TFTP server is
292 # easy on this point, so it makes sense to be also.
293 config_path, args = get_example_path_and_components()
294 # Ensure there's a leading slash.
295 config_path = "/" + config_path.lstrip("/")
296 match = re_config_file.match(config_path)
297 self.assertIsNotNone(match, config_path)
298 self.assertEqual(args, match.groupdict())
299
300 def test_re_config_file_without_leading_slash(self):
301 # The regular expression for extracting components of the file path
302 # doesn't care if there's no leading forward slash; the TFTP server is
303 # easy on this point, so it makes sense to be also.
304 config_path, args = get_example_path_and_components()
305 # Ensure there's no leading slash.
306 config_path = config_path.lstrip("/")
307 match = re_config_file.match(config_path)
308 self.assertIsNotNone(match, config_path)
309 self.assertEqual(args, match.groupdict())
310
311 def test_re_config_file_matches_classic_pxelinux_cfg(self):
312 # The default config path is simply "pxelinux.cfg" (without
313 # leading slash). The regex matches this.
314 mac = 'aa-bb-cc-dd-ee-ff'
315 match = re_config_file.match('ppc64el/pxelinux.cfg/01-%s' % mac)
316 self.assertIsNotNone(match)
317 self.assertEqual({'mac': mac}, match.groupdict())
318
319 def test_re_config_file_matches_pxelinux_cfg_with_leading_slash(self):
320 mac = 'aa-bb-cc-dd-ee-ff'
321 match = re_config_file.match('/ppc64el/pxelinux.cfg/01-%s' % mac)
322 self.assertIsNotNone(match)
323 self.assertEqual({'mac': mac}, match.groupdict())
324
325 def test_re_config_file_does_not_match_non_config_file(self):
326 self.assertIsNone(re_config_file.match('ppc64el/pxelinux.cfg/kernel'))
327
328 def test_re_config_file_does_not_match_file_in_root(self):
329 self.assertIsNone(re_config_file.match('01-aa-bb-cc-dd-ee-ff'))
330
331 def test_re_config_file_does_not_match_file_not_in_pxelinux_cfg(self):
332 self.assertIsNone(re_config_file.match('foo/01-aa-bb-cc-dd-ee-ff'))
333
334 def test_re_config_file_with_default(self):
335 match = re_config_file.match('ppc64el/pxelinux.cfg/default')
336 self.assertIsNotNone(match)
337 self.assertEqual({'mac': None}, match.groupdict())
0338
=== renamed file 'src/provisioningserver/boot/tests/test_powernv.py' => 'src/provisioningserver/boot/tests/test_powernv.py.moved'
=== added directory 'src/provisioningserver/drivers'
=== renamed directory 'src/provisioningserver/drivers' => 'src/provisioningserver/drivers.moved'
=== added file 'src/provisioningserver/drivers/__init__.py'
=== added directory 'src/provisioningserver/drivers/hardware'
=== added file 'src/provisioningserver/drivers/hardware/__init__.py'
=== added file 'src/provisioningserver/drivers/hardware/mscm.py'
--- src/provisioningserver/drivers/hardware/mscm.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/drivers/hardware/mscm.py 2014-09-03 15:03:36 +0000
@@ -0,0 +1,187 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Support for managing nodes via the Moonshot HP iLO Chassis Manager CLI.
5
6This module provides support for interacting with HP Moonshot iLO Chassis
7Management (MSCM) CLI via SSH, and for using that support to allow MAAS to
8manage systems via iLO.
9"""
10
11from __future__ import (
12 absolute_import,
13 print_function,
14 unicode_literals,
15 )
16str = None
17
18__metaclass__ = type
19__all__ = [
20 'power_control_mscm',
21 'probe_and_enlist_mscm',
22]
23
24import re
25
26from paramiko import (
27 AutoAddPolicy,
28 SSHClient,
29 )
30import provisioningserver.custom_hardware.utils as utils
31
32
33cartridge_mapping = {
34 'ProLiant Moonshot Cartridge': 'amd64/generic',
35 'ProLiant m300 Server Cartridge': 'amd64/generic',
36 'ProLiant m350 Server Cartridge': 'amd64/generic',
37 'ProLiant m400 Server Cartridge': 'arm64/xgene-uboot',
38 'ProLiant m500 Server Cartridge': 'amd64/generic',
39 'ProLiant m710 Server Cartridge': 'amd64/generic',
40 'ProLiant m800 Server Cartridge': 'armhf/keystone',
41 'Default': 'arm64/generic',
42}
43
44
45class MSCM_CLI_API(object):
46 """An API for interacting with the Moonshot iLO CM CLI."""
47
48 def __init__(self, host, username, password):
49 """MSCM_CLI_API Constructor."""
50 self.host = host
51 self.username = username
52 self.password = password
53 self._ssh = SSHClient()
54 self._ssh.set_missing_host_key_policy(AutoAddPolicy())
55
56 def _run_cli_command(self, command):
57 """Run a single command and return unparsed text from stdout."""
58 self._ssh.connect(
59 self.host, username=self.username, password=self.password)
60 try:
61 _, stdout, _ = self._ssh.exec_command(command)
62 output = stdout.read()
63 finally:
64 self._ssh.close()
65
66 return output
67
68 def discover_nodes(self):
69 """Discover all available nodes.
70
71 Example of stdout from running "show node list":
72
73 'show node list\r\r\nSlot ID Proc Manufacturer
74 Architecture Memory Power Health\r\n----
75 ----- ---------------------- --------------------
76 ------ ----- ------\r\n 01 c1n1 Intel Corporation
77 x86 Architecture 32 GB On OK \r\n 02 c2n1
78 N/A No Asset Information \r\n\r\n'
79
80 The regex 'c\d+n\d' is finding the node_id's c1-45n1-8
81 """
82 node_list = self._run_cli_command("show node list")
83 return re.findall(r'c\d+n\d', node_list)
84
85 def get_node_macaddr(self, node_id):
86 """Get node MAC address(es).
87
88 Example of stdout from running "show node macaddr <node_id>":
89
90 'show node macaddr c1n1\r\r\nSlot ID NIC 1 (Switch A)
91 NIC 2 (Switch B) NIC 3 (Switch A) NIC 4 (Switch B)\r\n
92 ---- ----- ----------------- ----------------- -----------------
93 -----------------\r\n 1 c1n1 a0:1d:48:b5:04:34 a0:1d:48:b5:04:35
94 a0:1d:48:b5:04:36 a0:1d:48:b5:04:37\r\n\r\n\r\n'
95
96 The regex '[\:]'.join(['[0-9A-F]{1,2}'] * 6) is finding
97 the MAC Addresses for the given node_id.
98 """
99 macs = self._run_cli_command("show node macaddr %s" % node_id)
100 return re.findall(r':'.join(['[0-9a-f]{2}'] * 6), macs)
101
102 def get_node_arch(self, node_id):
103 """Get node architecture.
104
105 Example of stdout from running "show node info <node_id>":
106
107 'show node info c1n1\r\r\n\r\nCartridge #1 \r\n Type: Compute\r\n
108 Manufacturer: HP\r\n Product Name: ProLiant m500 Server Cartridge\r\n'
109
110 Parsing this retrieves 'ProLiant m500 Server Cartridge'
111 """
112 node_detail = self._run_cli_command("show node info %s" % node_id)
113 cartridge = node_detail.split('Product Name: ')[1].splitlines()[0]
114 if cartridge in cartridge_mapping:
115 return cartridge_mapping[cartridge]
116 else:
117 return cartridge_mapping['Default']
118
119 def get_node_power_status(self, node_id):
120 """Get power state of node (on/off).
121
122 Example of stdout from running "show node power <node_id>":
123
124 'show node power c1n1\r\r\n\r\nCartridge #1\r\n Node #1\r\n
125 Power State: On\r\n'
126
127 Parsing this retrieves 'On'
128 """
129 power_state = self._run_cli_command("show node power %s" % node_id)
130 return power_state.split('Power State: ')[1].splitlines()[0]
131
132 def power_node_on(self, node_id):
133 """Power node on."""
134 return self._run_cli_command("set node power on %s" % node_id)
135
136 def power_node_off(self, node_id):
137 """Power node off."""
138 return self._run_cli_command("set node power off force %s" % node_id)
139
140 def configure_node_boot_m2(self, node_id):
141 """Configure HDD boot for node."""
142 return self._run_cli_command("set node boot M.2 %s" % node_id)
143
144 def configure_node_bootonce_pxe(self, node_id):
145 """Configure PXE boot for node once."""
146 return self._run_cli_command("set node bootonce pxe %s" % node_id)
147
148
149def power_control_mscm(host, username, password, node_id, power_change):
150 """Handle calls from the power template for nodes with a power type
151 of 'mscm'.
152 """
153 mscm = MSCM_CLI_API(host, username, password)
154 power_status = mscm.get_node_power_status(node_id)
155
156 if power_change == 'off':
157 mscm.power_node_off(node_id)
158 return
159
160 if power_change != 'on':
161 raise AssertionError('Unexpected maas power mode.')
162
163 if power_status == 'On':
164 mscm.power_node_off(node_id)
165
166 mscm.configure_node_bootonce_pxe(node_id)
167 mscm.power_node_on(node_id)
168
169
170def probe_and_enlist_mscm(host, username, password):
171 """ Extracts all of nodes from mscm, sets all of them to boot via HDD by,
172 default, sets them to bootonce via PXE, and then enlists them into MAAS.
173 """
174 mscm = MSCM_CLI_API(host, username, password)
175 nodes = mscm.discover_nodes()
176 for node_id in nodes:
177 # Set default boot to HDD
178 mscm.configure_node_boot_m2(node_id)
179 params = {
180 'power_address': host,
181 'power_user': username,
182 'power_pass': password,
183 'node_id': node_id,
184 }
185 arch = mscm.get_node_arch(node_id)
186 macs = mscm.get_node_macaddr(node_id)
187 utils.create_node(macs, arch, 'mscm', params)
0188
=== added directory 'src/provisioningserver/drivers/hardware/tests'
=== added file 'src/provisioningserver/drivers/hardware/tests/test_mscm.py'
--- src/provisioningserver/drivers/hardware/tests/test_mscm.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/drivers/hardware/tests/test_mscm.py 2014-09-03 15:03:36 +0000
@@ -0,0 +1,259 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for ``provisioningserver.drivers.hardware.mscm``."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17from random import randint
18import re
19from StringIO import StringIO
20
21from maastesting.factory import factory
22from maastesting.matchers import MockCalledOnceWith
23from maastesting.testcase import MAASTestCase
24from mock import Mock
25from provisioningserver.drivers.hardware.mscm import (
26 cartridge_mapping,
27 MSCM_CLI_API,
28 power_control_mscm,
29 probe_and_enlist_mscm,
30 )
31import provisioningserver.custom_hardware.utils as utils
32
33
34def make_mscm_api():
35 """Make a MSCM_CLI_API object with randomized parameters."""
36 host = factory.make_hostname('mscm')
37 username = factory.make_name('user')
38 password = factory.make_name('password')
39 return MSCM_CLI_API(host, username, password)
40
41
42def make_node_id():
43 """Make a node_id."""
44 return 'c%sn%s' % (randint(1, 45), randint(1, 8))
45
46
47def make_show_node_list(length=10):
48 """Make a fake return value for discover_nodes."""
49 return re.findall(r'c\d+n\d', ''.join(make_node_id()
50 for _ in xrange(length)))
51
52
53def make_show_node_macaddr(length=10):
54 """Make a fake return value for get_node_macaddr."""
55 return ''.join((factory.getRandomMACAddress() + ' ')
56 for _ in xrange(length))
57
58
59class TestRunCliCommand(MAASTestCase):
60 """Tests for ``MSCM_CLI_API.run_cli_command``."""
61
62 def test_returns_output(self):
63 api = make_mscm_api()
64 ssh_mock = self.patch(api, '_ssh')
65 expected = factory.make_name('output')
66 stdout = StringIO(expected)
67 streams = factory.make_streams(stdout=stdout)
68 ssh_mock.exec_command = Mock(return_value=streams)
69 output = api._run_cli_command(factory.make_name('command'))
70 self.assertEqual(expected, output)
71
72 def test_connects_and_closes_ssh_client(self):
73 api = make_mscm_api()
74 ssh_mock = self.patch(api, '_ssh')
75 ssh_mock.exec_command = Mock(return_value=factory.make_streams())
76 api._run_cli_command(factory.make_name('command'))
77 self.assertThat(
78 ssh_mock.connect,
79 MockCalledOnceWith(
80 api.host, username=api.username, password=api.password))
81 self.assertThat(ssh_mock.close, MockCalledOnceWith())
82
83 def test_closes_when_exception_raised(self):
84 api = make_mscm_api()
85 ssh_mock = self.patch(api, '_ssh')
86
87 def fail():
88 raise Exception('fail')
89
90 ssh_mock.exec_command = Mock(side_effect=fail)
91 command = factory.make_name('command')
92 self.assertRaises(Exception, api._run_cli_command, command)
93 self.assertThat(ssh_mock.close, MockCalledOnceWith())
94
95
96class TestDiscoverNodes(MAASTestCase):
97 """Tests for ``MSCM_CLI_API.discover_nodes``."""
98
99 def test_discover_nodes(self):
100 api = make_mscm_api()
101 ssh_mock = self.patch(api, '_ssh')
102 expected = make_show_node_list()
103 stdout = StringIO(expected)
104 streams = factory.make_streams(stdout=stdout)
105 ssh_mock.exec_command = Mock(return_value=streams)
106 output = api.discover_nodes()
107 self.assertEqual(expected, output)
108
109
110class TestNodeMACAddress(MAASTestCase):
111 """Tests for ``MSCM_CLI_API.get_node_macaddr``."""
112
113 def test_get_node_macaddr(self):
114 api = make_mscm_api()
115 expected = make_show_node_macaddr()
116 cli_mock = self.patch(api, '_run_cli_command')
117 cli_mock.return_value = expected
118 node_id = make_node_id()
119 output = api.get_node_macaddr(node_id)
120 self.assertEqual(re.findall(r':'.join(['[0-9a-f]{2}'] * 6),
121 expected), output)
122
123
124class TestNodeArch(MAASTestCase):
125 """Tests for ``MSCM_CLI_API.get_node_arch``."""
126
127 def test_get_node_arch(self):
128 api = make_mscm_api()
129 expected = '\r\n Product Name: ProLiant Moonshot Cartridge\r\n'
130 cli_mock = self.patch(api, '_run_cli_command')
131 cli_mock.return_value = expected
132 node_id = make_node_id()
133 output = api.get_node_arch(node_id)
134 key = expected.split('Product Name: ')[1].splitlines()[0]
135 self.assertEqual(cartridge_mapping[key], output)
136
137
138class TestGetNodePowerStatus(MAASTestCase):
139 """Tests for ``MSCM_CLI_API.get_node_power_status``."""
140
141 def test_get_node_power_status(self):
142 api = make_mscm_api()
143 expected = '\r\n Node #1\r\n Power State: On\r\n'
144 cli_mock = self.patch(api, '_run_cli_command')
145 cli_mock.return_value = expected
146 node_id = make_node_id()
147 output = api.get_node_power_status(node_id)
148 self.assertEqual(expected.split('Power State: ')[1].splitlines()[0],
149 output)
150
151
152class TestPowerAndConfigureNode(MAASTestCase):
153 """Tests for ``MSCM_CLI_API.configure_node_bootonce_pxe,
154 MSCM_CLI_API.power_node_on, and MSCM_CLI_API.power_node_off``.
155 """
156
157 scenarios = [
158 ('power_node_on()',
159 dict(method='power_node_on')),
160 ('power_node_off()',
161 dict(method='power_node_off')),
162 ('configure_node_bootonce_pxe()',
163 dict(method='configure_node_bootonce_pxe')),
164 ]
165
166 def test_returns_expected_outout(self):
167 api = make_mscm_api()
168 ssh_mock = self.patch(api, '_ssh')
169 expected = factory.make_name('output')
170 stdout = StringIO(expected)
171 streams = factory.make_streams(stdout=stdout)
172 ssh_mock.exec_command = Mock(return_value=streams)
173 output = getattr(api, self.method)(make_node_id())
174 self.assertEqual(expected, output)
175
176
177class TestPowerControlMSCM(MAASTestCase):
178 """Tests for ``power_control_ucsm``."""
179
180 def test_power_control_mscm_on_on(self):
181 # power_change and power_status are both 'on'
182 host = factory.make_hostname('mscm')
183 username = factory.make_name('user')
184 password = factory.make_name('password')
185 node_id = make_node_id()
186 bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
187 power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
188 power_status_mock.return_value = 'On'
189 power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
190 power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
191
192 power_control_mscm(host, username, password, node_id,
193 power_change='on')
194 self.assertThat(bootonce_mock, MockCalledOnceWith(node_id))
195 self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id))
196 self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id))
197
198 def test_power_control_mscm_on_off(self):
199 # power_change is 'on' and power_status is 'off'
200 host = factory.make_hostname('mscm')
201 username = factory.make_name('user')
202 password = factory.make_name('password')
203 node_id = make_node_id()
204 bootonce_mock = self.patch(MSCM_CLI_API, 'configure_node_bootonce_pxe')
205 power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
206 power_status_mock.return_value = 'Off'
207 power_node_on_mock = self.patch(MSCM_CLI_API, 'power_node_on')
208
209 power_control_mscm(host, username, password, node_id,
210 power_change='on')
211 self.assertThat(bootonce_mock, MockCalledOnceWith(node_id))
212 self.assertThat(power_node_on_mock, MockCalledOnceWith(node_id))
213
214 def test_power_control_mscm_off_on(self):
215 # power_change is 'off' and power_status is 'on'
216 host = factory.make_hostname('mscm')
217 username = factory.make_name('user')
218 password = factory.make_name('password')
219 node_id = make_node_id()
220 power_status_mock = self.patch(MSCM_CLI_API, 'get_node_power_status')
221 power_status_mock.return_value = 'On'
222 power_node_off_mock = self.patch(MSCM_CLI_API, 'power_node_off')
223
224 power_control_mscm(host, username, password, node_id,
225 power_change='off')
226 self.assertThat(power_node_off_mock, MockCalledOnceWith(node_id))
227
228
229class TestProbeAndEnlistMSCM(MAASTestCase):
230 """Tests for ``probe_and_enlist_mscm``."""
231
232 def test_probe_and_enlist(self):
233 host = factory.make_hostname('mscm')
234 username = factory.make_name('user')
235 password = factory.make_name('password')
236 node_id = make_node_id()
237 macs = make_show_node_macaddr(4)
238 arch = 'arm64/xgene-uboot'
239 discover_nodes_mock = self.patch(MSCM_CLI_API, 'discover_nodes')
240 discover_nodes_mock.return_value = [node_id]
241 boot_m2_mock = self.patch(MSCM_CLI_API, 'configure_node_boot_m2')
242 node_arch_mock = self.patch(MSCM_CLI_API, 'get_node_arch')
243 node_arch_mock.return_value = arch
244 node_macs_mock = self.patch(MSCM_CLI_API, 'get_node_macaddr')
245 node_macs_mock.return_value = macs
246 create_node_mock = self.patch(utils, 'create_node')
247 probe_and_enlist_mscm(host, username, password)
248 self.assertThat(discover_nodes_mock, MockCalledOnceWith())
249 self.assertThat(boot_m2_mock, MockCalledOnceWith(node_id))
250 self.assertThat(node_arch_mock, MockCalledOnceWith(node_id))
251 self.assertThat(node_macs_mock, MockCalledOnceWith(node_id))
252 params = {
253 'power_address': host,
254 'power_user': username,
255 'power_pass': password,
256 'node_id': node_id,
257 }
258 self.assertThat(create_node_mock,
259 MockCalledOnceWith(macs, arch, 'mscm', params))

Subscribers

People subscribed via source and target branches

to all changes: