Merge "Add ssh power manager."
This commit is contained in:
commit
89ea41ef51
@ -256,3 +256,7 @@ class ExclusiveLockRequired(NotAuthorized):
|
|||||||
|
|
||||||
class IPMIFailure(IronicException):
|
class IPMIFailure(IronicException):
|
||||||
message = _("IPMI command failed: %(cmd)s.")
|
message = _("IPMI command failed: %(cmd)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class SSHConnectFailed(IronicException):
|
||||||
|
message = _("Failed to establish SSH connection to host %(host)s.")
|
||||||
|
@ -24,6 +24,7 @@ import contextlib
|
|||||||
import errno
|
import errno
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
import paramiko
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@ -195,42 +196,30 @@ def trycmd(*args, **kwargs):
|
|||||||
return out, err
|
return out, err
|
||||||
|
|
||||||
|
|
||||||
def ssh_execute(ssh, cmd, process_input=None,
|
def ssh_connect(connection):
|
||||||
addl_env=None, check_exit_code=True):
|
"""Method to connect to a remote system using ssh protocol.
|
||||||
LOG.debug(_('Running cmd (SSH): %s'), cmd)
|
|
||||||
if addl_env:
|
|
||||||
raise exception.IronicException(_(
|
|
||||||
'Environment not supported over SSH'))
|
|
||||||
|
|
||||||
if process_input:
|
:param connection: a dict of connection parameters.
|
||||||
# This is (probably) fixable if we need it...
|
:returns: paramiko.SSHClient -- an active ssh connection.
|
||||||
msg = _('process_input not supported over SSH')
|
:raises: SSHConnectFailed
|
||||||
raise exception.IronicException(msg)
|
|
||||||
|
|
||||||
stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd)
|
"""
|
||||||
channel = stdout_stream.channel
|
try:
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect(connection.get('host'),
|
||||||
|
username=connection.get('username'),
|
||||||
|
password=connection.get('password', None),
|
||||||
|
port=connection.get('port', 22),
|
||||||
|
key_filename=connection.get('key_filename', None),
|
||||||
|
timeout=connection.get('timeout', 10))
|
||||||
|
|
||||||
#stdin.write('process_input would go here')
|
# send TCP keepalive packets every 20 seconds
|
||||||
#stdin.flush()
|
ssh.get_transport().set_keepalive(20)
|
||||||
|
except Exception:
|
||||||
|
raise exception.SSHConnectFailed(host=connection.get('host'))
|
||||||
|
|
||||||
# NOTE(justinsb): This seems suspicious...
|
return ssh
|
||||||
# ...other SSH clients have buffering issues with this approach
|
|
||||||
stdout = stdout_stream.read()
|
|
||||||
stderr = stderr_stream.read()
|
|
||||||
stdin_stream.close()
|
|
||||||
|
|
||||||
exit_status = channel.recv_exit_status()
|
|
||||||
|
|
||||||
# exit_status == -1 if no exit code was returned
|
|
||||||
if exit_status != -1:
|
|
||||||
LOG.debug(_('Result was %s') % exit_status)
|
|
||||||
if check_exit_code and exit_status != 0:
|
|
||||||
raise exception.ProcessExecutionError(exit_code=exit_status,
|
|
||||||
stdout=stdout,
|
|
||||||
stderr=stderr,
|
|
||||||
cmd=cmd)
|
|
||||||
|
|
||||||
return (stdout, stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_uid(topic, size=8):
|
def generate_uid(topic, size=8):
|
||||||
|
382
ironic/drivers/ssh.py
Normal file
382
ironic/drivers/ssh.py
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Ironic SSH power manager.
|
||||||
|
|
||||||
|
Provides basic power control of virtual machines via SSH.
|
||||||
|
|
||||||
|
For use in dev and test environments.
|
||||||
|
|
||||||
|
Currently supported environments are:
|
||||||
|
Virtual Box (vbox)
|
||||||
|
Virsh (virsh)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic.common import states
|
||||||
|
from ironic.common import utils
|
||||||
|
from ironic.drivers import base
|
||||||
|
from ironic.manager import task_manager
|
||||||
|
from ironic.openstack.common import jsonutils as json
|
||||||
|
from ironic.openstack.common import log as logging
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
COMMAND_SETS = {
|
||||||
|
'vbox': {
|
||||||
|
'base_cmd': '/usr/bin/VBoxManage',
|
||||||
|
'start_cmd': 'startvm {_NodeName_}',
|
||||||
|
'stop_cmd': 'controlvm {_NodeName_} poweroff',
|
||||||
|
'reboot_cmd': 'controlvm {_NodeName_} reset',
|
||||||
|
'list_all': "list vms|awk -F'\"' '{print $2}'",
|
||||||
|
'list_running': 'list runningvms',
|
||||||
|
'get_node_macs': ("showvminfo --machinereadable {_NodeName_} | "
|
||||||
|
"grep "
|
||||||
|
'"macaddress" | awk -F '
|
||||||
|
"'"
|
||||||
|
'"'
|
||||||
|
"' '{print $2}'")
|
||||||
|
},
|
||||||
|
"virsh": {
|
||||||
|
'base_cmd': '/usr/bin/virsh',
|
||||||
|
'start_cmd': 'start {_NodeName_}',
|
||||||
|
'stop_cmd': 'destroy {_NodeName_}',
|
||||||
|
'reboot_cmd': 'reset {_NodeName_}',
|
||||||
|
'list_all': "list --all | tail -n +2 | awk -F\" \" '{print $2}'",
|
||||||
|
'list_running':
|
||||||
|
"list --all|grep running|awk -v qc='\"' -F\" \" '{print qc$2qc}'",
|
||||||
|
'get_node_macs': ("dumpxml {_NodeName_} | grep "
|
||||||
|
'"mac address" | awk -F'
|
||||||
|
'"'
|
||||||
|
"'"
|
||||||
|
'" '
|
||||||
|
"'{print $2}' | tr -d ':'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_mac(mac):
|
||||||
|
return mac.translate(None, '-:').lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _exec_ssh_command(ssh_obj, command):
|
||||||
|
"""Execute a SSH command on the host."""
|
||||||
|
|
||||||
|
LOG.debug(_('Running cmd (SSH): %s'), command)
|
||||||
|
|
||||||
|
stdin_stream, stdout_stream, stderr_stream = ssh_obj.exec_command(command)
|
||||||
|
channel = stdout_stream.channel
|
||||||
|
|
||||||
|
# NOTE(justinsb): This seems suspicious...
|
||||||
|
# ...other SSH clients have buffering issues with this approach
|
||||||
|
stdout = stdout_stream.read()
|
||||||
|
stderr = stderr_stream.read()
|
||||||
|
stdin_stream.close()
|
||||||
|
|
||||||
|
exit_status = channel.recv_exit_status()
|
||||||
|
|
||||||
|
# exit_status == -1 if no exit code was returned
|
||||||
|
if exit_status != -1:
|
||||||
|
LOG.debug(_('Result was %s') % exit_status)
|
||||||
|
if exit_status != 0:
|
||||||
|
raise exception.ProcessExecutionError(exit_code=exit_status,
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
|
cmd=command)
|
||||||
|
|
||||||
|
return (stdout, stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_control_info(node):
|
||||||
|
info = json.loads(node.get('control_info', ''))
|
||||||
|
host = info.get('ssh_host', None)
|
||||||
|
username = info.get('ssh_user', None)
|
||||||
|
password = info.get('ssh_pass', None)
|
||||||
|
port = info.get('ssh_port', 22)
|
||||||
|
key_filename = info.get('ssh_key', None)
|
||||||
|
virt_type = info.get('virt_type', None)
|
||||||
|
|
||||||
|
res = {
|
||||||
|
'host': host,
|
||||||
|
'username': username,
|
||||||
|
'port': port,
|
||||||
|
'virt_type': virt_type,
|
||||||
|
'uuid': node.get('uuid')
|
||||||
|
}
|
||||||
|
|
||||||
|
if not virt_type:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"SSHPowerDriver requires virt_type be set."))
|
||||||
|
|
||||||
|
cmd_set = COMMAND_SETS.get(virt_type, None)
|
||||||
|
if not cmd_set:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"SSHPowerDriver unknown virt_type (%s).") % cmd_set)
|
||||||
|
res['cmd_set'] = cmd_set
|
||||||
|
|
||||||
|
if not host or not username:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"SSHPowerDriver requires both ssh_host and ssh_user be set."))
|
||||||
|
if password:
|
||||||
|
res['password'] = password
|
||||||
|
else:
|
||||||
|
if not key_filename:
|
||||||
|
raise exception.InvalidParameterValue(_(
|
||||||
|
"SSHPowerDriver requires either ssh_pass or ssh_key be set."))
|
||||||
|
if not os.path.isfile(key_filename):
|
||||||
|
raise exception.FileNotFound(file_path=key_filename)
|
||||||
|
res['key_filename'] = key_filename
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def _get_power_status(ssh_obj, c_info):
|
||||||
|
"""Returns a node's current power state."""
|
||||||
|
|
||||||
|
power_state = None
|
||||||
|
cmd_to_exec = c_info['cmd_set']['list_running']
|
||||||
|
running_list = _exec_ssh_command(ssh_obj, cmd_to_exec)
|
||||||
|
# Command should return a list of running vms. If the current node is
|
||||||
|
# not listed then we can assume it is not powered on.
|
||||||
|
node_name = _get_hosts_name_for_node(ssh_obj, c_info)
|
||||||
|
if node_name:
|
||||||
|
for node in running_list:
|
||||||
|
if node_name in node:
|
||||||
|
power_state = states.POWER_ON
|
||||||
|
break
|
||||||
|
if not power_state:
|
||||||
|
power_state = states.POWER_OFF
|
||||||
|
else:
|
||||||
|
power_state = states.ERROR
|
||||||
|
|
||||||
|
return power_state
|
||||||
|
|
||||||
|
|
||||||
|
def _get_connection(node):
|
||||||
|
return utils.ssh_connect(_parse_control_info(node))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_hosts_name_for_node(ssh_obj, c_info):
|
||||||
|
"""Get the name the host uses to reference the node."""
|
||||||
|
|
||||||
|
matched_name = None
|
||||||
|
cmd_to_exec = c_info['cmd_set']['list_all']
|
||||||
|
full_node_list = _exec_ssh_command(ssh_obj, cmd_to_exec)
|
||||||
|
# for each node check Mac Addresses
|
||||||
|
for node in full_node_list:
|
||||||
|
cmd_to_exec = c_info['cmd_set']['get_node_macs']
|
||||||
|
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', node)
|
||||||
|
hosts_node_mac_list = _exec_ssh_command(ssh_obj, cmd_to_exec)
|
||||||
|
|
||||||
|
for host_mac in hosts_node_mac_list:
|
||||||
|
for node_mac in c_info['macs']:
|
||||||
|
if _normalize_mac(host_mac) in _normalize_mac(node_mac):
|
||||||
|
matched_name = node
|
||||||
|
break
|
||||||
|
|
||||||
|
if matched_name:
|
||||||
|
break
|
||||||
|
if matched_name:
|
||||||
|
break
|
||||||
|
|
||||||
|
return matched_name
|
||||||
|
|
||||||
|
|
||||||
|
def _power_on(ssh_obj, c_info):
|
||||||
|
"""Power ON this node."""
|
||||||
|
|
||||||
|
current_pstate = _get_power_status(ssh_obj, c_info)
|
||||||
|
if current_pstate == states.POWER_ON:
|
||||||
|
_power_off(ssh_obj, c_info)
|
||||||
|
|
||||||
|
node_name = _get_hosts_name_for_node(ssh_obj, c_info)
|
||||||
|
cmd_to_power_on = c_info['cmd_set']['start_cmd']
|
||||||
|
cmd_to_power_on = cmd_to_power_on.replace('{_NodeName_}', node_name)
|
||||||
|
|
||||||
|
_exec_ssh_command(ssh_obj, cmd_to_power_on)
|
||||||
|
|
||||||
|
current_pstate = _get_power_status(ssh_obj, c_info)
|
||||||
|
if current_pstate == states.POWER_ON:
|
||||||
|
return current_pstate
|
||||||
|
else:
|
||||||
|
return states.ERROR
|
||||||
|
|
||||||
|
|
||||||
|
def _power_off(ssh_obj, c_info):
|
||||||
|
"""Power OFF this node."""
|
||||||
|
|
||||||
|
current_pstate = _get_power_status(ssh_obj, c_info)
|
||||||
|
if current_pstate == states.POWER_OFF:
|
||||||
|
return current_pstate
|
||||||
|
|
||||||
|
node_name = _get_hosts_name_for_node(ssh_obj, c_info)
|
||||||
|
cmd_to_power_off = c_info['cmd_set']['stop_cmd']
|
||||||
|
cmd_to_power_off = cmd_to_power_off.replace('{_NodeName_}', node_name)
|
||||||
|
|
||||||
|
_exec_ssh_command(ssh_obj, cmd_to_power_off)
|
||||||
|
|
||||||
|
current_pstate = _get_power_status(ssh_obj, c_info)
|
||||||
|
if current_pstate == states.POWER_OFF:
|
||||||
|
return current_pstate
|
||||||
|
else:
|
||||||
|
return states.ERROR
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nodes_mac_addresses(task, node):
|
||||||
|
"""Get all mac addresses for a node."""
|
||||||
|
interface_ports = task.dbapi.get_ports_by_node(node.get('id'))
|
||||||
|
macs = [p.address for p in interface_ports]
|
||||||
|
return macs
|
||||||
|
|
||||||
|
|
||||||
|
class SSHPowerDriver(base.ControlDriver):
|
||||||
|
"""SSH Power Driver.
|
||||||
|
|
||||||
|
This ControlDriver class provides a mechanism for controlling the power
|
||||||
|
state of virtual machines via SSH.
|
||||||
|
|
||||||
|
NOTE: This driver supports VirtualBox and Virsh commands.
|
||||||
|
NOTE: This driver does not currently support multi-node operations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def validate_driver_info(self, node):
|
||||||
|
"""Check that node['control_info'] contains the requisite fields.
|
||||||
|
|
||||||
|
:param node: Single node object.
|
||||||
|
|
||||||
|
:returns: True / False.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
_parse_control_info(node)
|
||||||
|
except exception.InvalidParameterValue:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_power_state(self, task, node):
|
||||||
|
"""Get the current power state.
|
||||||
|
|
||||||
|
Poll the host for the current power state of the node.
|
||||||
|
|
||||||
|
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
|
||||||
|
:param node: A single node.
|
||||||
|
|
||||||
|
:returns: power state. One of :class:`ironic.common.states`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
c_info = _parse_control_info(node)
|
||||||
|
c_info['macs'] = _get_nodes_mac_addresses(task, node)
|
||||||
|
ssh_obj = _get_connection(node)
|
||||||
|
return _get_power_status(ssh_obj, c_info)
|
||||||
|
|
||||||
|
@task_manager.require_exclusive_lock
|
||||||
|
def set_power_state(self, task, node, pstate):
|
||||||
|
"""Turn the power on or off.
|
||||||
|
|
||||||
|
Set the power state of a node.
|
||||||
|
|
||||||
|
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
|
||||||
|
:param node: A single node.
|
||||||
|
:param pstate: Either POWER_ON or POWER_OFF from :class:
|
||||||
|
`ironic.common.states`.
|
||||||
|
|
||||||
|
:returns NOTHING:
|
||||||
|
|
||||||
|
Can raise exception.IronicException or exception.PowerStateFailure.
|
||||||
|
|
||||||
|
"""
|
||||||
|
c_info = _parse_control_info(node)
|
||||||
|
c_info['macs'] = _get_nodes_mac_addresses(task, node)
|
||||||
|
ssh_obj = _get_connection(node)
|
||||||
|
|
||||||
|
if pstate == states.POWER_ON:
|
||||||
|
state = _power_on(ssh_obj, c_info)
|
||||||
|
elif pstate == states.POWER_OFF:
|
||||||
|
state = _power_off(ssh_obj, c_info)
|
||||||
|
else:
|
||||||
|
raise exception.IronicException(_(
|
||||||
|
"set_power_state called with invalid power state."))
|
||||||
|
|
||||||
|
if state != pstate:
|
||||||
|
raise exception.PowerStateFailure(pstate=pstate)
|
||||||
|
|
||||||
|
@task_manager.require_exclusive_lock
|
||||||
|
def reboot(self, task, node):
|
||||||
|
"""Cycles the power to a node.
|
||||||
|
|
||||||
|
Power cycles a node.
|
||||||
|
|
||||||
|
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
|
||||||
|
:param node: A single node.
|
||||||
|
|
||||||
|
:returns NOTHING:
|
||||||
|
|
||||||
|
Can raise exception.PowerStateFailure.
|
||||||
|
|
||||||
|
"""
|
||||||
|
c_info = _parse_control_info(node)
|
||||||
|
c_info['macs'] = _get_nodes_mac_addresses(task, node)
|
||||||
|
ssh_obj = _get_connection(node)
|
||||||
|
current_pstate = _get_power_status(ssh_obj, c_info)
|
||||||
|
if current_pstate == states.POWER_ON:
|
||||||
|
_power_off(ssh_obj, c_info)
|
||||||
|
|
||||||
|
state = _power_on(ssh_obj, c_info)
|
||||||
|
|
||||||
|
if state != states.POWER_ON:
|
||||||
|
raise exception.PowerStateFailure(pstate=states.POWER_ON)
|
||||||
|
|
||||||
|
def start_console(self, task, node):
|
||||||
|
"""Starts a console connection to a node.
|
||||||
|
|
||||||
|
CURRENTLY NOT SUPPORTED.
|
||||||
|
|
||||||
|
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
|
||||||
|
:param node: A single node.
|
||||||
|
|
||||||
|
Will raise raise exception.IronicException.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise exception.IronicException(_(
|
||||||
|
"start_console is not supported by SSHPowerDriver."))
|
||||||
|
|
||||||
|
def stop_console(self, task, node):
|
||||||
|
"""Stops a console connection to a node.
|
||||||
|
|
||||||
|
CURRENTLY NOT SUPPORTED.
|
||||||
|
|
||||||
|
:param task: A instance of `ironic.manager.task_manager.TaskManager`.
|
||||||
|
:param node: A single node.
|
||||||
|
|
||||||
|
Will raise raise exception.IronicException.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise exception.IronicException(_(
|
||||||
|
"stop_console is not supported by SSHPowerDriver."))
|
@ -20,13 +20,22 @@ from ironic.db.sqlalchemy import models
|
|||||||
from ironic.openstack.common import jsonutils as json
|
from ironic.openstack.common import jsonutils as json
|
||||||
|
|
||||||
|
|
||||||
_control_info = json.dumps(
|
_ipmi_control_info = json.dumps(
|
||||||
{
|
{
|
||||||
"ipmi_address": "1.2.3.4",
|
"ipmi_address": "1.2.3.4",
|
||||||
"ipmi_username": "admin",
|
"ipmi_username": "admin",
|
||||||
"ipmi_password": "fake",
|
"ipmi_password": "fake",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
_ssh_control_info = json.dumps(
|
||||||
|
{
|
||||||
|
"ssh_host": "1.2.3.4",
|
||||||
|
"ssh_user": "admin",
|
||||||
|
"ssh_pass": "fake",
|
||||||
|
"ssh_port": 22,
|
||||||
|
"virt_type": "vbox",
|
||||||
|
})
|
||||||
|
|
||||||
_deploy_info = json.dumps(
|
_deploy_info = json.dumps(
|
||||||
{
|
{
|
||||||
"image_path": "/path/to/image.qcow2",
|
"image_path": "/path/to/image.qcow2",
|
||||||
@ -53,7 +62,12 @@ def get_test_node(**kw):
|
|||||||
'8227348d-5f1d-4488-aad1-7c92b2d42504')
|
'8227348d-5f1d-4488-aad1-7c92b2d42504')
|
||||||
|
|
||||||
node.control_driver = kw.get('control_driver', 'ipmi')
|
node.control_driver = kw.get('control_driver', 'ipmi')
|
||||||
node.control_info = kw.get('control_info', _control_info)
|
node.control_info = kw.get('control_info', None)
|
||||||
|
|
||||||
|
if node.control_driver == 'ipmi' and not node.control_info:
|
||||||
|
node.control_info = _ipmi_control_info
|
||||||
|
elif node.control_driver == 'ssh' and not node.control_info:
|
||||||
|
node.control_info = _ssh_control_info
|
||||||
|
|
||||||
node.deploy_driver = kw.get('deploy_driver', 'pxe')
|
node.deploy_driver = kw.get('deploy_driver', 'pxe')
|
||||||
node.deploy_info = kw.get('deploy_info', _deploy_info)
|
node.deploy_info = kw.get('deploy_info', _deploy_info)
|
||||||
|
549
ironic/tests/drivers/test_ssh.py
Normal file
549
ironic/tests/drivers/test_ssh.py
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""Test class for Ironic SSH power driver."""
|
||||||
|
|
||||||
|
import mox
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from ironic.openstack.common import jsonutils as json
|
||||||
|
|
||||||
|
from ironic.common import exception
|
||||||
|
from ironic.common import states
|
||||||
|
from ironic.db import api as dbapi
|
||||||
|
from ironic.drivers import ssh
|
||||||
|
from ironic.manager import task_manager
|
||||||
|
from ironic.tests import base
|
||||||
|
from ironic.tests.db import utils as db_utils
|
||||||
|
from ironic.tests.manager import utils as mgr_utils
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class IronicSSHTestCase(base.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(IronicSSHTestCase, self).setUp()
|
||||||
|
(self.controller, self.deployer) = mgr_utils.get_mocked_node_manager(
|
||||||
|
control_driver="ssh")
|
||||||
|
self.node = db_utils.get_test_node(**{'control_driver': 'ssh',
|
||||||
|
'deploy_driver': 'fake'})
|
||||||
|
self.dbapi = dbapi.get_instance()
|
||||||
|
self.dbapi.create_node(self.node)
|
||||||
|
self.ifports = [db_utils.get_test_port(id=6, address='aa:bb:cc'),
|
||||||
|
db_utils.get_test_port(id=7, address='dd:ee:ff')]
|
||||||
|
self.dbapi.create_port(self.ifports[0])
|
||||||
|
self.dbapi.create_port(self.ifports[1])
|
||||||
|
self.ssh = ssh.SSHPowerDriver()
|
||||||
|
self.test_control_info_dict = {
|
||||||
|
"ssh_host": "1.2.3.4",
|
||||||
|
"ssh_user": "admin",
|
||||||
|
"ssh_pass": "fake",
|
||||||
|
"ssh_port": 22,
|
||||||
|
"virt_type": "vbox",
|
||||||
|
"ssh_key": "/not/real/file"}
|
||||||
|
|
||||||
|
def test__parse_control_info_good(self):
|
||||||
|
# make sure we get back the expected things
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
self.assertIsNotNone(info.get('host'))
|
||||||
|
self.assertIsNotNone(info.get('username'))
|
||||||
|
self.assertIsNotNone(info.get('password'))
|
||||||
|
self.assertIsNotNone(info.get('port'))
|
||||||
|
self.assertIsNotNone(info.get('virt_type'))
|
||||||
|
self.assertIsNotNone(info.get('cmd_set'))
|
||||||
|
self.assertIsNotNone(info.get('uuid'))
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__parse_control_info_missing_host(self):
|
||||||
|
# make sure error is raised when info is missing
|
||||||
|
tmp_dict = self.test_control_info_dict
|
||||||
|
del tmp_dict['ssh_host']
|
||||||
|
del tmp_dict['ssh_key']
|
||||||
|
_ssh_control_info = json.dumps(tmp_dict)
|
||||||
|
node = db_utils.get_test_node(control_info=_ssh_control_info)
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
ssh._parse_control_info,
|
||||||
|
node)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__parse_control_info_missing_user(self):
|
||||||
|
# make sure error is raised when info is missing
|
||||||
|
tmp_dict = self.test_control_info_dict
|
||||||
|
del tmp_dict['ssh_user']
|
||||||
|
del tmp_dict['ssh_key']
|
||||||
|
_ssh_control_info = json.dumps(tmp_dict)
|
||||||
|
node = db_utils.get_test_node(control_info=_ssh_control_info)
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
ssh._parse_control_info,
|
||||||
|
node)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__parse_control_info_missing_pass(self):
|
||||||
|
# make sure error is raised when info is missing
|
||||||
|
tmp_dict = self.test_control_info_dict
|
||||||
|
del tmp_dict['ssh_pass']
|
||||||
|
del tmp_dict['ssh_key']
|
||||||
|
_ssh_control_info = json.dumps(tmp_dict)
|
||||||
|
node = db_utils.get_test_node(control_info=_ssh_control_info)
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
ssh._parse_control_info,
|
||||||
|
node)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__parse_control_info_missing_virt_type(self):
|
||||||
|
# make sure error is raised when info is missing
|
||||||
|
tmp_dict = self.test_control_info_dict
|
||||||
|
del tmp_dict['virt_type']
|
||||||
|
del tmp_dict['ssh_key']
|
||||||
|
_ssh_control_info = json.dumps(tmp_dict)
|
||||||
|
node = db_utils.get_test_node(control_info=_ssh_control_info)
|
||||||
|
self.assertRaises(exception.InvalidParameterValue,
|
||||||
|
ssh._parse_control_info,
|
||||||
|
node)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__parse_control_info_missing_key(self):
|
||||||
|
# make sure error is raised when info is missing
|
||||||
|
tmp_dict = self.test_control_info_dict
|
||||||
|
del tmp_dict['ssh_pass']
|
||||||
|
_ssh_control_info = json.dumps(tmp_dict)
|
||||||
|
node = db_utils.get_test_node(control_info=_ssh_control_info)
|
||||||
|
self.assertRaises(exception.FileNotFound,
|
||||||
|
ssh._parse_control_info,
|
||||||
|
node)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__normalize_mac(self):
|
||||||
|
mac_raw = "0A:1B-2C-3D:4F"
|
||||||
|
mac_clean = ssh._normalize_mac(mac_raw)
|
||||||
|
self.assertEqual(mac_clean, "0a1b2c3d4f")
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__get_power_status_on(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
|
||||||
|
|
||||||
|
ssh._exec_ssh_command(
|
||||||
|
ssh_obj, info['cmd_set']['list_running']).AndReturn(
|
||||||
|
['"NodeName" {b43c4982-110c-4c29-9325-d5f41b053513}'])
|
||||||
|
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
pstate = ssh._get_power_status(ssh_obj, info)
|
||||||
|
self.assertEqual(pstate, states.POWER_ON)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__get_power_status_off(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
|
||||||
|
|
||||||
|
ssh._exec_ssh_command(
|
||||||
|
ssh_obj, info['cmd_set']['list_running']).AndReturn(
|
||||||
|
['"NodeName" {b43c4982-110c-4c29-9325-d5f41b053513}'])
|
||||||
|
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NotNodeName")
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
pstate = ssh._get_power_status(ssh_obj, info)
|
||||||
|
self.assertEqual(pstate, states.POWER_OFF)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__get_power_status_error(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
|
||||||
|
|
||||||
|
ssh._exec_ssh_command(
|
||||||
|
ssh_obj, info['cmd_set']['list_running']).AndReturn(
|
||||||
|
['"NodeName" {b43c4982-110c-4c29-9325-d5f41b053513}'])
|
||||||
|
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn(None)
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
pstate = ssh._get_power_status(ssh_obj, info)
|
||||||
|
self.assertEqual(pstate, states.ERROR)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__get_hosts_name_for_node_match(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
|
||||||
|
ssh._exec_ssh_command(ssh_obj, info['cmd_set']['list_all']).AndReturn(
|
||||||
|
['NodeName'])
|
||||||
|
cmd_to_exec = info['cmd_set']['get_node_macs']
|
||||||
|
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
|
||||||
|
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(
|
||||||
|
['52:54:00:cf:2d:31'])
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
found_name = ssh._get_hosts_name_for_node(ssh_obj, info)
|
||||||
|
self.assertEqual(found_name, 'NodeName')
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__get_hosts_name_for_node_no_match(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "22:22:22:22:22:22"]
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
|
||||||
|
ssh._exec_ssh_command(ssh_obj, info['cmd_set']['list_all']).AndReturn(
|
||||||
|
['NodeName'])
|
||||||
|
cmd_to_exec = info['cmd_set']['get_node_macs']
|
||||||
|
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
|
||||||
|
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(
|
||||||
|
['52:54:00:cf:2d:31'])
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
found_name = ssh._get_hosts_name_for_node(ssh_obj, info)
|
||||||
|
self.assertEqual(found_name, None)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__power_on_good(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_power_status')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
|
||||||
|
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_OFF)
|
||||||
|
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
|
||||||
|
cmd_to_exec = info['cmd_set']['start_cmd']
|
||||||
|
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
|
||||||
|
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(None)
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
current_state = ssh._power_on(ssh_obj, info)
|
||||||
|
self.assertEqual(current_state, states.POWER_ON)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__power_on_fail(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_power_status')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
|
||||||
|
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_OFF)
|
||||||
|
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
|
||||||
|
cmd_to_exec = info['cmd_set']['start_cmd']
|
||||||
|
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
|
||||||
|
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(None)
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_OFF)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
current_state = ssh._power_on(ssh_obj, info)
|
||||||
|
self.assertEqual(current_state, states.ERROR)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__power_off_good(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_power_status')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
|
||||||
|
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
|
||||||
|
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
|
||||||
|
cmd_to_exec = info['cmd_set']['stop_cmd']
|
||||||
|
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
|
||||||
|
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(None)
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_OFF)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
current_state = ssh._power_off(ssh_obj, info)
|
||||||
|
self.assertEqual(current_state, states.POWER_OFF)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__power_off_fail(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_power_status')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_hosts_name_for_node')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_exec_ssh_command')
|
||||||
|
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
|
||||||
|
ssh._get_hosts_name_for_node(ssh_obj, info).AndReturn("NodeName")
|
||||||
|
cmd_to_exec = info['cmd_set']['stop_cmd']
|
||||||
|
cmd_to_exec = cmd_to_exec.replace('{_NodeName_}', 'NodeName')
|
||||||
|
ssh._exec_ssh_command(ssh_obj, cmd_to_exec).AndReturn(None)
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
current_state = ssh._power_off(ssh_obj, info)
|
||||||
|
self.assertEqual(current_state, states.ERROR)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_reboot_good(self):
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(ssh, '_parse_control_info')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_connection')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_power_status')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_power_off')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_power_on')
|
||||||
|
ssh._parse_control_info(self.node).AndReturn(info)
|
||||||
|
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
|
||||||
|
info['macs'])
|
||||||
|
ssh._get_connection(self.node).AndReturn(ssh_obj)
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
|
||||||
|
ssh._power_off(ssh_obj, info).AndReturn(None)
|
||||||
|
ssh._power_on(ssh_obj, info).AndReturn(states.POWER_ON)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
with task_manager.acquire([info['uuid']], shared=False) as task:
|
||||||
|
task.resources[0].controller.reboot(task, self.node)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_reboot_fail(self):
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(ssh, '_parse_control_info')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_connection')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_power_status')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_power_off')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_power_on')
|
||||||
|
ssh._parse_control_info(self.node).AndReturn(info)
|
||||||
|
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
|
||||||
|
info['macs'])
|
||||||
|
ssh._get_connection(self.node).AndReturn(ssh_obj)
|
||||||
|
ssh._get_power_status(ssh_obj, info).AndReturn(states.POWER_ON)
|
||||||
|
ssh._power_off(ssh_obj, info).AndReturn(None)
|
||||||
|
ssh._power_on(ssh_obj, info).AndReturn(states.POWER_OFF)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
with task_manager.acquire([info['uuid']], shared=False) as task:
|
||||||
|
self.assertRaises(exception.PowerStateFailure,
|
||||||
|
task.resources[0].controller.reboot,
|
||||||
|
task,
|
||||||
|
self.node)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_exec_ssh_command_good(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh_obj, 'exec_command')
|
||||||
|
|
||||||
|
class Channel(object):
|
||||||
|
def recv_exit_status(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class Stream(object):
|
||||||
|
def __init__(self, buffer=''):
|
||||||
|
self.buffer = buffer
|
||||||
|
self.channel = Channel()
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
return self.buffer
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
ssh_obj.exec_command("command").AndReturn(
|
||||||
|
(Stream(), Stream('hello'), Stream()))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
stdout, stderr = ssh._exec_ssh_command(ssh_obj, "command")
|
||||||
|
self.assertEqual(stdout, 'hello')
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_exec_ssh_command_fail(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
self.mox.StubOutWithMock(ssh_obj, 'exec_command')
|
||||||
|
|
||||||
|
class Channel(object):
|
||||||
|
def recv_exit_status(self):
|
||||||
|
return 127
|
||||||
|
|
||||||
|
class Stream(object):
|
||||||
|
def __init__(self, buffer=''):
|
||||||
|
self.buffer = buffer
|
||||||
|
self.channel = Channel()
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
return self.buffer
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
ssh_obj.exec_command("command").AndReturn(
|
||||||
|
(Stream(), Stream('hello'), Stream()))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
self.assertRaises(exception.ProcessExecutionError,
|
||||||
|
ssh._exec_ssh_command,
|
||||||
|
ssh_obj,
|
||||||
|
"command")
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_start_console(self):
|
||||||
|
self.assertRaises(exception.IronicException,
|
||||||
|
self.ssh.start_console,
|
||||||
|
None,
|
||||||
|
self.node)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_stop_console(self):
|
||||||
|
self.assertRaises(exception.IronicException,
|
||||||
|
self.ssh.start_console,
|
||||||
|
None,
|
||||||
|
self.node)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_set_power_state_bad_state(self):
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(ssh, '_parse_control_info')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_connection')
|
||||||
|
ssh._parse_control_info(self.node).AndReturn(info)
|
||||||
|
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
|
||||||
|
info['macs'])
|
||||||
|
ssh._get_connection(self.node).AndReturn(ssh_obj)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
with task_manager.acquire([info['uuid']], shared=False) as task:
|
||||||
|
self.assertRaises(exception.IronicException,
|
||||||
|
task.resources[0].controller.set_power_state,
|
||||||
|
task,
|
||||||
|
self.node,
|
||||||
|
"BAD_PSTATE")
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_set_power_state_on_good(self):
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(ssh, '_parse_control_info')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_connection')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_power_on')
|
||||||
|
ssh._parse_control_info(self.node).AndReturn(info)
|
||||||
|
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
|
||||||
|
info['macs'])
|
||||||
|
ssh._get_connection(self.node).AndReturn(ssh_obj)
|
||||||
|
ssh._power_on(ssh_obj, info).AndReturn(states.POWER_ON)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
with task_manager.acquire([info['uuid']], shared=False) as task:
|
||||||
|
task.resources[0].controller.set_power_state(
|
||||||
|
task,
|
||||||
|
self.node,
|
||||||
|
states.POWER_ON)
|
||||||
|
self.assert_(True)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_set_power_state_on_fail(self):
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(ssh, '_parse_control_info')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_connection')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_power_on')
|
||||||
|
ssh._parse_control_info(self.node).AndReturn(info)
|
||||||
|
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
|
||||||
|
info['macs'])
|
||||||
|
ssh._get_connection(self.node).AndReturn(ssh_obj)
|
||||||
|
ssh._power_on(ssh_obj, info).AndReturn(states.POWER_OFF)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
with task_manager.acquire([info['uuid']], shared=False) as task:
|
||||||
|
self.assertRaises(exception.PowerStateFailure,
|
||||||
|
task.resources[0].controller.set_power_state,
|
||||||
|
task,
|
||||||
|
self.node,
|
||||||
|
states.POWER_ON)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_set_power_state_off_good(self):
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(ssh, '_parse_control_info')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_connection')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_power_off')
|
||||||
|
ssh._parse_control_info(self.node).AndReturn(info)
|
||||||
|
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
|
||||||
|
info['macs'])
|
||||||
|
ssh._get_connection(self.node).AndReturn(ssh_obj)
|
||||||
|
ssh._power_off(ssh_obj, info).AndReturn(states.POWER_OFF)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
with task_manager.acquire([info['uuid']], shared=False) as task:
|
||||||
|
task.resources[0].controller.set_power_state(
|
||||||
|
task,
|
||||||
|
self.node,
|
||||||
|
states.POWER_OFF)
|
||||||
|
self.assert_(True)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_set_power_state_off_fail(self):
|
||||||
|
ssh_obj = paramiko.SSHClient()
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
info['macs'] = ["11:11:11:11:11:11", "52:54:00:cf:2d:31"]
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(ssh, '_parse_control_info')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_nodes_mac_addresses')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_get_connection')
|
||||||
|
self.mox.StubOutWithMock(ssh, '_power_off')
|
||||||
|
ssh._parse_control_info(self.node).AndReturn(info)
|
||||||
|
ssh._get_nodes_mac_addresses(mox.IgnoreArg(), self.node).AndReturn(
|
||||||
|
info['macs'])
|
||||||
|
ssh._get_connection(self.node).AndReturn(ssh_obj)
|
||||||
|
ssh._power_off(ssh_obj, info).AndReturn(states.POWER_ON)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
with task_manager.acquire([info['uuid']], shared=False) as task:
|
||||||
|
self.assertRaises(exception.PowerStateFailure,
|
||||||
|
task.resources[0].controller.set_power_state,
|
||||||
|
task,
|
||||||
|
self.node,
|
||||||
|
states.POWER_OFF)
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test__get_nodes_mac_addresses(self):
|
||||||
|
info = ssh._parse_control_info(self.node)
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(self.dbapi, 'get_ports_by_node')
|
||||||
|
self.dbapi.get_ports_by_node(self.node.get('uuid')).\
|
||||||
|
AndReturn(self.ifports)
|
||||||
|
self.dbapi.get_ports_by_node(self.node.get('id')).\
|
||||||
|
AndReturn(self.ifports)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
with task_manager.acquire([info['uuid']]) as task:
|
||||||
|
node_macs = ssh._get_nodes_mac_addresses(task, self.node)
|
||||||
|
self.assertEqual(node_macs, ['aa:bb:cc', 'dd:ee:ff'])
|
||||||
|
self.mox.VerifyAll()
|
@ -35,7 +35,7 @@ console_scripts =
|
|||||||
ironic.controllers =
|
ironic.controllers =
|
||||||
fake = ironic.drivers.fake:FakeControlDriver
|
fake = ironic.drivers.fake:FakeControlDriver
|
||||||
ipmi = ironic.drivers.ipmi:IPMIPowerDriver
|
ipmi = ironic.drivers.ipmi:IPMIPowerDriver
|
||||||
# vpd = ironic.drivers.vpd:VirtualPowerDriver
|
ssh = ironic.drivers.ssh:SSHPowerDriver
|
||||||
|
|
||||||
ironic.deployers =
|
ironic.deployers =
|
||||||
fake = ironic.drivers.fake:FakeDeployDriver
|
fake = ironic.drivers.fake:FakeDeployDriver
|
||||||
|
Loading…
Reference in New Issue
Block a user