Remove most unsupported drivers
In accordance with ironic CI policies, this patch removesi some drivers that are not tested on upstream or third-party CI and for which maintainers have not communicated any plans to have such testing. This includes: - virtualbox drivers - seamicro drivers - msftocs drivers Change-Id: Ia3a7d798c877f4628946ee6c56d850b9847e6c3e Closes-Bug: #1663018
This commit is contained in:
parent
d601a11e91
commit
02ce7246ec
@ -125,9 +125,8 @@ The web console can be configured in Bare Metal service in the following way:
|
|||||||
ironic driver-properties <driver>
|
ironic driver-properties <driver>
|
||||||
|
|
||||||
For ``*_ipmitool`` and ``*_ipminative`` drivers, this option is ``ipmi_terminal_port``.
|
For ``*_ipmitool`` and ``*_ipminative`` drivers, this option is ``ipmi_terminal_port``.
|
||||||
For ``seamicro`` driver, this option is ``seamicro_terminal_port``. Give a customized port
|
Give a customized port number to ``<customized_port>``,
|
||||||
number to ``<customized_port>``, for example ``8023``, this customized port is used in
|
for example ``8023``, this customized port is used in web console url.
|
||||||
web console url.
|
|
||||||
|
|
||||||
Get web console information for a node as follows::
|
Get web console information for a node as follows::
|
||||||
|
|
||||||
|
@ -48,14 +48,6 @@ iLO driver
|
|||||||
|
|
||||||
../drivers/ilo
|
../drivers/ilo
|
||||||
|
|
||||||
SeaMicro driver
|
|
||||||
---------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
../drivers/seamicro
|
|
||||||
|
|
||||||
iRMC driver
|
iRMC driver
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
@ -64,15 +56,6 @@ iRMC driver
|
|||||||
|
|
||||||
../drivers/irmc
|
../drivers/irmc
|
||||||
|
|
||||||
VirtualBox driver
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
|
|
||||||
../drivers/vbox
|
|
||||||
|
|
||||||
|
|
||||||
Cisco UCS driver
|
Cisco UCS driver
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
@ -118,5 +101,8 @@ and as of Ocata release they are removed form ironic:
|
|||||||
- AMT driver - available as part of ironic-staging-drivers_
|
- AMT driver - available as part of ironic-staging-drivers_
|
||||||
- iBoot driver - available as part of ironic-staging-drivers_
|
- iBoot driver - available as part of ironic-staging-drivers_
|
||||||
- Wake-On-Lan driver - available as part of ironic-staging-drivers_
|
- Wake-On-Lan driver - available as part of ironic-staging-drivers_
|
||||||
|
- Virtualbox drivers
|
||||||
|
- SeaMicro drivers
|
||||||
|
- MSFT OCS drivers
|
||||||
|
|
||||||
.. _ironic-staging-drivers: http://ironic-staging-drivers.readthedocs.io
|
.. _ironic-staging-drivers: http://ironic-staging-drivers.readthedocs.io
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
.. _SeaMicro:
|
|
||||||
|
|
||||||
===============
|
|
||||||
SeaMicro driver
|
|
||||||
===============
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
|
||||||
The SeaMicro power driver enables you to take advantage of power cycle
|
|
||||||
management of servers (nodes) within the SeaMicro chassis. The SeaMicro
|
|
||||||
driver is targeted for SeaMicro Fabric Compute systems.
|
|
||||||
|
|
||||||
Prerequisites
|
|
||||||
=============
|
|
||||||
|
|
||||||
* ``python-seamicroclient`` is a python package which contains a set of modules
|
|
||||||
for managing SeaMicro Fabric Compute systems.
|
|
||||||
|
|
||||||
Install ``python-seamicroclient`` [1]_ module on the Ironic conductor node.
|
|
||||||
Minimum version required is 0.4.0::
|
|
||||||
|
|
||||||
$ pip install "python-seamicroclient>=0.4.0"
|
|
||||||
|
|
||||||
Drivers
|
|
||||||
=======
|
|
||||||
|
|
||||||
pxe_seamicro driver
|
|
||||||
^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Overview
|
|
||||||
~~~~~~~~
|
|
||||||
``pxe_seamicro`` driver uses PXE/iSCSI (just like ``pxe_ipmitool`` driver) to
|
|
||||||
deploy the image and uses SeaMicro to do all management operations on the
|
|
||||||
baremetal node (instead of using IPMI).
|
|
||||||
|
|
||||||
Target Users
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
* Users who want to use PXE/iSCSI for deployment in their environment.
|
|
||||||
* Users who want to use SeaMicro Fabric Compute systems.
|
|
||||||
|
|
||||||
Tested Platforms
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
This driver works on SeaMicro Fabric Compute system.
|
|
||||||
It has been tested with the following servers:
|
|
||||||
|
|
||||||
* SeaMicro SM15000-XN
|
|
||||||
* SeaMicro SM15000-OP
|
|
||||||
|
|
||||||
Requirements
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
None.
|
|
||||||
|
|
||||||
Configuring and Enabling the driver
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
1. Build or download a deploy image, see `Building or downloading a deploy ramdisk image`_.
|
|
||||||
|
|
||||||
2. Upload these images to Glance::
|
|
||||||
|
|
||||||
glance image-create --name deploy-ramdisk.kernel --disk-format aki --container-format aki < deploy-ramdisk.kernel
|
|
||||||
glance image-create --name deploy-ramdisk.initramfs --disk-format ari --container-format ari < deploy-ramdisk.initramfs
|
|
||||||
|
|
||||||
3. Add ``pxe_seamicro`` to the list of ``enabled_drivers`` in
|
|
||||||
``/etc/ironic/ironic.conf``. For example::
|
|
||||||
|
|
||||||
enabled_drivers = pxe_ipmitool,pxe_seamicro
|
|
||||||
|
|
||||||
4. Restart the Ironic conductor service::
|
|
||||||
|
|
||||||
service ironic-conductor restart
|
|
||||||
|
|
||||||
Registering SeaMicro node in Ironic
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
Nodes configured for SeaMicro driver should have the ``driver`` property set to
|
|
||||||
``pxe_seamicro``. The following configuration values are also required in
|
|
||||||
``driver_info``:
|
|
||||||
|
|
||||||
- ``seamicro_api_endpoint``: IP address or hostname of the SeaMicro with valid
|
|
||||||
URL as http://<IP_address/hostname>/v2.0
|
|
||||||
- ``seamicro_server_id``: SeaMicro Server ID. Expected format is <int>/<int>
|
|
||||||
- ``seamicro_username``: SeaMicro Username with administrator privileges.
|
|
||||||
- ``seamicro_password``: Password for the above SeaMicro user.
|
|
||||||
- ``deploy_kernel``: The Glance UUID of the deployment kernel.
|
|
||||||
- ``deploy_ramdisk``: The Glance UUID of the deployment ramdisk.
|
|
||||||
- ``seamicro_api_version``: (optional) SeaMicro API Version defaults to "2".
|
|
||||||
- ``seamicro_terminal_port``: (optional) Node's UDP port for console access.
|
|
||||||
Any unused port on the Ironic conductor node may be used.
|
|
||||||
|
|
||||||
The following sequence of commands can be used to enroll a SeaMicro node and
|
|
||||||
boot an instance on it:
|
|
||||||
|
|
||||||
Create nova baremetal flavor corresponding to SeaMicro server's config::
|
|
||||||
|
|
||||||
nova flavor-create baremetal auto <memory_size_in_MB> <disk_size_in_GB> <number_of_cpus>
|
|
||||||
|
|
||||||
Create Node::
|
|
||||||
|
|
||||||
ironic node-create -d pxe_seamicro -i seamicro_api_endpoint=https://<seamicro_ip_address>/ -i seamicro_server_id=<seamicro_server_id> -i seamicro_username=<seamicro_username> -i seamicro_password=<seamicro_password> -i seamicro_api_version=<seamicro_api_version> -i seamicro_terminal_port=<seamicro_terminal_port> -i deploy_kernel=<glance_uuid_of_deploy_kernel> -i deploy_ramdisk=<glance_uuid_of_deploy_ramdisk> -p cpus=<number_of_cpus> -p memory_mb=<memory_size_in_MB> -p local_gb=<local_disk_size_in_GB> -p cpu_arch=<cpu_arch>
|
|
||||||
|
|
||||||
Associate port with the node created::
|
|
||||||
|
|
||||||
ironic port-create -n $NODE -a <MAC_address_of_SeaMicro_server's_NIC>
|
|
||||||
|
|
||||||
Associate properties with the flavor::
|
|
||||||
|
|
||||||
nova flavor-key baremetal set "cpu_arch"=<cpu_arch>
|
|
||||||
|
|
||||||
Boot the Instance::
|
|
||||||
|
|
||||||
nova boot --flavor baremetal --image test-image instance-1
|
|
||||||
|
|
||||||
References
|
|
||||||
==========
|
|
||||||
.. [1] Python-seamicroclient - https://pypi.python.org/pypi/python-seamicroclient
|
|
||||||
.. [2] DiskImage-Builder - http://docs.openstack.org/developer/diskimage-builder/
|
|
||||||
|
|
||||||
.. _`Building or downloading a deploy ramdisk image`: http://docs.openstack.org/project-install-guide/baremetal/draft/deploy-ramdisk.html
|
|
@ -1,121 +0,0 @@
|
|||||||
.. _vbox:
|
|
||||||
|
|
||||||
==================
|
|
||||||
VirtualBox drivers
|
|
||||||
==================
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
|
||||||
|
|
||||||
VirtualBox drivers can be used to test Ironic by using VirtualBox VMs to
|
|
||||||
simulate bare metal nodes.
|
|
||||||
|
|
||||||
Ironic provides support via the ``pxe_ssh`` and ``agent_ssh`` drivers for using
|
|
||||||
a VirtualBox VM as a bare metal target and do provisioning on it. It works by
|
|
||||||
connecting via SSH into the VirtualBox host and running commands using
|
|
||||||
VBoxManage. This works well if you have VirtualBox installed on a Linux box.
|
|
||||||
But when VirtualBox is installed on a Windows box, configuring and getting SSH
|
|
||||||
to work with VBoxManage is difficult (if not impossible) due to the following
|
|
||||||
reasons:
|
|
||||||
|
|
||||||
* Windows doesn't come with native SSH support and one needs to use some
|
|
||||||
third-party software to enable SSH support on Windows.
|
|
||||||
* Even after configuring SSH, VBoxManage doesn't work remotely due to how
|
|
||||||
Windows manages user accounts -- the native Windows user account is different
|
|
||||||
from the corresponding SSH user account, and VBoxManage doesn't work
|
|
||||||
properly when done with the SSH user account.
|
|
||||||
* Even after tweaking the policies of the VirtualBox application, the remote
|
|
||||||
VBoxManage and VBoxSvc don't sync each other properly and often results in
|
|
||||||
a crash.
|
|
||||||
|
|
||||||
VirtualBox drivers use SOAP to talk to the VirtualBox web service running on
|
|
||||||
the VirtualBox host. These drivers are primarily intended for Ironic developers
|
|
||||||
running Windows on their laptops/desktops, although they can be used on other
|
|
||||||
operating systems as well. Using these drivers, a developer could configure a
|
|
||||||
cloud controller on one VirtualBox VM and use other VMs in the same VirtualBox
|
|
||||||
as bare metals for that cloud controller.
|
|
||||||
|
|
||||||
These VirtualBox drivers are available :
|
|
||||||
|
|
||||||
* ``pxe_vbox``: uses iSCSI-based deployment mechanism.
|
|
||||||
* ``agent_vbox``: uses agent-based deployment mechanism.
|
|
||||||
* ``fake_vbox``: uses VirtualBox for power and management, but uses fake
|
|
||||||
deploy.
|
|
||||||
|
|
||||||
|
|
||||||
Setting up development environment
|
|
||||||
==================================
|
|
||||||
|
|
||||||
* Install VirtualBox on your desktop or laptop.
|
|
||||||
|
|
||||||
* Create a VM for the cloud controller. Do not power on the VM now.
|
|
||||||
For example, ``cloud-controller``.
|
|
||||||
|
|
||||||
* In VirtualBox Manager, Select ``cloud-controller`` VM -> Click Settings ->
|
|
||||||
Network -> Adapter 2 -> Select 'Enable Network Adapter' ->
|
|
||||||
Select Attached to: Internal Network -> Select Name: intnet
|
|
||||||
|
|
||||||
* Create a VM in VirtualBox to act as bare metal. A VM with 1 CPU,
|
|
||||||
1 GB memory should be sufficient. Let's name this VM as ``baremetal``.
|
|
||||||
|
|
||||||
* In VirtualBox Manager, Select ``baremetal`` VM -> Click Settings ->
|
|
||||||
Network -> Adapter 1 -> Select 'Enable Network Adapter' ->
|
|
||||||
Select Attached to: Internal Network -> Select Name: intnet
|
|
||||||
|
|
||||||
* Configure the VirtualBox web service to disable authentication. (This is
|
|
||||||
only a suggestion. If you want, enable authentication with the appropriate
|
|
||||||
web service authentication library.)
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
VBoxManage setproperty websrvauthlibrary null
|
|
||||||
|
|
||||||
* Run VirtualBox web service::
|
|
||||||
|
|
||||||
C:\Program Files\Oracle\VirtualBox\VBoxWebSrv.exe
|
|
||||||
|
|
||||||
* Power on the ``cloud-controller`` VM.
|
|
||||||
|
|
||||||
* All the following instructions are to be done in the ``cloud-controller`` VM.
|
|
||||||
|
|
||||||
* Install the GNU/Linux distribution of your choice.
|
|
||||||
|
|
||||||
* Set up devstack.
|
|
||||||
|
|
||||||
* Install pyremotevbox::
|
|
||||||
|
|
||||||
sudo pip install "pyremotevbox>=0.5.0"
|
|
||||||
|
|
||||||
* Enable one (or more) of the VirtualBox drivers (``pxe_vbox``, ``agent_vbox``,
|
|
||||||
or ``fake_vbox``) via the ``enabled_drivers`` configuration option in
|
|
||||||
``/etc/ironic/ironic.conf``, and restart Ironic conductor.
|
|
||||||
|
|
||||||
* Set up flat networking on ``eth1``. For details on how to do this, see
|
|
||||||
`Configure Networking to communicate with the bare metal server`_.
|
|
||||||
|
|
||||||
* Enroll a VirtualBox node. The following examples use the ``pxe_vbox``
|
|
||||||
driver.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
ironic node-create -d pxe_vbox -i virtualbox_host='10.0.2.2' -i virtualbox_vmname='baremetal'
|
|
||||||
|
|
||||||
If you are using authentication with VirtualBox web service, your username
|
|
||||||
and password need to be provided. The ironic node-create command will look
|
|
||||||
like::
|
|
||||||
|
|
||||||
ironic node-create -d pxe_vbox -i virtualbox_host='10.0.2.2' -i virtualbox_vmname='baremetal' -i virtualbox_username=<username> -i virtualbox_password=<password>
|
|
||||||
|
|
||||||
If VirtualBox web service is listening on a different port than the default
|
|
||||||
18083, then that port may be specified using the driver_info
|
|
||||||
parameter ``virtualbox_port``.
|
|
||||||
|
|
||||||
* Add other Node properties and trigger provisioning on the bare metal node.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
When a newly created bare metal VM is powered on for the first time by
|
|
||||||
Ironic (during provisioning), VirtualBox will automatically pop up a
|
|
||||||
dialog box asking to 'Select start-up disk'. Just press 'Cancel' to
|
|
||||||
continue booting the VM.
|
|
||||||
|
|
||||||
.. _`Configure Networking to communicate with the bare metal server`: http://docs.openstack.org/project-install-guide/baremetal/draft/configure-integration.html#configure-networking-to-communicate-with-the-bare-metal-server
|
|
@ -10,14 +10,8 @@ pysnmp
|
|||||||
python-ironic-inspector-client>=1.5.0
|
python-ironic-inspector-client>=1.5.0
|
||||||
python-oneviewclient<3.0.0,>=2.5.2
|
python-oneviewclient<3.0.0,>=2.5.2
|
||||||
python-scciclient>=0.4.0
|
python-scciclient>=0.4.0
|
||||||
python-seamicroclient>=0.4.0
|
|
||||||
UcsSdk==0.8.2.2
|
UcsSdk==0.8.2.2
|
||||||
python-dracclient>=0.1.0
|
python-dracclient>=0.1.0
|
||||||
|
|
||||||
# 'pxe_vbox' and 'agent_vbox' drivers require pyremotevbox library.
|
|
||||||
# Refer documentation on how to install and configure this:
|
|
||||||
# http://docs.openstack.org/developer/ironic/drivers/vbox.html
|
|
||||||
pyremotevbox>=0.5.0
|
|
||||||
|
|
||||||
# The CIMC drivers use the Cisco IMC SDK version 0.7.2 or greater
|
# The CIMC drivers use the Cisco IMC SDK version 0.7.2 or greater
|
||||||
ImcSdk>=0.7.2
|
ImcSdk>=0.7.2
|
||||||
|
@ -3291,20 +3291,6 @@
|
|||||||
#ipxe_use_swift = false
|
#ipxe_use_swift = false
|
||||||
|
|
||||||
|
|
||||||
[seamicro]
|
|
||||||
|
|
||||||
#
|
|
||||||
# From ironic
|
|
||||||
#
|
|
||||||
|
|
||||||
# Maximum retries for SeaMicro operations (integer value)
|
|
||||||
#max_retry = 3
|
|
||||||
|
|
||||||
# Seconds to wait for power action to be completed (integer
|
|
||||||
# value)
|
|
||||||
#action_timeout = 10
|
|
||||||
|
|
||||||
|
|
||||||
[service_catalog]
|
[service_catalog]
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -3543,16 +3529,3 @@
|
|||||||
# Username (string value)
|
# Username (string value)
|
||||||
# Deprecated group/name - [swift]/user-name
|
# Deprecated group/name - [swift]/user-name
|
||||||
#username = <None>
|
#username = <None>
|
||||||
|
|
||||||
|
|
||||||
[virtualbox]
|
|
||||||
|
|
||||||
#
|
|
||||||
# From ironic
|
|
||||||
#
|
|
||||||
|
|
||||||
# Port on which VirtualBox web service is listening. (port
|
|
||||||
# value)
|
|
||||||
# Minimum value: 0
|
|
||||||
# Maximum value: 65535
|
|
||||||
#port = 18083
|
|
||||||
|
@ -43,8 +43,8 @@ service via hrefs.
|
|||||||
There are however some limitations for different drivers:
|
There are however some limitations for different drivers:
|
||||||
|
|
||||||
* If you're using one of the drivers that use agent deploy method (namely,
|
* If you're using one of the drivers that use agent deploy method (namely,
|
||||||
``agent_ilo``, ``agent_ipmitool``, ``agent_pyghmi``, ``agent_ssh`` or
|
``agent_ilo``, ``agent_ipmitool``, ``agent_pyghmi`` or ``agent_ssh``)
|
||||||
``agent_vbox``) you have to know MD5 checksum for your instance image. To
|
you have to know MD5 checksum for your instance image. To
|
||||||
compute it, you can use the following command::
|
compute it, you can use the following command::
|
||||||
|
|
||||||
md5sum image.qcow2
|
md5sum image.qcow2
|
||||||
|
@ -476,10 +476,6 @@ class IPMIFailure(IronicException):
|
|||||||
_msg_fmt = _("IPMI call failed: %(cmd)s.")
|
_msg_fmt = _("IPMI call failed: %(cmd)s.")
|
||||||
|
|
||||||
|
|
||||||
class MSFTOCSClientApiException(IronicException):
|
|
||||||
_msg_fmt = _("MSFT OCS call failed.")
|
|
||||||
|
|
||||||
|
|
||||||
class SSHConnectFailed(IronicException):
|
class SSHConnectFailed(IronicException):
|
||||||
_msg_fmt = _("Failed to establish SSH connection to host %(host)s.")
|
_msg_fmt = _("Failed to establish SSH connection to host %(host)s.")
|
||||||
|
|
||||||
@ -666,11 +662,6 @@ class IRMCSharedFileSystemNotMounted(IronicException):
|
|||||||
_msg_fmt = _("iRMC shared file system '%(share)s' is not mounted.")
|
_msg_fmt = _("iRMC shared file system '%(share)s' is not mounted.")
|
||||||
|
|
||||||
|
|
||||||
class VirtualBoxOperationFailed(IronicException):
|
|
||||||
_msg_fmt = _("VirtualBox operation '%(operation)s' failed. "
|
|
||||||
"Error: %(error)s")
|
|
||||||
|
|
||||||
|
|
||||||
class HardwareInspectionFailure(IronicException):
|
class HardwareInspectionFailure(IronicException):
|
||||||
_msg_fmt = _("Failed to inspect hardware. Reason: %(error)s")
|
_msg_fmt = _("Failed to inspect hardware. Reason: %(error)s")
|
||||||
|
|
||||||
|
@ -38,12 +38,10 @@ from ironic.conf import metrics_statsd
|
|||||||
from ironic.conf import neutron
|
from ironic.conf import neutron
|
||||||
from ironic.conf import oneview
|
from ironic.conf import oneview
|
||||||
from ironic.conf import pxe
|
from ironic.conf import pxe
|
||||||
from ironic.conf import seamicro
|
|
||||||
from ironic.conf import service_catalog
|
from ironic.conf import service_catalog
|
||||||
from ironic.conf import snmp
|
from ironic.conf import snmp
|
||||||
from ironic.conf import ssh
|
from ironic.conf import ssh
|
||||||
from ironic.conf import swift
|
from ironic.conf import swift
|
||||||
from ironic.conf import virtualbox
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
@ -70,9 +68,7 @@ metrics_statsd.register_opts(CONF)
|
|||||||
neutron.register_opts(CONF)
|
neutron.register_opts(CONF)
|
||||||
oneview.register_opts(CONF)
|
oneview.register_opts(CONF)
|
||||||
pxe.register_opts(CONF)
|
pxe.register_opts(CONF)
|
||||||
seamicro.register_opts(CONF)
|
|
||||||
service_catalog.register_opts(CONF)
|
service_catalog.register_opts(CONF)
|
||||||
snmp.register_opts(CONF)
|
snmp.register_opts(CONF)
|
||||||
ssh.register_opts(CONF)
|
ssh.register_opts(CONF)
|
||||||
swift.register_opts(CONF)
|
swift.register_opts(CONF)
|
||||||
virtualbox.register_opts(CONF)
|
|
||||||
|
@ -54,12 +54,10 @@ _opts = [
|
|||||||
('neutron', ironic.conf.neutron.list_opts()),
|
('neutron', ironic.conf.neutron.list_opts()),
|
||||||
('oneview', ironic.conf.oneview.opts),
|
('oneview', ironic.conf.oneview.opts),
|
||||||
('pxe', ironic.conf.pxe.opts),
|
('pxe', ironic.conf.pxe.opts),
|
||||||
('seamicro', ironic.conf.seamicro.opts),
|
|
||||||
('service_catalog', ironic.conf.service_catalog.list_opts()),
|
('service_catalog', ironic.conf.service_catalog.list_opts()),
|
||||||
('snmp', ironic.conf.snmp.opts),
|
('snmp', ironic.conf.snmp.opts),
|
||||||
('ssh', ironic.conf.ssh.opts),
|
('ssh', ironic.conf.ssh.opts),
|
||||||
('swift', ironic.conf.swift.list_opts()),
|
('swift', ironic.conf.swift.list_opts()),
|
||||||
('virtualbox', ironic.conf.virtualbox.opts),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
# Copyright 2016 Intel Corporation
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from ironic.common.i18n import _
|
|
||||||
|
|
||||||
opts = [
|
|
||||||
cfg.IntOpt('max_retry',
|
|
||||||
default=3,
|
|
||||||
help=_('Maximum retries for SeaMicro operations')),
|
|
||||||
cfg.IntOpt('action_timeout',
|
|
||||||
default=10,
|
|
||||||
help=_('Seconds to wait for power action to be completed'))
|
|
||||||
]
|
|
||||||
|
|
||||||
opt_group = cfg.OptGroup(name='seamicro',
|
|
||||||
title='Options for the seamicro power driver')
|
|
||||||
|
|
||||||
|
|
||||||
def register_opts(conf):
|
|
||||||
conf.register_group(opt_group)
|
|
||||||
conf.register_opts(opts, group=opt_group)
|
|
@ -1,27 +0,0 @@
|
|||||||
# Copyright 2016 Intel Corporation
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from ironic.common.i18n import _
|
|
||||||
|
|
||||||
opts = [
|
|
||||||
cfg.PortOpt('port',
|
|
||||||
default=18083,
|
|
||||||
help=_('Port on which VirtualBox web service is listening.')),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def register_opts(conf):
|
|
||||||
conf.register_opts(opts, group='virtualbox')
|
|
@ -27,7 +27,6 @@ from ironic.drivers.modules import pxe
|
|||||||
from ironic.drivers.modules import ssh
|
from ironic.drivers.modules import ssh
|
||||||
from ironic.drivers.modules.ucs import management as ucs_mgmt
|
from ironic.drivers.modules.ucs import management as ucs_mgmt
|
||||||
from ironic.drivers.modules.ucs import power as ucs_power
|
from ironic.drivers.modules.ucs import power as ucs_power
|
||||||
from ironic.drivers.modules import virtualbox
|
|
||||||
|
|
||||||
|
|
||||||
# For backward compatibility
|
# For backward compatibility
|
||||||
@ -87,33 +86,6 @@ class AgentAndSSHDriver(base.BaseDriver):
|
|||||||
self.console = ssh.ShellinaboxConsole()
|
self.console = ssh.ShellinaboxConsole()
|
||||||
|
|
||||||
|
|
||||||
class AgentAndVirtualBoxDriver(base.BaseDriver):
|
|
||||||
"""Agent + VirtualBox driver.
|
|
||||||
|
|
||||||
NOTE: This driver is meant only for testing environments.
|
|
||||||
|
|
||||||
This driver implements the `core` functionality, combining
|
|
||||||
:class:`ironic.drivers.modules.virtualbox.VirtualBoxPower` (for power
|
|
||||||
on/off and reboot of VirtualBox virtual machines), with
|
|
||||||
:class:`ironic.drivers.modules.agent.AgentDeploy` (for image
|
|
||||||
deployment). Implementations are in those respective classes; this class
|
|
||||||
is merely the glue between them.
|
|
||||||
"""
|
|
||||||
|
|
||||||
supported = False
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if not importutils.try_import('pyremotevbox'):
|
|
||||||
raise exception.DriverLoadError(
|
|
||||||
driver=self.__class__.__name__,
|
|
||||||
reason=_("Unable to import pyremotevbox library"))
|
|
||||||
self.power = virtualbox.VirtualBoxPower()
|
|
||||||
self.boot = pxe.PXEBoot()
|
|
||||||
self.deploy = agent.AgentDeploy()
|
|
||||||
self.management = virtualbox.VirtualBoxManagement()
|
|
||||||
self.raid = agent.AgentRAID()
|
|
||||||
|
|
||||||
|
|
||||||
class AgentAndUcsDriver(base.BaseDriver):
|
class AgentAndUcsDriver(base.BaseDriver):
|
||||||
"""Agent + Cisco UCSM driver.
|
"""Agent + Cisco UCSM driver.
|
||||||
|
|
||||||
|
@ -42,18 +42,14 @@ from ironic.drivers.modules.irmc import inspect as irmc_inspect
|
|||||||
from ironic.drivers.modules.irmc import management as irmc_management
|
from ironic.drivers.modules.irmc import management as irmc_management
|
||||||
from ironic.drivers.modules.irmc import power as irmc_power
|
from ironic.drivers.modules.irmc import power as irmc_power
|
||||||
from ironic.drivers.modules import iscsi_deploy
|
from ironic.drivers.modules import iscsi_deploy
|
||||||
from ironic.drivers.modules.msftocs import management as msftocs_management
|
|
||||||
from ironic.drivers.modules.msftocs import power as msftocs_power
|
|
||||||
from ironic.drivers.modules.oneview import common as oneview_common
|
from ironic.drivers.modules.oneview import common as oneview_common
|
||||||
from ironic.drivers.modules.oneview import management as oneview_management
|
from ironic.drivers.modules.oneview import management as oneview_management
|
||||||
from ironic.drivers.modules.oneview import power as oneview_power
|
from ironic.drivers.modules.oneview import power as oneview_power
|
||||||
from ironic.drivers.modules import pxe
|
from ironic.drivers.modules import pxe
|
||||||
from ironic.drivers.modules import seamicro
|
|
||||||
from ironic.drivers.modules import snmp
|
from ironic.drivers.modules import snmp
|
||||||
from ironic.drivers.modules import ssh
|
from ironic.drivers.modules import ssh
|
||||||
from ironic.drivers.modules.ucs import management as ucs_mgmt
|
from ironic.drivers.modules.ucs import management as ucs_mgmt
|
||||||
from ironic.drivers.modules.ucs import power as ucs_power
|
from ironic.drivers.modules.ucs import power as ucs_power
|
||||||
from ironic.drivers.modules import virtualbox
|
|
||||||
from ironic.drivers import utils
|
from ironic.drivers import utils
|
||||||
|
|
||||||
|
|
||||||
@ -146,23 +142,6 @@ class FakeIPMINativeDriver(base.BaseDriver):
|
|||||||
self.management = ipminative.NativeIPMIManagement()
|
self.management = ipminative.NativeIPMIManagement()
|
||||||
|
|
||||||
|
|
||||||
class FakeSeaMicroDriver(base.BaseDriver):
|
|
||||||
"""Fake SeaMicro driver."""
|
|
||||||
|
|
||||||
supported = False
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if not importutils.try_import('seamicroclient'):
|
|
||||||
raise exception.DriverLoadError(
|
|
||||||
driver=self.__class__.__name__,
|
|
||||||
reason=_("Unable to import seamicroclient library"))
|
|
||||||
self.power = seamicro.Power()
|
|
||||||
self.deploy = fake.FakeDeploy()
|
|
||||||
self.management = seamicro.Management()
|
|
||||||
self.vendor = seamicro.VendorPassthru()
|
|
||||||
self.console = seamicro.ShellinaboxConsole()
|
|
||||||
|
|
||||||
|
|
||||||
class FakeAgentDriver(base.BaseDriver):
|
class FakeAgentDriver(base.BaseDriver):
|
||||||
"""Example implementation of an AgentDriver."""
|
"""Example implementation of an AgentDriver."""
|
||||||
|
|
||||||
@ -230,21 +209,6 @@ class FakeIRMCDriver(base.BaseDriver):
|
|||||||
self.inspect = irmc_inspect.IRMCInspect()
|
self.inspect = irmc_inspect.IRMCInspect()
|
||||||
|
|
||||||
|
|
||||||
class FakeVirtualBoxDriver(base.BaseDriver):
|
|
||||||
"""Fake VirtualBox driver."""
|
|
||||||
|
|
||||||
supported = False
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if not importutils.try_import('pyremotevbox'):
|
|
||||||
raise exception.DriverLoadError(
|
|
||||||
driver=self.__class__.__name__,
|
|
||||||
reason=_("Unable to import pyremotevbox library"))
|
|
||||||
self.power = virtualbox.VirtualBoxPower()
|
|
||||||
self.deploy = fake.FakeDeploy()
|
|
||||||
self.management = virtualbox.VirtualBoxManagement()
|
|
||||||
|
|
||||||
|
|
||||||
class FakeIPMIToolInspectorDriver(base.BaseDriver):
|
class FakeIPMIToolInspectorDriver(base.BaseDriver):
|
||||||
"""Fake Inspector driver."""
|
"""Fake Inspector driver."""
|
||||||
|
|
||||||
@ -260,17 +224,6 @@ class FakeIPMIToolInspectorDriver(base.BaseDriver):
|
|||||||
self.inspect = inspector.Inspector()
|
self.inspect = inspector.Inspector()
|
||||||
|
|
||||||
|
|
||||||
class FakeMSFTOCSDriver(base.BaseDriver):
|
|
||||||
"""Fake MSFT OCS driver."""
|
|
||||||
|
|
||||||
supported = False
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.power = msftocs_power.MSFTOCSPower()
|
|
||||||
self.deploy = fake.FakeDeploy()
|
|
||||||
self.management = msftocs_management.MSFTOCSManagement()
|
|
||||||
|
|
||||||
|
|
||||||
class FakeUcsDriver(base.BaseDriver):
|
class FakeUcsDriver(base.BaseDriver):
|
||||||
"""Fake UCS driver."""
|
"""Fake UCS driver."""
|
||||||
|
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
# Copyright 2015 Cloudbase Solutions Srl
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import re
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common.i18n import _
|
|
||||||
from ironic.drivers.modules.msftocs import msftocsclient
|
|
||||||
|
|
||||||
REQUIRED_PROPERTIES = {
|
|
||||||
'msftocs_base_url': _('Base url of the OCS chassis manager REST API, '
|
|
||||||
'e.g.: http://10.0.0.1:8000. Required.'),
|
|
||||||
'msftocs_blade_id': _('Blade id, must be a number between 1 and the '
|
|
||||||
'maximum number of blades available in the chassis. '
|
|
||||||
'Required.'),
|
|
||||||
'msftocs_username': _('Username to access the chassis manager REST API. '
|
|
||||||
'Required.'),
|
|
||||||
'msftocs_password': _('Password to access the chassis manager REST API. '
|
|
||||||
'Required.'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_client_info(driver_info):
|
|
||||||
"""Returns an instance of the REST API client and the blade id.
|
|
||||||
|
|
||||||
:param driver_info: the node's driver_info dict.
|
|
||||||
"""
|
|
||||||
client = msftocsclient.MSFTOCSClientApi(driver_info['msftocs_base_url'],
|
|
||||||
driver_info['msftocs_username'],
|
|
||||||
driver_info['msftocs_password'])
|
|
||||||
return client, driver_info['msftocs_blade_id']
|
|
||||||
|
|
||||||
|
|
||||||
def get_properties():
|
|
||||||
"""Returns the driver's properties."""
|
|
||||||
return copy.deepcopy(REQUIRED_PROPERTIES)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_valid_url(url):
|
|
||||||
"""Checks whether a URL is valid.
|
|
||||||
|
|
||||||
:param url: a url string.
|
|
||||||
:returns: True if the url is valid or None, False otherwise.
|
|
||||||
"""
|
|
||||||
r = re.compile(
|
|
||||||
r'^https?://'
|
|
||||||
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)*[A-Z]{2,6}\.?|'
|
|
||||||
r'localhost|'
|
|
||||||
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
|
|
||||||
r'(?::\d+)?'
|
|
||||||
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
|
|
||||||
|
|
||||||
return bool(isinstance(url, six.string_types) and r.search(url))
|
|
||||||
|
|
||||||
|
|
||||||
def _check_required_properties(driver_info):
|
|
||||||
"""Checks if all required properties are present.
|
|
||||||
|
|
||||||
:param driver_info: the node's driver_info dict.
|
|
||||||
:raises: MissingParameterValue if one or more required properties are
|
|
||||||
missing.
|
|
||||||
"""
|
|
||||||
missing_properties = set(REQUIRED_PROPERTIES) - set(driver_info)
|
|
||||||
if missing_properties:
|
|
||||||
raise exception.MissingParameterValue(
|
|
||||||
_('The following parameters were missing: %s') %
|
|
||||||
' '.join(missing_properties))
|
|
||||||
|
|
||||||
|
|
||||||
def parse_driver_info(node):
|
|
||||||
"""Checks for the required properties and values validity.
|
|
||||||
|
|
||||||
:param node: the target node.
|
|
||||||
:raises: MissingParameterValue if one or more required properties are
|
|
||||||
missing.
|
|
||||||
:raises: InvalidParameterValue if a parameter value is invalid.
|
|
||||||
"""
|
|
||||||
driver_info = node.driver_info
|
|
||||||
_check_required_properties(driver_info)
|
|
||||||
|
|
||||||
base_url = driver_info.get('msftocs_base_url')
|
|
||||||
if not _is_valid_url(base_url):
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
_('"%s" is not a valid "msftocs_base_url"') % base_url)
|
|
||||||
|
|
||||||
blade_id = driver_info.get('msftocs_blade_id')
|
|
||||||
try:
|
|
||||||
blade_id = int(blade_id)
|
|
||||||
except ValueError:
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
_('"%s" is not a valid "msftocs_blade_id"') % blade_id)
|
|
||||||
if blade_id < 1:
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
_('"msftocs_blade_id" must be greater than 0. The provided value '
|
|
||||||
'is: %s') % blade_id)
|
|
@ -1,121 +0,0 @@
|
|||||||
# Copyright 2015 Cloudbase Solutions Srl
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common.i18n import _
|
|
||||||
from ironic.conductor import task_manager
|
|
||||||
from ironic.drivers import base
|
|
||||||
from ironic.drivers.modules.msftocs import common as msftocs_common
|
|
||||||
from ironic.drivers.modules.msftocs import msftocsclient
|
|
||||||
from ironic.drivers import utils as drivers_utils
|
|
||||||
|
|
||||||
BOOT_TYPE_TO_DEVICE_MAP = {
|
|
||||||
msftocsclient.BOOT_TYPE_FORCE_PXE: boot_devices.PXE,
|
|
||||||
msftocsclient.BOOT_TYPE_FORCE_DEFAULT_HDD: boot_devices.DISK,
|
|
||||||
msftocsclient.BOOT_TYPE_FORCE_INTO_BIOS_SETUP: boot_devices.BIOS,
|
|
||||||
}
|
|
||||||
DEVICE_TO_BOOT_TYPE_MAP = {v: k for k, v in BOOT_TYPE_TO_DEVICE_MAP.items()}
|
|
||||||
|
|
||||||
DEFAULT_BOOT_DEVICE = boot_devices.DISK
|
|
||||||
|
|
||||||
|
|
||||||
class MSFTOCSManagement(base.ManagementInterface):
|
|
||||||
def get_properties(self):
|
|
||||||
"""Returns the driver's properties."""
|
|
||||||
return msftocs_common.get_properties()
|
|
||||||
|
|
||||||
def validate(self, task):
|
|
||||||
"""Validate the driver_info in the node.
|
|
||||||
|
|
||||||
Check if the driver_info contains correct required fields.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance containing the target node.
|
|
||||||
:raises: MissingParameterValue if any required parameters are missing.
|
|
||||||
:raises: InvalidParameterValue if any parameters have invalid values.
|
|
||||||
"""
|
|
||||||
msftocs_common.parse_driver_info(task.node)
|
|
||||||
|
|
||||||
def get_supported_boot_devices(self, task):
|
|
||||||
"""Get a list of the supported boot devices.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:returns: A list with the supported boot devices.
|
|
||||||
"""
|
|
||||||
return list(BOOT_TYPE_TO_DEVICE_MAP.values())
|
|
||||||
|
|
||||||
def _check_valid_device(self, device, node):
|
|
||||||
"""Checks if the desired boot device is valid for this driver.
|
|
||||||
|
|
||||||
:param device: a boot device.
|
|
||||||
:param node: the target node.
|
|
||||||
:raises: InvalidParameterValue if the boot device is not valid.
|
|
||||||
"""
|
|
||||||
if device not in DEVICE_TO_BOOT_TYPE_MAP:
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
_("set_boot_device called with invalid device %(device)s for "
|
|
||||||
"node %(node_id)s.") %
|
|
||||||
{'device': device, 'node_id': node.uuid})
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
|
||||||
def set_boot_device(self, task, device, persistent=False):
|
|
||||||
"""Set the boot device for the task's node.
|
|
||||||
|
|
||||||
Set the boot device to use on next boot of the node.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:param device: the boot device.
|
|
||||||
:param persistent: Boolean value. True if the boot device will
|
|
||||||
persist to all future boots, False if not.
|
|
||||||
Default: False.
|
|
||||||
:raises: InvalidParameterValue if an invalid boot device is specified.
|
|
||||||
"""
|
|
||||||
self._check_valid_device(device, task.node)
|
|
||||||
client, blade_id = msftocs_common.get_client_info(
|
|
||||||
task.node.driver_info)
|
|
||||||
|
|
||||||
boot_mode = drivers_utils.get_node_capability(task.node, 'boot_mode')
|
|
||||||
uefi = (boot_mode == 'uefi')
|
|
||||||
|
|
||||||
boot_type = DEVICE_TO_BOOT_TYPE_MAP[device]
|
|
||||||
client.set_next_boot(blade_id, boot_type, persistent, uefi)
|
|
||||||
|
|
||||||
def get_boot_device(self, task):
|
|
||||||
"""Get the current boot device for the task's node.
|
|
||||||
|
|
||||||
Returns the current boot device of the node.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:returns: a dictionary containing:
|
|
||||||
|
|
||||||
:boot_device: the boot device
|
|
||||||
:persistent: Whether the boot device will persist to all
|
|
||||||
future boots or not, None if it is unknown.
|
|
||||||
|
|
||||||
"""
|
|
||||||
client, blade_id = msftocs_common.get_client_info(
|
|
||||||
task.node.driver_info)
|
|
||||||
device = BOOT_TYPE_TO_DEVICE_MAP.get(
|
|
||||||
client.get_next_boot(blade_id), DEFAULT_BOOT_DEVICE)
|
|
||||||
|
|
||||||
# Note(alexpilotti): Although the ChasssisManager REST API allows to
|
|
||||||
# specify the persistent boot status in SetNextBoot, currently it does
|
|
||||||
# not provide a way to retrieve the value with GetNextBoot.
|
|
||||||
# This is being addressed in the ChassisManager API.
|
|
||||||
return {'boot_device': device,
|
|
||||||
'persistent': None}
|
|
||||||
|
|
||||||
def get_sensors_data(self, task):
|
|
||||||
raise NotImplementedError()
|
|
@ -1,176 +0,0 @@
|
|||||||
# Copyright 2015 Cloudbase Solutions Srl
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
MSFT OCS ChassisManager v2.0 REST API client
|
|
||||||
https://github.com/MSOpenTech/ChassisManager
|
|
||||||
"""
|
|
||||||
|
|
||||||
import posixpath
|
|
||||||
from xml import etree
|
|
||||||
|
|
||||||
from oslo_log import log
|
|
||||||
import requests
|
|
||||||
from requests import auth
|
|
||||||
from requests import exceptions as requests_exceptions
|
|
||||||
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common.i18n import _, _LE
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
WCSNS = 'http://schemas.datacontract.org/2004/07/Microsoft.GFS.WCS.Contracts'
|
|
||||||
COMPLETION_CODE_SUCCESS = "Success"
|
|
||||||
|
|
||||||
BOOT_TYPE_UNKNOWN = 0
|
|
||||||
BOOT_TYPE_NO_OVERRIDE = 1
|
|
||||||
BOOT_TYPE_FORCE_PXE = 2
|
|
||||||
BOOT_TYPE_FORCE_DEFAULT_HDD = 3
|
|
||||||
BOOT_TYPE_FORCE_INTO_BIOS_SETUP = 4
|
|
||||||
BOOT_TYPE_FORCE_FLOPPY_OR_REMOVABLE = 5
|
|
||||||
|
|
||||||
BOOT_TYPE_MAP = {
|
|
||||||
'Unknown': BOOT_TYPE_UNKNOWN,
|
|
||||||
'NoOverride': BOOT_TYPE_NO_OVERRIDE,
|
|
||||||
'ForcePxe': BOOT_TYPE_FORCE_PXE,
|
|
||||||
'ForceDefaultHdd': BOOT_TYPE_FORCE_DEFAULT_HDD,
|
|
||||||
'ForceIntoBiosSetup': BOOT_TYPE_FORCE_INTO_BIOS_SETUP,
|
|
||||||
'ForceFloppyOrRemovable': BOOT_TYPE_FORCE_FLOPPY_OR_REMOVABLE,
|
|
||||||
}
|
|
||||||
|
|
||||||
POWER_STATUS_ON = "ON"
|
|
||||||
POWER_STATUS_OFF = "OFF"
|
|
||||||
|
|
||||||
|
|
||||||
class MSFTOCSClientApi(object):
|
|
||||||
def __init__(self, base_url, username, password):
|
|
||||||
self._base_url = base_url
|
|
||||||
self._username = username
|
|
||||||
self._password = password
|
|
||||||
|
|
||||||
def _exec_cmd(self, rel_url):
|
|
||||||
"""Executes a command by calling the chassis manager API."""
|
|
||||||
url = posixpath.join(self._base_url, rel_url)
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
url, auth=auth.HTTPBasicAuth(self._username, self._password))
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests_exceptions.RequestException as ex:
|
|
||||||
msg = _("HTTP call failed: %s") % ex
|
|
||||||
LOG.exception(msg)
|
|
||||||
raise exception.MSFTOCSClientApiException(msg)
|
|
||||||
|
|
||||||
xml_response = response.text
|
|
||||||
LOG.debug("Call to %(url)s got response: %(xml_response)s",
|
|
||||||
{"url": url, "xml_response": xml_response})
|
|
||||||
return xml_response
|
|
||||||
|
|
||||||
def _check_completion_code(self, xml_response):
|
|
||||||
try:
|
|
||||||
et = etree.ElementTree.fromstring(xml_response)
|
|
||||||
except etree.ElementTree.ParseError as ex:
|
|
||||||
LOG.exception(_LE("XML parsing failed: %s"), ex)
|
|
||||||
raise exception.MSFTOCSClientApiException(
|
|
||||||
_("Invalid XML: %s") % xml_response)
|
|
||||||
item = et.find("./n:completionCode", namespaces={'n': WCSNS})
|
|
||||||
if item is None or item.text != COMPLETION_CODE_SUCCESS:
|
|
||||||
raise exception.MSFTOCSClientApiException(
|
|
||||||
_("Operation failed: %s") % xml_response)
|
|
||||||
return et
|
|
||||||
|
|
||||||
def get_blade_state(self, blade_id):
|
|
||||||
"""Returns whether a blade's chipset is receiving power (soft-power).
|
|
||||||
|
|
||||||
:param blade_id: the blade id
|
|
||||||
:returns: one of:
|
|
||||||
POWER_STATUS_ON,
|
|
||||||
POWER_STATUS_OFF
|
|
||||||
:raises: MSFTOCSClientApiException
|
|
||||||
"""
|
|
||||||
et = self._check_completion_code(
|
|
||||||
self._exec_cmd("GetBladeState?bladeId=%d" % blade_id))
|
|
||||||
return et.find('./n:bladeState', namespaces={'n': WCSNS}).text
|
|
||||||
|
|
||||||
def set_blade_on(self, blade_id):
|
|
||||||
"""Supplies power to a blade chipset (soft-power state).
|
|
||||||
|
|
||||||
:param blade_id: the blade id
|
|
||||||
:raises: MSFTOCSClientApiException
|
|
||||||
"""
|
|
||||||
self._check_completion_code(
|
|
||||||
self._exec_cmd("SetBladeOn?bladeId=%d" % blade_id))
|
|
||||||
|
|
||||||
def set_blade_off(self, blade_id):
|
|
||||||
"""Shuts down a given blade (soft-power state).
|
|
||||||
|
|
||||||
:param blade_id: the blade id
|
|
||||||
:raises: MSFTOCSClientApiException
|
|
||||||
"""
|
|
||||||
self._check_completion_code(
|
|
||||||
self._exec_cmd("SetBladeOff?bladeId=%d" % blade_id))
|
|
||||||
|
|
||||||
def set_blade_power_cycle(self, blade_id, off_time=0):
|
|
||||||
"""Performs a soft reboot of a given blade.
|
|
||||||
|
|
||||||
:param blade_id: the blade id
|
|
||||||
:param off_time: seconds to wait between shutdown and boot
|
|
||||||
:raises: MSFTOCSClientApiException
|
|
||||||
"""
|
|
||||||
self._check_completion_code(
|
|
||||||
self._exec_cmd("SetBladeActivePowerCycle?bladeId=%(blade_id)d&"
|
|
||||||
"offTime=%(off_time)d" %
|
|
||||||
{"blade_id": blade_id, "off_time": off_time}))
|
|
||||||
|
|
||||||
def get_next_boot(self, blade_id):
|
|
||||||
"""Returns the next boot device configured for a given blade.
|
|
||||||
|
|
||||||
:param blade_id: the blade id
|
|
||||||
:returns: one of:
|
|
||||||
BOOT_TYPE_UNKNOWN,
|
|
||||||
BOOT_TYPE_NO_OVERRIDE,
|
|
||||||
BOOT_TYPE_FORCE_PXE, BOOT_TYPE_FORCE_DEFAULT_HDD,
|
|
||||||
BOOT_TYPE_FORCE_INTO_BIOS_SETUP,
|
|
||||||
BOOT_TYPE_FORCE_FLOPPY_OR_REMOVABLE
|
|
||||||
:raises: MSFTOCSClientApiException
|
|
||||||
"""
|
|
||||||
et = self._check_completion_code(
|
|
||||||
self._exec_cmd("GetNextBoot?bladeId=%d" % blade_id))
|
|
||||||
return BOOT_TYPE_MAP[
|
|
||||||
et.find('./n:nextBoot', namespaces={'n': WCSNS}).text]
|
|
||||||
|
|
||||||
def set_next_boot(self, blade_id, boot_type, persistent=True, uefi=True):
|
|
||||||
"""Sets the next boot device for a given blade.
|
|
||||||
|
|
||||||
:param blade_id: the blade id
|
|
||||||
:param boot_type: possible values:
|
|
||||||
BOOT_TYPE_UNKNOWN,
|
|
||||||
BOOT_TYPE_NO_OVERRIDE,
|
|
||||||
BOOT_TYPE_FORCE_PXE,
|
|
||||||
BOOT_TYPE_FORCE_DEFAULT_HDD,
|
|
||||||
BOOT_TYPE_FORCE_INTO_BIOS_SETUP,
|
|
||||||
BOOT_TYPE_FORCE_FLOPPY_OR_REMOVABLE
|
|
||||||
:param persistent: whether this setting affects the next boot only or
|
|
||||||
every subsequent boot
|
|
||||||
:param uefi: True if UEFI, False otherwise
|
|
||||||
:raises: MSFTOCSClientApiException
|
|
||||||
"""
|
|
||||||
self._check_completion_code(
|
|
||||||
self._exec_cmd(
|
|
||||||
"SetNextBoot?bladeId=%(blade_id)d&bootType=%(boot_type)d&"
|
|
||||||
"uefi=%(uefi)s&persistent=%(persistent)s" %
|
|
||||||
{"blade_id": blade_id,
|
|
||||||
"boot_type": boot_type,
|
|
||||||
"uefi": str(uefi).lower(),
|
|
||||||
"persistent": str(persistent).lower()}))
|
|
@ -1,105 +0,0 @@
|
|||||||
# Copyright 2015 Cloudbase Solutions Srl
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
MSFT OCS Power Driver
|
|
||||||
"""
|
|
||||||
from oslo_log import log
|
|
||||||
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common.i18n import _, _LE
|
|
||||||
from ironic.common import states
|
|
||||||
from ironic.conductor import task_manager
|
|
||||||
from ironic.drivers import base
|
|
||||||
from ironic.drivers.modules.msftocs import common as msftocs_common
|
|
||||||
from ironic.drivers.modules.msftocs import msftocsclient
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
|
||||||
|
|
||||||
POWER_STATES_MAP = {
|
|
||||||
msftocsclient.POWER_STATUS_ON: states.POWER_ON,
|
|
||||||
msftocsclient.POWER_STATUS_OFF: states.POWER_OFF,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class MSFTOCSPower(base.PowerInterface):
|
|
||||||
def get_properties(self):
|
|
||||||
"""Returns the driver's properties."""
|
|
||||||
return msftocs_common.get_properties()
|
|
||||||
|
|
||||||
def validate(self, task):
|
|
||||||
"""Validate the driver_info in the node.
|
|
||||||
|
|
||||||
Check if the driver_info contains correct required fields.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance containing the target node.
|
|
||||||
:raises: MissingParameterValue if any required parameters are missing.
|
|
||||||
:raises: InvalidParameterValue if any parameters have invalid values.
|
|
||||||
"""
|
|
||||||
msftocs_common.parse_driver_info(task.node)
|
|
||||||
|
|
||||||
def get_power_state(self, task):
|
|
||||||
"""Get the power state from the node.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance containing the target node.
|
|
||||||
:raises: MSFTOCSClientApiException.
|
|
||||||
"""
|
|
||||||
client, blade_id = msftocs_common.get_client_info(
|
|
||||||
task.node.driver_info)
|
|
||||||
return POWER_STATES_MAP[client.get_blade_state(blade_id)]
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
|
||||||
def set_power_state(self, task, pstate):
|
|
||||||
"""Set the power state of the node.
|
|
||||||
|
|
||||||
Turn the node power on or off.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance contains the target node.
|
|
||||||
:param pstate: The desired power state of the node.
|
|
||||||
:raises: PowerStateFailure if the power cannot set to pstate.
|
|
||||||
:raises: InvalidParameterValue
|
|
||||||
"""
|
|
||||||
client, blade_id = msftocs_common.get_client_info(
|
|
||||||
task.node.driver_info)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if pstate == states.POWER_ON:
|
|
||||||
client.set_blade_on(blade_id)
|
|
||||||
elif pstate == states.POWER_OFF:
|
|
||||||
client.set_blade_off(blade_id)
|
|
||||||
else:
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
_('Unsupported target_state: %s') % pstate)
|
|
||||||
except exception.MSFTOCSClientApiException as ex:
|
|
||||||
LOG.exception(_LE("Changing the power state to %(pstate)s failed. "
|
|
||||||
"Error: %(err_msg)s"),
|
|
||||||
{"pstate": pstate, "err_msg": ex})
|
|
||||||
raise exception.PowerStateFailure(pstate=pstate)
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
|
||||||
def reboot(self, task):
|
|
||||||
"""Cycle the power of the node
|
|
||||||
|
|
||||||
:param task: a TaskManager instance contains the target node.
|
|
||||||
:raises: PowerStateFailure if failed to reboot.
|
|
||||||
"""
|
|
||||||
client, blade_id = msftocs_common.get_client_info(
|
|
||||||
task.node.driver_info)
|
|
||||||
try:
|
|
||||||
client.set_blade_power_cycle(blade_id)
|
|
||||||
except exception.MSFTOCSClientApiException as ex:
|
|
||||||
LOG.exception(_LE("Reboot failed. Error: %(err_msg)s"),
|
|
||||||
{"err_msg": ex})
|
|
||||||
raise exception.PowerStateFailure(pstate=states.REBOOT)
|
|
@ -1,672 +0,0 @@
|
|||||||
# 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 SeaMicro interfaces.
|
|
||||||
|
|
||||||
Provides basic power control of servers in SeaMicro chassis via
|
|
||||||
python-seamicroclient.
|
|
||||||
|
|
||||||
Provides vendor passthru methods for SeaMicro specific functionality.
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_service import loopingcall
|
|
||||||
from oslo_utils import importutils
|
|
||||||
from six.moves.urllib import parse as urlparse
|
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common.i18n import _, _LE, _LW
|
|
||||||
from ironic.common import states
|
|
||||||
from ironic.common import utils
|
|
||||||
from ironic.conductor import task_manager
|
|
||||||
from ironic.conf import CONF
|
|
||||||
from ironic.drivers import base
|
|
||||||
from ironic.drivers.modules import console_utils
|
|
||||||
|
|
||||||
seamicroclient = importutils.try_import('seamicroclient')
|
|
||||||
if seamicroclient:
|
|
||||||
from seamicroclient import client as seamicro_client
|
|
||||||
from seamicroclient import exceptions as seamicro_client_exception
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_BOOT_DEVICES_MAP = {
|
|
||||||
boot_devices.DISK: 'hd0',
|
|
||||||
boot_devices.PXE: 'pxe',
|
|
||||||
}
|
|
||||||
|
|
||||||
REQUIRED_PROPERTIES = {
|
|
||||||
'seamicro_api_endpoint': _("API endpoint. Required."),
|
|
||||||
'seamicro_password': _("password. Required."),
|
|
||||||
'seamicro_server_id': _("server ID. Required."),
|
|
||||||
'seamicro_username': _("username. Required."),
|
|
||||||
}
|
|
||||||
OPTIONAL_PROPERTIES = {
|
|
||||||
'seamicro_api_version': _("version of SeaMicro API client; default is 2. "
|
|
||||||
"Optional.")
|
|
||||||
}
|
|
||||||
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
|
|
||||||
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
|
|
||||||
CONSOLE_PROPERTIES = {
|
|
||||||
'seamicro_terminal_port': _("node's UDP port to connect to. "
|
|
||||||
"Only required for console access.")
|
|
||||||
}
|
|
||||||
PORT_BASE = 2000
|
|
||||||
|
|
||||||
|
|
||||||
def _get_client(*args, **kwargs):
|
|
||||||
"""Creates the python-seamicro_client
|
|
||||||
|
|
||||||
:param kwargs: A dict of keyword arguments to be passed to the method,
|
|
||||||
which should contain: 'username', 'password',
|
|
||||||
'auth_url', 'api_version' parameters.
|
|
||||||
:returns: SeaMicro API client.
|
|
||||||
"""
|
|
||||||
|
|
||||||
cl_kwargs = {'username': kwargs['username'],
|
|
||||||
'password': kwargs['password'],
|
|
||||||
'auth_url': kwargs['api_endpoint']}
|
|
||||||
try:
|
|
||||||
return seamicro_client.Client(kwargs['api_version'], **cl_kwargs)
|
|
||||||
except seamicro_client_exception.UnsupportedVersion as e:
|
|
||||||
raise exception.InvalidParameterValue(_(
|
|
||||||
"Invalid 'seamicro_api_version' parameter. Reason: %s.") % e)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_driver_info(node):
|
|
||||||
"""Parses and creates seamicro driver info
|
|
||||||
|
|
||||||
:param node: An Ironic node object.
|
|
||||||
:returns: SeaMicro driver info.
|
|
||||||
:raises: MissingParameterValue if any required parameters are missing.
|
|
||||||
:raises: InvalidParameterValue if required parameter are invalid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
info = node.driver_info or {}
|
|
||||||
missing_info = [key for key in REQUIRED_PROPERTIES if not info.get(key)]
|
|
||||||
if missing_info:
|
|
||||||
raise exception.MissingParameterValue(_(
|
|
||||||
"SeaMicro driver requires the following parameters to be set in"
|
|
||||||
" node's driver_info: %s.") % missing_info)
|
|
||||||
|
|
||||||
api_endpoint = info.get('seamicro_api_endpoint')
|
|
||||||
username = info.get('seamicro_username')
|
|
||||||
password = info.get('seamicro_password')
|
|
||||||
server_id = info.get('seamicro_server_id')
|
|
||||||
api_version = info.get('seamicro_api_version', "2")
|
|
||||||
port = info.get('seamicro_terminal_port')
|
|
||||||
|
|
||||||
if port is not None:
|
|
||||||
port = utils.validate_network_port(port, 'seamicro_terminal_port')
|
|
||||||
|
|
||||||
r = re.compile(r"(^[0-9]+)/([0-9]+$)")
|
|
||||||
if not r.match(server_id):
|
|
||||||
raise exception.InvalidParameterValue(_(
|
|
||||||
"Invalid 'seamicro_server_id' parameter in node's "
|
|
||||||
"driver_info. Expected format of 'seamicro_server_id' "
|
|
||||||
"is <int>/<int>"))
|
|
||||||
|
|
||||||
url = urlparse.urlparse(api_endpoint)
|
|
||||||
if (not (url.scheme == "http") or not url.netloc):
|
|
||||||
raise exception.InvalidParameterValue(_(
|
|
||||||
"Invalid 'seamicro_api_endpoint' parameter in node's "
|
|
||||||
"driver_info."))
|
|
||||||
|
|
||||||
res = {'username': username,
|
|
||||||
'password': password,
|
|
||||||
'api_endpoint': api_endpoint,
|
|
||||||
'server_id': server_id,
|
|
||||||
'api_version': api_version,
|
|
||||||
'uuid': node.uuid,
|
|
||||||
'port': port}
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def _get_server(driver_info):
|
|
||||||
"""Get server from server_id."""
|
|
||||||
|
|
||||||
s_client = _get_client(**driver_info)
|
|
||||||
return s_client.servers.get(driver_info['server_id'])
|
|
||||||
|
|
||||||
|
|
||||||
def _get_volume(driver_info, volume_id):
|
|
||||||
"""Get volume from volume_id."""
|
|
||||||
|
|
||||||
s_client = _get_client(**driver_info)
|
|
||||||
return s_client.volumes.get(volume_id)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_power_status(node):
|
|
||||||
"""Get current power state of this node
|
|
||||||
|
|
||||||
:param node: Ironic node one of :class:`ironic.db.models.Node`
|
|
||||||
:raises: InvalidParameterValue if a seamicro parameter is invalid.
|
|
||||||
:raises: MissingParameterValue if required seamicro parameters are
|
|
||||||
missing.
|
|
||||||
:raises: ServiceUnavailable on an error from SeaMicro Client.
|
|
||||||
:returns: Power state of the given node
|
|
||||||
"""
|
|
||||||
|
|
||||||
seamicro_info = _parse_driver_info(node)
|
|
||||||
try:
|
|
||||||
server = _get_server(seamicro_info)
|
|
||||||
if not hasattr(server, 'active') or server.active is None:
|
|
||||||
return states.ERROR
|
|
||||||
if not server.active:
|
|
||||||
return states.POWER_OFF
|
|
||||||
elif server.active:
|
|
||||||
return states.POWER_ON
|
|
||||||
|
|
||||||
except seamicro_client_exception.NotFound:
|
|
||||||
raise exception.NodeNotFound(node=node.uuid)
|
|
||||||
except seamicro_client_exception.ClientException as ex:
|
|
||||||
LOG.error(_LE("SeaMicro client exception %(msg)s for node %(uuid)s"),
|
|
||||||
{'msg': ex.message, 'uuid': node.uuid})
|
|
||||||
raise exception.ServiceUnavailable(message=ex.message)
|
|
||||||
|
|
||||||
|
|
||||||
def _power_on(node, timeout=None):
|
|
||||||
"""Power ON this node
|
|
||||||
|
|
||||||
:param node: An Ironic node object.
|
|
||||||
:param timeout: Time in seconds to wait till power on is complete.
|
|
||||||
:raises: InvalidParameterValue if a seamicro parameter is invalid.
|
|
||||||
:raises: MissingParameterValue if required seamicro parameters are
|
|
||||||
missing.
|
|
||||||
:returns: Power state of the given node.
|
|
||||||
"""
|
|
||||||
if timeout is None:
|
|
||||||
timeout = CONF.seamicro.action_timeout
|
|
||||||
state = [None]
|
|
||||||
retries = [0]
|
|
||||||
seamicro_info = _parse_driver_info(node)
|
|
||||||
server = _get_server(seamicro_info)
|
|
||||||
|
|
||||||
def _wait_for_power_on(state, retries):
|
|
||||||
"""Called at an interval until the node is powered on."""
|
|
||||||
|
|
||||||
state[0] = _get_power_status(node)
|
|
||||||
if state[0] == states.POWER_ON:
|
|
||||||
raise loopingcall.LoopingCallDone()
|
|
||||||
|
|
||||||
if retries[0] > CONF.seamicro.max_retry:
|
|
||||||
state[0] = states.ERROR
|
|
||||||
raise loopingcall.LoopingCallDone()
|
|
||||||
try:
|
|
||||||
retries[0] += 1
|
|
||||||
server.power_on()
|
|
||||||
except seamicro_client_exception.ClientException:
|
|
||||||
LOG.warning(_LW("Power-on failed for node %s."),
|
|
||||||
node.uuid)
|
|
||||||
|
|
||||||
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_on,
|
|
||||||
state, retries)
|
|
||||||
timer.start(interval=timeout).wait()
|
|
||||||
return state[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _power_off(node, timeout=None):
|
|
||||||
"""Power OFF this node
|
|
||||||
|
|
||||||
:param node: Ironic node one of :class:`ironic.db.models.Node`
|
|
||||||
:param timeout: Time in seconds to wait till power off is compelete
|
|
||||||
:raises: InvalidParameterValue if a seamicro parameter is invalid.
|
|
||||||
:raises: MissingParameterValue if required seamicro parameters are
|
|
||||||
missing.
|
|
||||||
:returns: Power state of the given node
|
|
||||||
"""
|
|
||||||
if timeout is None:
|
|
||||||
timeout = CONF.seamicro.action_timeout
|
|
||||||
state = [None]
|
|
||||||
retries = [0]
|
|
||||||
seamicro_info = _parse_driver_info(node)
|
|
||||||
server = _get_server(seamicro_info)
|
|
||||||
|
|
||||||
def _wait_for_power_off(state, retries):
|
|
||||||
"""Called at an interval until the node is powered off."""
|
|
||||||
|
|
||||||
state[0] = _get_power_status(node)
|
|
||||||
if state[0] == states.POWER_OFF:
|
|
||||||
raise loopingcall.LoopingCallDone()
|
|
||||||
|
|
||||||
if retries[0] > CONF.seamicro.max_retry:
|
|
||||||
state[0] = states.ERROR
|
|
||||||
raise loopingcall.LoopingCallDone()
|
|
||||||
try:
|
|
||||||
retries[0] += 1
|
|
||||||
server.power_off()
|
|
||||||
except seamicro_client_exception.ClientException:
|
|
||||||
LOG.warning(_LW("Power-off failed for node %s."),
|
|
||||||
node.uuid)
|
|
||||||
|
|
||||||
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_off,
|
|
||||||
state, retries)
|
|
||||||
timer.start(interval=timeout).wait()
|
|
||||||
return state[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _reboot(node, timeout=None):
|
|
||||||
"""Reboot this node.
|
|
||||||
|
|
||||||
:param node: Ironic node one of :class:`ironic.db.models.Node`
|
|
||||||
:param timeout: Time in seconds to wait till reboot is compelete
|
|
||||||
:raises: InvalidParameterValue if a seamicro parameter is invalid.
|
|
||||||
:raises: MissingParameterValue if required seamicro parameters are
|
|
||||||
missing.
|
|
||||||
:returns: Power state of the given node
|
|
||||||
"""
|
|
||||||
if timeout is None:
|
|
||||||
timeout = CONF.seamicro.action_timeout
|
|
||||||
state = [None]
|
|
||||||
retries = [0]
|
|
||||||
seamicro_info = _parse_driver_info(node)
|
|
||||||
server = _get_server(seamicro_info)
|
|
||||||
|
|
||||||
def _wait_for_reboot(state, retries):
|
|
||||||
"""Called at an interval until the node is rebooted successfully."""
|
|
||||||
|
|
||||||
state[0] = _get_power_status(node)
|
|
||||||
if state[0] == states.POWER_ON:
|
|
||||||
raise loopingcall.LoopingCallDone()
|
|
||||||
|
|
||||||
if retries[0] > CONF.seamicro.max_retry:
|
|
||||||
state[0] = states.ERROR
|
|
||||||
raise loopingcall.LoopingCallDone()
|
|
||||||
|
|
||||||
try:
|
|
||||||
retries[0] += 1
|
|
||||||
server.reset()
|
|
||||||
except seamicro_client_exception.ClientException:
|
|
||||||
LOG.warning(_LW("Reboot failed for node %s."),
|
|
||||||
node.uuid)
|
|
||||||
|
|
||||||
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_reboot,
|
|
||||||
state, retries)
|
|
||||||
server.reset()
|
|
||||||
timer.start(interval=timeout).wait()
|
|
||||||
return state[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_volume(driver_info, volume_id):
|
|
||||||
"""Validates if volume is in Storage pools designated for ironic."""
|
|
||||||
|
|
||||||
volume = _get_volume(driver_info, volume_id)
|
|
||||||
|
|
||||||
# Check if the ironic <scard>/ironic-<pool_id>/<volume_id> naming scheme
|
|
||||||
# is present in volume id
|
|
||||||
try:
|
|
||||||
pool_id = volume.id.split('/')[1].lower()
|
|
||||||
except IndexError:
|
|
||||||
pool_id = ""
|
|
||||||
|
|
||||||
if "ironic-" in pool_id:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
raise exception.InvalidParameterValue(_(
|
|
||||||
"Invalid volume id specified"))
|
|
||||||
|
|
||||||
|
|
||||||
def _get_pools(driver_info, filters=None):
|
|
||||||
"""Get SeaMicro storage pools matching given filters."""
|
|
||||||
|
|
||||||
s_client = _get_client(**driver_info)
|
|
||||||
return s_client.pools.list(filters=filters)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_volume(driver_info, volume_size):
|
|
||||||
"""Create volume in the SeaMicro storage pools designated for ironic."""
|
|
||||||
|
|
||||||
ironic_pools = _get_pools(driver_info, filters={'id': 'ironic-'})
|
|
||||||
if ironic_pools is None:
|
|
||||||
raise exception.VendorPassthruException(_(
|
|
||||||
"No storage pools found for ironic"))
|
|
||||||
|
|
||||||
least_used_pool = sorted(ironic_pools,
|
|
||||||
key=lambda x: x.freeSize)[0]
|
|
||||||
return _get_client(**driver_info).volumes.create(volume_size,
|
|
||||||
least_used_pool)
|
|
||||||
|
|
||||||
|
|
||||||
def get_telnet_port(driver_info):
|
|
||||||
"""Get SeaMicro telnet port to listen."""
|
|
||||||
server_id = int(driver_info['server_id'].split("/")[0])
|
|
||||||
return PORT_BASE + (10 * server_id)
|
|
||||||
|
|
||||||
|
|
||||||
class Power(base.PowerInterface):
|
|
||||||
"""SeaMicro Power Interface.
|
|
||||||
|
|
||||||
This PowerInterface class provides a mechanism for controlling the power
|
|
||||||
state of servers in a seamicro chassis.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_properties(self):
|
|
||||||
return COMMON_PROPERTIES
|
|
||||||
|
|
||||||
def validate(self, task):
|
|
||||||
"""Check that node 'driver_info' is valid.
|
|
||||||
|
|
||||||
Check that node 'driver_info' contains the required fields.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance containing the node to act on.
|
|
||||||
:raises: MissingParameterValue if required seamicro parameters are
|
|
||||||
missing.
|
|
||||||
"""
|
|
||||||
_parse_driver_info(task.node)
|
|
||||||
|
|
||||||
def get_power_state(self, task):
|
|
||||||
"""Get the current power state of the task's node.
|
|
||||||
|
|
||||||
Poll the host for the current power state of the node.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance containing the node to act on.
|
|
||||||
:raises: ServiceUnavailable on an error from SeaMicro Client.
|
|
||||||
:raises: InvalidParameterValue if a seamicro parameter is invalid.
|
|
||||||
:raises: MissingParameterValue when a required parameter is missing
|
|
||||||
:returns: power state. One of :class:`ironic.common.states`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return _get_power_status(task.node)
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
|
||||||
def set_power_state(self, task, pstate):
|
|
||||||
"""Turn the power on or off.
|
|
||||||
|
|
||||||
Set the power state of a node.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance containing the node to act on.
|
|
||||||
:param pstate: Either POWER_ON or POWER_OFF from :class:
|
|
||||||
`ironic.common.states`.
|
|
||||||
:raises: InvalidParameterValue if an invalid power state was specified
|
|
||||||
or a seamicro parameter is invalid.
|
|
||||||
:raises: MissingParameterValue when a required parameter is missing
|
|
||||||
:raises: PowerStateFailure if the desired power state couldn't be set.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if pstate == states.POWER_ON:
|
|
||||||
state = _power_on(task.node)
|
|
||||||
elif pstate == states.POWER_OFF:
|
|
||||||
state = _power_off(task.node)
|
|
||||||
else:
|
|
||||||
raise exception.InvalidParameterValue(_(
|
|
||||||
"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):
|
|
||||||
"""Cycles the power to the task's node.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance containing the node to act on.
|
|
||||||
:raises: InvalidParameterValue if a seamicro parameter is invalid.
|
|
||||||
:raises: MissingParameterValue if required seamicro parameters are
|
|
||||||
missing.
|
|
||||||
:raises: PowerStateFailure if the final state of the node is not
|
|
||||||
POWER_ON.
|
|
||||||
"""
|
|
||||||
state = _reboot(task.node)
|
|
||||||
|
|
||||||
if state != states.POWER_ON:
|
|
||||||
raise exception.PowerStateFailure(pstate=states.POWER_ON)
|
|
||||||
|
|
||||||
|
|
||||||
class VendorPassthru(base.VendorInterface):
|
|
||||||
"""SeaMicro vendor-specific methods."""
|
|
||||||
|
|
||||||
def get_properties(self):
|
|
||||||
return COMMON_PROPERTIES
|
|
||||||
|
|
||||||
def validate(self, task, method, **kwargs):
|
|
||||||
_parse_driver_info(task.node)
|
|
||||||
|
|
||||||
@base.passthru(['POST'],
|
|
||||||
description=_("Set an untagged VLAN ID for NIC 0 of node. "
|
|
||||||
"Required argument: 'vlan_id' - ID of "
|
|
||||||
"untagged VLAN."))
|
|
||||||
def set_node_vlan_id(self, task, **kwargs):
|
|
||||||
"""Sets an untagged vlan id for NIC 0 of node.
|
|
||||||
|
|
||||||
@kwargs vlan_id: id of untagged vlan for NIC 0 of node
|
|
||||||
"""
|
|
||||||
node = task.node
|
|
||||||
vlan_id = kwargs.get('vlan_id')
|
|
||||||
if not vlan_id:
|
|
||||||
raise exception.MissingParameterValue(_("No vlan id provided"))
|
|
||||||
|
|
||||||
seamicro_info = _parse_driver_info(node)
|
|
||||||
try:
|
|
||||||
server = _get_server(seamicro_info)
|
|
||||||
|
|
||||||
# remove current vlan for server
|
|
||||||
if len(server.nic['0']['untaggedVlan']) > 0:
|
|
||||||
server.unset_untagged_vlan(server.nic['0']['untaggedVlan'])
|
|
||||||
server = server.refresh(5)
|
|
||||||
server.set_untagged_vlan(vlan_id)
|
|
||||||
except seamicro_client_exception.ClientException as ex:
|
|
||||||
LOG.error(_LE("SeaMicro client exception: %s"), ex.message)
|
|
||||||
raise exception.VendorPassthruException(message=ex.message)
|
|
||||||
|
|
||||||
properties = node.properties
|
|
||||||
properties['seamicro_vlan_id'] = vlan_id
|
|
||||||
node.properties = properties
|
|
||||||
node.save()
|
|
||||||
|
|
||||||
@base.passthru(['POST'],
|
|
||||||
description=_("Attach volume to node. Arguments: "
|
|
||||||
"1. 'volume_id' - ID of pre-provisioned "
|
|
||||||
"volume. This is optional. If not specified, "
|
|
||||||
"a volume is created in SeaMicro storage "
|
|
||||||
"pool. 2. 'volume_size' - size of new volume "
|
|
||||||
"(if volume_id is not specified)."))
|
|
||||||
def attach_volume(self, task, **kwargs):
|
|
||||||
"""Attach a volume to a node.
|
|
||||||
|
|
||||||
Attach volume from SeaMicro storage pools for ironic to node.
|
|
||||||
If kwargs['volume_id'] not given, Create volume in SeaMicro
|
|
||||||
storage pool and attach to node.
|
|
||||||
|
|
||||||
@kwargs volume_id: id of pre-provisioned volume that is to be attached
|
|
||||||
as root volume of node
|
|
||||||
@kwargs volume_size: size of new volume to be created and attached
|
|
||||||
as root volume of node
|
|
||||||
"""
|
|
||||||
node = task.node
|
|
||||||
seamicro_info = _parse_driver_info(node)
|
|
||||||
volume_id = kwargs.get('volume_id')
|
|
||||||
|
|
||||||
if volume_id is None:
|
|
||||||
volume_size = kwargs.get('volume_size')
|
|
||||||
if volume_size is None:
|
|
||||||
raise exception.MissingParameterValue(
|
|
||||||
_("No volume size provided for creating volume"))
|
|
||||||
volume_id = _create_volume(seamicro_info, volume_size)
|
|
||||||
|
|
||||||
if _validate_volume(seamicro_info, volume_id):
|
|
||||||
try:
|
|
||||||
server = _get_server(seamicro_info)
|
|
||||||
server.detach_volume()
|
|
||||||
server = server.refresh(5)
|
|
||||||
server.attach_volume(volume_id)
|
|
||||||
except seamicro_client_exception.ClientException as ex:
|
|
||||||
LOG.error(_LE("SeaMicro client exception: %s"), ex.message)
|
|
||||||
raise exception.VendorPassthruException(message=ex.message)
|
|
||||||
|
|
||||||
properties = node.properties
|
|
||||||
properties['seamicro_volume_id'] = volume_id
|
|
||||||
node.properties = properties
|
|
||||||
node.save()
|
|
||||||
|
|
||||||
|
|
||||||
class Management(base.ManagementInterface):
|
|
||||||
|
|
||||||
def get_properties(self):
|
|
||||||
return COMMON_PROPERTIES
|
|
||||||
|
|
||||||
def validate(self, task):
|
|
||||||
"""Check that 'driver_info' contains SeaMicro credentials.
|
|
||||||
|
|
||||||
Validates whether the 'driver_info' property of the supplied
|
|
||||||
task's node contains the required credentials information.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:raises: MissingParameterValue when a required parameter is missing
|
|
||||||
|
|
||||||
"""
|
|
||||||
_parse_driver_info(task.node)
|
|
||||||
|
|
||||||
def get_supported_boot_devices(self, task):
|
|
||||||
"""Get a list of the supported boot devices.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:returns: A list with the supported boot devices defined
|
|
||||||
in :mod:`ironic.common.boot_devices`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return list(_BOOT_DEVICES_MAP.keys())
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
|
||||||
def set_boot_device(self, task, device, persistent=False):
|
|
||||||
"""Set the boot device for the task's node.
|
|
||||||
|
|
||||||
Set the boot device to use on next reboot of the node.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:param device: the boot device, one of
|
|
||||||
:mod:`ironic.common.boot_devices`.
|
|
||||||
:param persistent: Boolean value. True if the boot device will
|
|
||||||
persist to all future boots, False if not.
|
|
||||||
Default: False. Ignored by this driver.
|
|
||||||
:raises: InvalidParameterValue if an invalid boot device is
|
|
||||||
specified or if a seamicro parameter is invalid.
|
|
||||||
:raises: IronicException on an error from seamicro-client.
|
|
||||||
:raises: MissingParameterValue when a required parameter is missing
|
|
||||||
|
|
||||||
"""
|
|
||||||
if device not in self.get_supported_boot_devices(task):
|
|
||||||
raise exception.InvalidParameterValue(_(
|
|
||||||
"Invalid boot device %s specified.") % device)
|
|
||||||
|
|
||||||
seamicro_info = _parse_driver_info(task.node)
|
|
||||||
try:
|
|
||||||
server = _get_server(seamicro_info)
|
|
||||||
boot_device = _BOOT_DEVICES_MAP[device]
|
|
||||||
server.set_boot_order(boot_device)
|
|
||||||
except seamicro_client_exception.ClientException as ex:
|
|
||||||
LOG.error(_LE("Seamicro set boot device failed for node "
|
|
||||||
"%(node)s with the following error: %(error)s"),
|
|
||||||
{'node': task.node.uuid, 'error': ex})
|
|
||||||
raise exception.IronicException(ex)
|
|
||||||
|
|
||||||
def get_boot_device(self, task):
|
|
||||||
"""Get the current boot device for the task's node.
|
|
||||||
|
|
||||||
Returns the current boot device of the node. Be aware that not
|
|
||||||
all drivers support this.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:returns: a dictionary containing:
|
|
||||||
|
|
||||||
:boot_device: the boot device, one of
|
|
||||||
:mod:`ironic.common.boot_devices` or None if it is unknown.
|
|
||||||
:persistent: Whether the boot device will persist to all
|
|
||||||
future boots or not, None if it is unknown.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# TODO(lucasagomes): The python-seamicroclient library currently
|
|
||||||
# doesn't expose a method to get the boot device, update it once
|
|
||||||
# it's implemented.
|
|
||||||
return {'boot_device': None, 'persistent': None}
|
|
||||||
|
|
||||||
def get_sensors_data(self, task):
|
|
||||||
"""Get sensors data method.
|
|
||||||
|
|
||||||
Not implemented by this driver.
|
|
||||||
:param task: a TaskManager instance.
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class ShellinaboxConsole(base.ConsoleInterface):
|
|
||||||
"""A ConsoleInterface that uses telnet and shellinabox."""
|
|
||||||
|
|
||||||
def get_properties(self):
|
|
||||||
d = COMMON_PROPERTIES.copy()
|
|
||||||
d.update(CONSOLE_PROPERTIES)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def validate(self, task):
|
|
||||||
"""Validate the Node console info.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:raises: MissingParameterValue if required seamicro parameters are
|
|
||||||
missing
|
|
||||||
:raises: InvalidParameterValue if required parameter are invalid.
|
|
||||||
"""
|
|
||||||
driver_info = _parse_driver_info(task.node)
|
|
||||||
if not driver_info['port']:
|
|
||||||
raise exception.MissingParameterValue(_(
|
|
||||||
"Missing 'seamicro_terminal_port' parameter in node's "
|
|
||||||
"driver_info"))
|
|
||||||
|
|
||||||
def start_console(self, task):
|
|
||||||
"""Start a remote console for the node.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager
|
|
||||||
:raises: MissingParameterValue if required seamicro parameters are
|
|
||||||
missing
|
|
||||||
:raises: ConsoleError if the directory for the PID file cannot be
|
|
||||||
created
|
|
||||||
:raises: ConsoleSubprocessFailed when invoking the subprocess failed
|
|
||||||
:raises: InvalidParameterValue if required parameter are invalid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
driver_info = _parse_driver_info(task.node)
|
|
||||||
telnet_port = get_telnet_port(driver_info)
|
|
||||||
chassis_ip = urlparse.urlparse(driver_info['api_endpoint']).netloc
|
|
||||||
|
|
||||||
seamicro_cmd = ("/:%(uid)s:%(gid)s:HOME:telnet %(chassis)s %(port)s"
|
|
||||||
% {'uid': os.getuid(),
|
|
||||||
'gid': os.getgid(),
|
|
||||||
'chassis': chassis_ip,
|
|
||||||
'port': telnet_port})
|
|
||||||
|
|
||||||
console_utils.start_shellinabox_console(driver_info['uuid'],
|
|
||||||
driver_info['port'],
|
|
||||||
seamicro_cmd)
|
|
||||||
|
|
||||||
def stop_console(self, task):
|
|
||||||
"""Stop the remote console session for the node.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager
|
|
||||||
:raises: ConsoleError if unable to stop the console
|
|
||||||
"""
|
|
||||||
|
|
||||||
console_utils.stop_shellinabox_console(task.node.uuid)
|
|
||||||
|
|
||||||
def get_console(self, task):
|
|
||||||
"""Get the type and connection information about the console.
|
|
||||||
|
|
||||||
:raises: MissingParameterValue if required seamicro parameters are
|
|
||||||
missing
|
|
||||||
:raises: InvalidParameterValue if required parameter are invalid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
driver_info = _parse_driver_info(task.node)
|
|
||||||
url = console_utils.get_shellinabox_console_url(driver_info['port'])
|
|
||||||
return {'type': 'shellinabox', 'url': url}
|
|
@ -1,393 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
VirtualBox Driver Modules
|
|
||||||
"""
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import importutils
|
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common.i18n import _, _LE, _LW
|
|
||||||
from ironic.common import states
|
|
||||||
from ironic.common import utils
|
|
||||||
from ironic.conductor import task_manager
|
|
||||||
from ironic.conf import CONF
|
|
||||||
from ironic.drivers import base
|
|
||||||
|
|
||||||
pyremotevbox = importutils.try_import('pyremotevbox')
|
|
||||||
if pyremotevbox:
|
|
||||||
from pyremotevbox import exception as virtualbox_exc
|
|
||||||
from pyremotevbox import vbox as virtualbox
|
|
||||||
|
|
||||||
IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING = {
|
|
||||||
boot_devices.PXE: 'Network',
|
|
||||||
boot_devices.DISK: 'HardDisk',
|
|
||||||
boot_devices.CDROM: 'DVD',
|
|
||||||
}
|
|
||||||
VIRTUALBOX_TO_IRONIC_DEVICE_MAPPING = {
|
|
||||||
v: k for k, v in IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING.items()}
|
|
||||||
|
|
||||||
VIRTUALBOX_TO_IRONIC_POWER_MAPPING = {
|
|
||||||
'PoweredOff': states.POWER_OFF,
|
|
||||||
'Running': states.POWER_ON,
|
|
||||||
'Error': states.ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
REQUIRED_PROPERTIES = {
|
|
||||||
'virtualbox_vmname': _("Name of the VM in VirtualBox. Required."),
|
|
||||||
'virtualbox_host': _("IP address or hostname of the VirtualBox host. "
|
|
||||||
"Required.")
|
|
||||||
}
|
|
||||||
|
|
||||||
OPTIONAL_PROPERTIES = {
|
|
||||||
'virtualbox_username': _("Username for the VirtualBox host. "
|
|
||||||
"Default value is ''. Optional."),
|
|
||||||
'virtualbox_password': _("Password for 'virtualbox_username'. "
|
|
||||||
"Default value is ''. Optional."),
|
|
||||||
'virtualbox_port': _("Port on which VirtualBox web service is listening. "
|
|
||||||
"Optional."),
|
|
||||||
}
|
|
||||||
|
|
||||||
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
|
|
||||||
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
|
|
||||||
|
|
||||||
|
|
||||||
def _strip_virtualbox_from_param_name(param_name):
|
|
||||||
if param_name.startswith('virtualbox_'):
|
|
||||||
return param_name[11:]
|
|
||||||
else:
|
|
||||||
return param_name
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_driver_info(node):
|
|
||||||
"""Gets the driver specific node driver info.
|
|
||||||
|
|
||||||
This method validates whether the 'driver_info' property of the
|
|
||||||
supplied node contains the required information for this driver.
|
|
||||||
|
|
||||||
:param node: an Ironic Node object.
|
|
||||||
:returns: a dict containing information from driver_info (or where
|
|
||||||
applicable, config values).
|
|
||||||
:raises: MissingParameterValue, if some required parameter(s) are missing
|
|
||||||
in the node's driver_info.
|
|
||||||
:raises: InvalidParameterValue, if some parameter(s) have invalid value(s)
|
|
||||||
in the node's driver_info.
|
|
||||||
"""
|
|
||||||
info = node.driver_info
|
|
||||||
d_info = {}
|
|
||||||
|
|
||||||
missing_params = []
|
|
||||||
for param in REQUIRED_PROPERTIES:
|
|
||||||
try:
|
|
||||||
d_info_param_name = _strip_virtualbox_from_param_name(param)
|
|
||||||
d_info[d_info_param_name] = info[param]
|
|
||||||
except KeyError:
|
|
||||||
missing_params.append(param)
|
|
||||||
|
|
||||||
if missing_params:
|
|
||||||
msg = (_("The following parameters are missing in driver_info: %s") %
|
|
||||||
', '.join(missing_params))
|
|
||||||
raise exception.MissingParameterValue(msg)
|
|
||||||
|
|
||||||
for param in OPTIONAL_PROPERTIES:
|
|
||||||
if param in info:
|
|
||||||
d_info_param_name = _strip_virtualbox_from_param_name(param)
|
|
||||||
d_info[d_info_param_name] = info[param]
|
|
||||||
|
|
||||||
port = d_info.get('port', CONF.virtualbox.port)
|
|
||||||
d_info['port'] = utils.validate_network_port(port, 'virtualbox_port')
|
|
||||||
|
|
||||||
return d_info
|
|
||||||
|
|
||||||
|
|
||||||
def _run_virtualbox_method(node, ironic_method, vm_object_method,
|
|
||||||
*call_args, **call_kwargs):
|
|
||||||
"""Runs a method of pyremotevbox.vbox.VirtualMachine
|
|
||||||
|
|
||||||
This runs a method from pyremotevbox.vbox.VirtualMachine.
|
|
||||||
The VirtualMachine method to be invoked and the argument(s) to be
|
|
||||||
passed to it are to be provided.
|
|
||||||
|
|
||||||
:param node: an Ironic Node object.
|
|
||||||
:param ironic_method: the Ironic method which called
|
|
||||||
'_run_virtualbox_method'. This is used for logging only.
|
|
||||||
:param vm_object_method: The method on the VirtualMachine object
|
|
||||||
to be called.
|
|
||||||
:param call_args: The args to be passed to 'vm_object_method'.
|
|
||||||
:param call_kwargs: The kwargs to be passed to the 'vm_object_method'.
|
|
||||||
:returns: The value returned by 'vm_object_method'
|
|
||||||
:raises: VirtualBoxOperationFailed, if execution of 'vm_object_method'
|
|
||||||
failed.
|
|
||||||
:raises: InvalidParameterValue,
|
|
||||||
- if 'vm_object_method' is not a valid 'VirtualMachine' method.
|
|
||||||
- if some parameter(s) have invalid value(s) in the node's driver_info.
|
|
||||||
:raises: MissingParameterValue, if some required parameter(s) are missing
|
|
||||||
in the node's driver_info.
|
|
||||||
:raises: pyremotevbox.exception.VmInWrongPowerState, if operation cannot
|
|
||||||
be performed when vm is in the current power state.
|
|
||||||
"""
|
|
||||||
driver_info = _parse_driver_info(node)
|
|
||||||
try:
|
|
||||||
host = virtualbox.VirtualBoxHost(**driver_info)
|
|
||||||
vm_object = host.find_vm(driver_info['vmname'])
|
|
||||||
except virtualbox_exc.PyRemoteVBoxException as exc:
|
|
||||||
LOG.error(_LE("Failed while creating a VirtualMachine object for "
|
|
||||||
"node %(node_id)s. Error: %(error)s."),
|
|
||||||
{'node_id': node.uuid, 'error': exc})
|
|
||||||
raise exception.VirtualBoxOperationFailed(operation=vm_object_method,
|
|
||||||
error=exc)
|
|
||||||
|
|
||||||
try:
|
|
||||||
func = getattr(vm_object, vm_object_method)
|
|
||||||
except AttributeError:
|
|
||||||
error_msg = _("Invalid VirtualMachine method '%s' passed "
|
|
||||||
"to '_run_virtualbox_method'.")
|
|
||||||
raise exception.InvalidParameterValue(error_msg % vm_object_method)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return func(*call_args, **call_kwargs)
|
|
||||||
except virtualbox_exc.PyRemoteVBoxException as exc:
|
|
||||||
error_msg = _LE("'%(ironic_method)s' failed for node %(node_id)s with "
|
|
||||||
"error: %(error)s.")
|
|
||||||
LOG.error(error_msg, {'ironic_method': ironic_method,
|
|
||||||
'node_id': node.uuid,
|
|
||||||
'error': exc})
|
|
||||||
raise exception.VirtualBoxOperationFailed(operation=vm_object_method,
|
|
||||||
error=exc)
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualBoxPower(base.PowerInterface):
|
|
||||||
def get_properties(self):
|
|
||||||
return COMMON_PROPERTIES
|
|
||||||
|
|
||||||
def validate(self, task):
|
|
||||||
"""Check if node.driver_info contains the required credentials.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance.
|
|
||||||
:raises: MissingParameterValue, if some required parameter(s) are
|
|
||||||
missing in the node's driver_info.
|
|
||||||
:raises: InvalidParameterValue, if some parameter(s) have invalid
|
|
||||||
value(s) in the node's driver_info.
|
|
||||||
"""
|
|
||||||
_parse_driver_info(task.node)
|
|
||||||
|
|
||||||
def _apply_boot_device(self, task):
|
|
||||||
"""Get the target boot device and apply on the baremetal machine .
|
|
||||||
|
|
||||||
:param task: a TaskManager instance.
|
|
||||||
"""
|
|
||||||
driver_internal_info = task.node.driver_internal_info
|
|
||||||
device = driver_internal_info.pop('vbox_target_boot_device', None)
|
|
||||||
if device is not None:
|
|
||||||
task.driver.management.set_boot_device(task, device)
|
|
||||||
task.node.driver_internal_info = driver_internal_info
|
|
||||||
task.node.save()
|
|
||||||
|
|
||||||
def get_power_state(self, task):
|
|
||||||
"""Gets the current power state.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance.
|
|
||||||
:returns: one of :mod:`ironic.common.states`
|
|
||||||
:raises: MissingParameterValue, if some required parameter(s) are
|
|
||||||
missing in the node's driver_info.
|
|
||||||
:raises: InvalidParameterValue, if some parameter(s) have invalid
|
|
||||||
value(s) in the node's driver_info.
|
|
||||||
:raises: VirtualBoxOperationFailed, if error encountered from
|
|
||||||
VirtualBox operation.
|
|
||||||
"""
|
|
||||||
power_status = _run_virtualbox_method(task.node, 'get_power_state',
|
|
||||||
'get_power_status')
|
|
||||||
try:
|
|
||||||
return VIRTUALBOX_TO_IRONIC_POWER_MAPPING[power_status]
|
|
||||||
except KeyError:
|
|
||||||
msg = _LE("VirtualBox returned unknown state '%(state)s' for "
|
|
||||||
"node %(node)s")
|
|
||||||
LOG.error(msg, {'state': power_status, 'node': task.node.uuid})
|
|
||||||
return states.ERROR
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
|
||||||
def set_power_state(self, task, target_state):
|
|
||||||
"""Turn the current power state on or off.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance.
|
|
||||||
:param target_state: The desired power state POWER_ON,POWER_OFF or
|
|
||||||
REBOOT from :mod:`ironic.common.states`.
|
|
||||||
:raises: MissingParameterValue, if some required parameter(s) are
|
|
||||||
missing in the node's driver_info.
|
|
||||||
:raises: InvalidParameterValue, if some parameter(s) have invalid
|
|
||||||
value(s) in the node's driver_info OR if an invalid power state
|
|
||||||
was specified.
|
|
||||||
:raises: VirtualBoxOperationFailed, if error encountered from
|
|
||||||
VirtualBox operation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# We set boot device before power on to avoid the case that user
|
|
||||||
# shuts down the machine without calling power off method here. For
|
|
||||||
# instance, soft power off the machine from OS.
|
|
||||||
if target_state == states.POWER_OFF:
|
|
||||||
_run_virtualbox_method(task.node, 'set_power_state', 'stop')
|
|
||||||
self._apply_boot_device(task)
|
|
||||||
elif target_state == states.POWER_ON:
|
|
||||||
self._apply_boot_device(task)
|
|
||||||
_run_virtualbox_method(task.node, 'set_power_state', 'start')
|
|
||||||
elif target_state == states.REBOOT:
|
|
||||||
self.reboot(task)
|
|
||||||
else:
|
|
||||||
msg = _("'set_power_state' called with invalid power "
|
|
||||||
"state '%s'") % target_state
|
|
||||||
raise exception.InvalidParameterValue(msg)
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
|
||||||
def reboot(self, task):
|
|
||||||
"""Reboot the node.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance.
|
|
||||||
:raises: MissingParameterValue, if some required parameter(s) are
|
|
||||||
missing in the node's driver_info.
|
|
||||||
:raises: InvalidParameterValue, if some parameter(s) have invalid
|
|
||||||
value(s) in the node's driver_info.
|
|
||||||
:raises: VirtualBoxOperationFailed, if error encountered from
|
|
||||||
VirtualBox operation.
|
|
||||||
"""
|
|
||||||
_run_virtualbox_method(task.node, 'reboot', 'stop')
|
|
||||||
self._apply_boot_device(task)
|
|
||||||
_run_virtualbox_method(task.node, 'reboot', 'start')
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualBoxManagement(base.ManagementInterface):
|
|
||||||
def get_properties(self):
|
|
||||||
return COMMON_PROPERTIES
|
|
||||||
|
|
||||||
def validate(self, task):
|
|
||||||
"""Check that 'driver_info' contains required credentials.
|
|
||||||
|
|
||||||
Validates whether the 'driver_info' property of the supplied
|
|
||||||
task's node contains the required credentials information.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:raises: MissingParameterValue, if some required parameter(s) are
|
|
||||||
missing in the node's driver_info.
|
|
||||||
:raises: InvalidParameterValue, if some parameter(s) have invalid
|
|
||||||
value(s) in the node's driver_info.
|
|
||||||
"""
|
|
||||||
_parse_driver_info(task.node)
|
|
||||||
|
|
||||||
def get_supported_boot_devices(self, task):
|
|
||||||
"""Get a list of the supported boot devices.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:returns: A list with the supported boot devices defined
|
|
||||||
in :mod:`ironic.common.boot_devices`.
|
|
||||||
"""
|
|
||||||
return list(IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING.keys())
|
|
||||||
|
|
||||||
def _get_boot_device_from_hardware(self, task):
|
|
||||||
boot_dev = _run_virtualbox_method(task.node,
|
|
||||||
'get_boot_device', 'get_boot_device')
|
|
||||||
ironic_boot_dev = VIRTUALBOX_TO_IRONIC_DEVICE_MAPPING.get(boot_dev)
|
|
||||||
persistent = True
|
|
||||||
if not ironic_boot_dev:
|
|
||||||
persistent = None
|
|
||||||
msg = _LW("VirtualBox returned unknown boot "
|
|
||||||
"device '%(device)s' for node %(node)s")
|
|
||||||
LOG.warning(msg, {'device': boot_dev, 'node': task.node.uuid})
|
|
||||||
return (ironic_boot_dev, persistent)
|
|
||||||
|
|
||||||
def get_boot_device(self, task):
|
|
||||||
"""Get the current boot device for a node.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:returns: a dictionary containing:
|
|
||||||
'boot_device': one of the ironic.common.boot_devices or None
|
|
||||||
'persistent': True if boot device is persistent, False otherwise
|
|
||||||
:raises: MissingParameterValue, if some required parameter(s) are
|
|
||||||
missing in the node's driver_info.
|
|
||||||
:raises: InvalidParameterValue, if some parameter(s) have invalid
|
|
||||||
value(s) in the node's driver_info.
|
|
||||||
:raises: VirtualBoxOperationFailed, if error encountered from
|
|
||||||
VirtualBox operation.
|
|
||||||
"""
|
|
||||||
if task.driver.power.get_power_state(task) == states.POWER_OFF:
|
|
||||||
ironic_boot_dev, persistent = \
|
|
||||||
self._get_boot_device_from_hardware(task)
|
|
||||||
else:
|
|
||||||
ironic_boot_dev = task.node. \
|
|
||||||
driver_internal_info.get('vbox_target_boot_device')
|
|
||||||
if ironic_boot_dev is not None:
|
|
||||||
msg = _LW("As ironic node %(node)s is"
|
|
||||||
" powered on, we will set to boot"
|
|
||||||
" from %(device)s before next boot.")
|
|
||||||
LOG.warning(msg, {'node': task.node.uuid,
|
|
||||||
'device': ironic_boot_dev})
|
|
||||||
persistent = True
|
|
||||||
else:
|
|
||||||
# Maybe the vbox_target_boot_device is cleaned
|
|
||||||
ironic_boot_dev, persistent = \
|
|
||||||
self._get_boot_device_from_hardware(task)
|
|
||||||
return {'boot_device': ironic_boot_dev, 'persistent': persistent}
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
|
||||||
def set_boot_device(self, task, device, persistent=False):
|
|
||||||
"""Set the boot device for a node.
|
|
||||||
|
|
||||||
:param task: a task from TaskManager.
|
|
||||||
:param device: ironic.common.boot_devices
|
|
||||||
:param persistent: This argument is ignored as VirtualBox support only
|
|
||||||
persistent boot devices.
|
|
||||||
:raises: MissingParameterValue, if some required parameter(s) are
|
|
||||||
missing in the node's driver_info.
|
|
||||||
:raises: InvalidParameterValue, if some parameter(s) have invalid
|
|
||||||
value(s) in the node's driver_info.
|
|
||||||
:raises: VirtualBoxOperationFailed, if error encountered from
|
|
||||||
VirtualBox operation.
|
|
||||||
"""
|
|
||||||
# NOTE(rameshg87): VirtualBox has only persistent boot devices.
|
|
||||||
try:
|
|
||||||
boot_dev = IRONIC_TO_VIRTUALBOX_DEVICE_MAPPING[device]
|
|
||||||
except KeyError:
|
|
||||||
raise exception.InvalidParameterValue(
|
|
||||||
_("Invalid boot device %s specified.") % device)
|
|
||||||
|
|
||||||
if task.driver.power.get_power_state(task) == states.POWER_OFF:
|
|
||||||
|
|
||||||
_run_virtualbox_method(task.node, 'set_boot_device',
|
|
||||||
'set_boot_device', boot_dev)
|
|
||||||
else:
|
|
||||||
LOG.warning(_LW('Node %(node_uuid)s: As VirtualBox do not support '
|
|
||||||
'setting boot device when VM is powered on, we '
|
|
||||||
'will set booting from %(device)s when reboot '
|
|
||||||
'next time.'),
|
|
||||||
{'node_uuid': task.node.uuid, 'device': device})
|
|
||||||
# We should store target boot device in case the
|
|
||||||
# end user shutoff the baremetal machine from NOVA API.
|
|
||||||
boot_device_now = self.get_boot_device(task)['boot_device']
|
|
||||||
if device != boot_device_now:
|
|
||||||
driver_internal_info = task.node.driver_internal_info
|
|
||||||
driver_internal_info['vbox_target_boot_device'] = device
|
|
||||||
task.node.driver_internal_info = driver_internal_info
|
|
||||||
task.node.save()
|
|
||||||
|
|
||||||
def get_sensors_data(self, task):
|
|
||||||
"""Get sensors data.
|
|
||||||
|
|
||||||
:param task: a TaskManager instance.
|
|
||||||
:raises: FailedToGetSensorData when getting the sensor data fails.
|
|
||||||
:raises: FailedToParseSensorData when parsing sensor data fails.
|
|
||||||
:returns: returns a consistent format dict of sensor data grouped by
|
|
||||||
sensor type, which can be processed by Ceilometer.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
@ -39,15 +39,11 @@ from ironic.drivers.modules.irmc import inspect as irmc_inspect
|
|||||||
from ironic.drivers.modules.irmc import management as irmc_management
|
from ironic.drivers.modules.irmc import management as irmc_management
|
||||||
from ironic.drivers.modules.irmc import power as irmc_power
|
from ironic.drivers.modules.irmc import power as irmc_power
|
||||||
from ironic.drivers.modules import iscsi_deploy
|
from ironic.drivers.modules import iscsi_deploy
|
||||||
from ironic.drivers.modules.msftocs import management as msftocs_management
|
|
||||||
from ironic.drivers.modules.msftocs import power as msftocs_power
|
|
||||||
from ironic.drivers.modules import pxe
|
from ironic.drivers.modules import pxe
|
||||||
from ironic.drivers.modules import seamicro
|
|
||||||
from ironic.drivers.modules import snmp
|
from ironic.drivers.modules import snmp
|
||||||
from ironic.drivers.modules import ssh
|
from ironic.drivers.modules import ssh
|
||||||
from ironic.drivers.modules.ucs import management as ucs_mgmt
|
from ironic.drivers.modules.ucs import management as ucs_mgmt
|
||||||
from ironic.drivers.modules.ucs import power as ucs_power
|
from ironic.drivers.modules.ucs import power as ucs_power
|
||||||
from ironic.drivers.modules import virtualbox
|
|
||||||
|
|
||||||
|
|
||||||
# For backward compatibility
|
# For backward compatibility
|
||||||
@ -110,32 +106,6 @@ class PXEAndIPMINativeDriver(base.BaseDriver):
|
|||||||
self.raid = agent.AgentRAID()
|
self.raid = agent.AgentRAID()
|
||||||
|
|
||||||
|
|
||||||
class PXEAndSeaMicroDriver(base.BaseDriver):
|
|
||||||
"""PXE + SeaMicro driver.
|
|
||||||
|
|
||||||
This driver implements the `core` functionality, combining
|
|
||||||
:class:`ironic.drivers.modules.seamicro.Power` for power
|
|
||||||
on/off and reboot with
|
|
||||||
:class:`ironic.drivers.modules.iscsi_deploy.ISCSIDeploy`
|
|
||||||
for image deployment. Implementations are in those respective
|
|
||||||
classes; this class is merely the glue between them.
|
|
||||||
"""
|
|
||||||
|
|
||||||
supported = False
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if not importutils.try_import('seamicroclient'):
|
|
||||||
raise exception.DriverLoadError(
|
|
||||||
driver=self.__class__.__name__,
|
|
||||||
reason=_("Unable to import seamicroclient library"))
|
|
||||||
self.power = seamicro.Power()
|
|
||||||
self.boot = pxe.PXEBoot()
|
|
||||||
self.deploy = iscsi_deploy.ISCSIDeploy()
|
|
||||||
self.management = seamicro.Management()
|
|
||||||
self.vendor = seamicro.VendorPassthru()
|
|
||||||
self.console = seamicro.ShellinaboxConsole()
|
|
||||||
|
|
||||||
|
|
||||||
class PXEAndIloDriver(base.BaseDriver):
|
class PXEAndIloDriver(base.BaseDriver):
|
||||||
"""PXE + Ilo Driver using IloClient interface.
|
"""PXE + Ilo Driver using IloClient interface.
|
||||||
|
|
||||||
@ -206,52 +176,6 @@ class PXEAndIRMCDriver(base.BaseDriver):
|
|||||||
self.inspect = irmc_inspect.IRMCInspect()
|
self.inspect = irmc_inspect.IRMCInspect()
|
||||||
|
|
||||||
|
|
||||||
class PXEAndVirtualBoxDriver(base.BaseDriver):
|
|
||||||
"""PXE + VirtualBox driver.
|
|
||||||
|
|
||||||
NOTE: This driver is meant only for testing environments.
|
|
||||||
|
|
||||||
This driver implements the `core` functionality, combining
|
|
||||||
:class:`ironic.drivers.virtualbox.VirtualBoxPower` for power on/off and
|
|
||||||
reboot of VirtualBox virtual machines, with
|
|
||||||
:class:`ironic.drivers.modules.iscsi_deploy.ISCSIDeploy` for image
|
|
||||||
deployment. Implementations are in those respective classes;
|
|
||||||
this class is merely the glue between them.
|
|
||||||
"""
|
|
||||||
|
|
||||||
supported = False
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if not importutils.try_import('pyremotevbox'):
|
|
||||||
raise exception.DriverLoadError(
|
|
||||||
driver=self.__class__.__name__,
|
|
||||||
reason=_("Unable to import pyremotevbox library"))
|
|
||||||
self.power = virtualbox.VirtualBoxPower()
|
|
||||||
self.boot = pxe.PXEBoot()
|
|
||||||
self.deploy = iscsi_deploy.ISCSIDeploy()
|
|
||||||
self.management = virtualbox.VirtualBoxManagement()
|
|
||||||
self.raid = agent.AgentRAID()
|
|
||||||
|
|
||||||
|
|
||||||
class PXEAndMSFTOCSDriver(base.BaseDriver):
|
|
||||||
"""PXE + MSFT OCS driver.
|
|
||||||
|
|
||||||
This driver implements the `core` functionality, combining
|
|
||||||
:class:`ironic.drivers.modules.msftocs.power.MSFTOCSPower` for power on/off
|
|
||||||
and reboot with :class:`ironic.drivers.modules.iscsi_deploy.ISCSIDeploy`
|
|
||||||
for image deployment. Implementations are in those respective classes;
|
|
||||||
this class is merely the glue between them.
|
|
||||||
"""
|
|
||||||
|
|
||||||
supported = False
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.power = msftocs_power.MSFTOCSPower()
|
|
||||||
self.boot = pxe.PXEBoot()
|
|
||||||
self.deploy = iscsi_deploy.ISCSIDeploy()
|
|
||||||
self.management = msftocs_management.MSFTOCSManagement()
|
|
||||||
|
|
||||||
|
|
||||||
class PXEAndUcsDriver(base.BaseDriver):
|
class PXEAndUcsDriver(base.BaseDriver):
|
||||||
"""PXE + Cisco UCSM driver.
|
"""PXE + Cisco UCSM driver.
|
||||||
|
|
||||||
|
@ -4776,12 +4776,6 @@ class ManagerTestProperties(mgr_utils.ServiceSetUpMixin,
|
|||||||
'deploy_forces_oob_reboot']
|
'deploy_forces_oob_reboot']
|
||||||
self._check_driver_properties("fake_pxe", expected)
|
self._check_driver_properties("fake_pxe", expected)
|
||||||
|
|
||||||
def test_driver_properties_fake_seamicro(self):
|
|
||||||
expected = ['seamicro_api_endpoint', 'seamicro_password',
|
|
||||||
'seamicro_server_id', 'seamicro_username',
|
|
||||||
'seamicro_api_version', 'seamicro_terminal_port']
|
|
||||||
self._check_driver_properties("fake_seamicro", expected)
|
|
||||||
|
|
||||||
def test_driver_properties_fake_snmp(self):
|
def test_driver_properties_fake_snmp(self):
|
||||||
expected = ['snmp_driver', 'snmp_address', 'snmp_port', 'snmp_version',
|
expected = ['snmp_driver', 'snmp_address', 'snmp_port', 'snmp_version',
|
||||||
'snmp_community', 'snmp_security', 'snmp_outlet']
|
'snmp_community', 'snmp_security', 'snmp_outlet']
|
||||||
@ -4813,14 +4807,6 @@ class ManagerTestProperties(mgr_utils.ServiceSetUpMixin,
|
|||||||
'deploy_forces_oob_reboot']
|
'deploy_forces_oob_reboot']
|
||||||
self._check_driver_properties("pxe_ssh", expected)
|
self._check_driver_properties("pxe_ssh", expected)
|
||||||
|
|
||||||
def test_driver_properties_pxe_seamicro(self):
|
|
||||||
expected = ['deploy_kernel', 'deploy_ramdisk',
|
|
||||||
'seamicro_api_endpoint', 'seamicro_password',
|
|
||||||
'seamicro_server_id', 'seamicro_username',
|
|
||||||
'seamicro_api_version', 'seamicro_terminal_port',
|
|
||||||
'deploy_forces_oob_reboot']
|
|
||||||
self._check_driver_properties("pxe_seamicro", expected)
|
|
||||||
|
|
||||||
def test_driver_properties_pxe_snmp(self):
|
def test_driver_properties_pxe_snmp(self):
|
||||||
expected = ['deploy_kernel', 'deploy_ramdisk',
|
expected = ['deploy_kernel', 'deploy_ramdisk',
|
||||||
'snmp_driver', 'snmp_address', 'snmp_port', 'snmp_version',
|
'snmp_driver', 'snmp_address', 'snmp_port', 'snmp_version',
|
||||||
|
@ -89,15 +89,6 @@ def get_test_pxe_instance_info():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_test_seamicro_info():
|
|
||||||
return {
|
|
||||||
"seamicro_api_endpoint": "http://1.2.3.4",
|
|
||||||
"seamicro_username": "admin",
|
|
||||||
"seamicro_password": "fake",
|
|
||||||
"seamicro_server_id": "0/0",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_test_ilo_info():
|
def get_test_ilo_info():
|
||||||
return {
|
return {
|
||||||
"ilo_address": "1.2.3.4",
|
"ilo_address": "1.2.3.4",
|
||||||
@ -127,15 +118,6 @@ def get_test_irmc_info():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_test_msftocs_info():
|
|
||||||
return {
|
|
||||||
"msftocs_base_url": "http://fakehost:8000",
|
|
||||||
"msftocs_username": "admin",
|
|
||||||
"msftocs_password": "fake",
|
|
||||||
"msftocs_blade_id": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_test_agent_instance_info():
|
def get_test_agent_instance_info():
|
||||||
return {
|
return {
|
||||||
'image_source': 'fake-image',
|
'image_source': 'fake-image',
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
# Copyright 2015 Cloudbase Solutions Srl
|
|
||||||
# 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 MSFT OCS common functions
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.conductor import task_manager
|
|
||||||
from ironic.drivers.modules.msftocs import common as msftocs_common
|
|
||||||
from ironic.tests.unit.conductor import mgr_utils
|
|
||||||
from ironic.tests.unit.db import base as db_base
|
|
||||||
from ironic.tests.unit.db import utils as db_utils
|
|
||||||
from ironic.tests.unit.objects import utils as obj_utils
|
|
||||||
|
|
||||||
INFO_DICT = db_utils.get_test_msftocs_info()
|
|
||||||
|
|
||||||
|
|
||||||
class MSFTOCSCommonTestCase(db_base.DbTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(MSFTOCSCommonTestCase, self).setUp()
|
|
||||||
mgr_utils.mock_the_extension_manager(driver='fake_msftocs')
|
|
||||||
self.info = INFO_DICT
|
|
||||||
self.node = obj_utils.create_test_node(self.context,
|
|
||||||
driver='fake_msftocs',
|
|
||||||
driver_info=self.info)
|
|
||||||
|
|
||||||
def test_get_client_info(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
driver_info = task.node.driver_info
|
|
||||||
(client, blade_id) = msftocs_common.get_client_info(driver_info)
|
|
||||||
|
|
||||||
self.assertEqual(driver_info['msftocs_base_url'], client._base_url)
|
|
||||||
self.assertEqual(driver_info['msftocs_username'], client._username)
|
|
||||||
self.assertEqual(driver_info['msftocs_password'], client._password)
|
|
||||||
self.assertEqual(driver_info['msftocs_blade_id'], blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, '_is_valid_url', autospec=True)
|
|
||||||
def test_parse_driver_info(self, mock_is_valid_url):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
msftocs_common.parse_driver_info(task.node)
|
|
||||||
mock_is_valid_url.assert_called_once_with(
|
|
||||||
task.node.driver_info['msftocs_base_url'])
|
|
||||||
|
|
||||||
def test_parse_driver_info_fail_missing_param(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
del task.node.driver_info['msftocs_base_url']
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
msftocs_common.parse_driver_info,
|
|
||||||
task.node)
|
|
||||||
|
|
||||||
def test_parse_driver_info_fail_bad_url(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
task.node.driver_info['msftocs_base_url'] = "bad-url"
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
msftocs_common.parse_driver_info,
|
|
||||||
task.node)
|
|
||||||
|
|
||||||
def test_parse_driver_info_fail_bad_blade_id_type(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
task.node.driver_info['msftocs_blade_id'] = "bad-blade-id"
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
msftocs_common.parse_driver_info,
|
|
||||||
task.node)
|
|
||||||
|
|
||||||
def test_parse_driver_info_fail_bad_blade_id_value(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
task.node.driver_info['msftocs_blade_id'] = 0
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
msftocs_common.parse_driver_info,
|
|
||||||
task.node)
|
|
||||||
|
|
||||||
def test__is_valid_url(self):
|
|
||||||
self.assertIs(True, msftocs_common._is_valid_url("http://fake.com"))
|
|
||||||
self.assertIs(
|
|
||||||
True, msftocs_common._is_valid_url("http://www.fake.com"))
|
|
||||||
self.assertIs(True, msftocs_common._is_valid_url("http://FAKE.com"))
|
|
||||||
self.assertIs(True, msftocs_common._is_valid_url("http://fake"))
|
|
||||||
self.assertIs(
|
|
||||||
True, msftocs_common._is_valid_url("http://fake.com/blah"))
|
|
||||||
self.assertIs(True, msftocs_common._is_valid_url("http://localhost"))
|
|
||||||
self.assertIs(True, msftocs_common._is_valid_url("https://fake.com"))
|
|
||||||
self.assertIs(True, msftocs_common._is_valid_url("http://10.0.0.1"))
|
|
||||||
self.assertIs(False, msftocs_common._is_valid_url("bad-url"))
|
|
||||||
self.assertIs(False, msftocs_common._is_valid_url("http://.bad-url"))
|
|
||||||
self.assertIs(False, msftocs_common._is_valid_url("http://bad-url$"))
|
|
||||||
self.assertIs(False, msftocs_common._is_valid_url("http://$bad-url"))
|
|
||||||
self.assertIs(False, msftocs_common._is_valid_url("http://bad$url"))
|
|
||||||
self.assertIs(False, msftocs_common._is_valid_url(None))
|
|
||||||
self.assertIs(False, msftocs_common._is_valid_url(0))
|
|
@ -1,132 +0,0 @@
|
|||||||
# Copyright 2015 Cloudbase Solutions Srl
|
|
||||||
# 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 MSFT OCS ManagementInterface
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.conductor import task_manager
|
|
||||||
from ironic.drivers.modules.msftocs import common as msftocs_common
|
|
||||||
from ironic.drivers.modules.msftocs import msftocsclient
|
|
||||||
from ironic.drivers import utils as drivers_utils
|
|
||||||
from ironic.tests.unit.conductor import mgr_utils
|
|
||||||
from ironic.tests.unit.db import base as db_base
|
|
||||||
from ironic.tests.unit.db import utils as db_utils
|
|
||||||
from ironic.tests.unit.objects import utils as obj_utils
|
|
||||||
|
|
||||||
INFO_DICT = db_utils.get_test_msftocs_info()
|
|
||||||
|
|
||||||
|
|
||||||
class MSFTOCSManagementTestCase(db_base.DbTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(MSFTOCSManagementTestCase, self).setUp()
|
|
||||||
mgr_utils.mock_the_extension_manager(driver='fake_msftocs')
|
|
||||||
self.info = INFO_DICT
|
|
||||||
self.node = obj_utils.create_test_node(self.context,
|
|
||||||
driver='fake_msftocs',
|
|
||||||
driver_info=self.info)
|
|
||||||
|
|
||||||
def test_get_properties(self):
|
|
||||||
expected = msftocs_common.REQUIRED_PROPERTIES
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
self.assertEqual(expected, task.driver.get_properties())
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True)
|
|
||||||
def test_validate(self, mock_drvinfo):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
task.driver.power.validate(task)
|
|
||||||
mock_drvinfo.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True)
|
|
||||||
def test_validate_fail(self, mock_drvinfo):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
mock_drvinfo.side_effect = exception.InvalidParameterValue('x')
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.power.validate,
|
|
||||||
task)
|
|
||||||
|
|
||||||
def test_get_supported_boot_devices(self):
|
|
||||||
expected = [boot_devices.PXE, boot_devices.DISK, boot_devices.BIOS]
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
self.assertEqual(
|
|
||||||
sorted(expected),
|
|
||||||
sorted(task.driver.management.
|
|
||||||
get_supported_boot_devices(task)))
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
|
|
||||||
def _test_set_boot_device_one_time(self, persistent, uefi,
|
|
||||||
mock_gci):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
|
|
||||||
blade_id = task.node.driver_info['msftocs_blade_id']
|
|
||||||
mock_gci.return_value = (mock_c, blade_id)
|
|
||||||
|
|
||||||
if uefi:
|
|
||||||
drivers_utils.add_node_capability(task, 'boot_mode', 'uefi')
|
|
||||||
|
|
||||||
task.driver.management.set_boot_device(
|
|
||||||
task, boot_devices.PXE, persistent)
|
|
||||||
|
|
||||||
mock_gci.assert_called_once_with(task.node.driver_info)
|
|
||||||
mock_c.set_next_boot.assert_called_once_with(
|
|
||||||
blade_id, msftocsclient.BOOT_TYPE_FORCE_PXE, persistent, uefi)
|
|
||||||
|
|
||||||
def test_set_boot_device_one_time(self):
|
|
||||||
self._test_set_boot_device_one_time(False, False)
|
|
||||||
|
|
||||||
def test_set_boot_device_persistent(self):
|
|
||||||
self._test_set_boot_device_one_time(True, False)
|
|
||||||
|
|
||||||
def test_set_boot_device_uefi(self):
|
|
||||||
self._test_set_boot_device_one_time(True, True)
|
|
||||||
|
|
||||||
def test_set_boot_device_fail(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.management.set_boot_device,
|
|
||||||
task, 'fake-device')
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
|
|
||||||
def test_get_boot_device(self, mock_gci):
|
|
||||||
expected = {'boot_device': boot_devices.DISK, 'persistent': None}
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
|
|
||||||
blade_id = task.node.driver_info['msftocs_blade_id']
|
|
||||||
mock_gci.return_value = (mock_c, blade_id)
|
|
||||||
force_hdd = msftocsclient.BOOT_TYPE_FORCE_DEFAULT_HDD
|
|
||||||
mock_c.get_next_boot.return_value = force_hdd
|
|
||||||
|
|
||||||
self.assertEqual(expected,
|
|
||||||
task.driver.management.get_boot_device(task))
|
|
||||||
mock_gci.assert_called_once_with(task.node.driver_info)
|
|
||||||
mock_c.get_next_boot.assert_called_once_with(blade_id)
|
|
||||||
|
|
||||||
def test_get_sensor_data(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
task.driver.management.get_sensors_data,
|
|
||||||
task)
|
|
@ -1,182 +0,0 @@
|
|||||||
# Copyright 2015 Cloudbase Solutions Srl
|
|
||||||
# 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 MSFT OCS REST API client
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mock
|
|
||||||
import requests
|
|
||||||
from requests import exceptions as requests_exceptions
|
|
||||||
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.drivers.modules.msftocs import msftocsclient
|
|
||||||
from ironic.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
FAKE_BOOT_RESPONSE = (
|
|
||||||
'<BootResponse xmlns="%s" '
|
|
||||||
'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">'
|
|
||||||
'<completionCode>Success</completionCode>'
|
|
||||||
'<apiVersion>1</apiVersion>'
|
|
||||||
'<statusDescription>Success</statusDescription>'
|
|
||||||
'<bladeNumber>1</bladeNumber>'
|
|
||||||
'<nextBoot>ForcePxe</nextBoot>'
|
|
||||||
'</BootResponse>') % msftocsclient.WCSNS
|
|
||||||
|
|
||||||
FAKE_BLADE_RESPONSE = (
|
|
||||||
'<BladeResponse xmlns="%s" '
|
|
||||||
'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">'
|
|
||||||
'<completionCode>Success</completionCode>'
|
|
||||||
'<apiVersion>1</apiVersion>'
|
|
||||||
'<statusDescription/>'
|
|
||||||
'<bladeNumber>1</bladeNumber>'
|
|
||||||
'</BladeResponse>') % msftocsclient.WCSNS
|
|
||||||
|
|
||||||
FAKE_POWER_STATE_RESPONSE = (
|
|
||||||
'<PowerStateResponse xmlns="%s" '
|
|
||||||
'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">'
|
|
||||||
'<completionCode>Success</completionCode>'
|
|
||||||
'<apiVersion>1</apiVersion>'
|
|
||||||
'<statusDescription>Blade Power is On, firmware decompressed'
|
|
||||||
'</statusDescription>'
|
|
||||||
'<bladeNumber>1</bladeNumber>'
|
|
||||||
'<Decompression>0</Decompression>'
|
|
||||||
'<powerState>ON</powerState>'
|
|
||||||
'</PowerStateResponse>') % msftocsclient.WCSNS
|
|
||||||
|
|
||||||
FAKE_BLADE_STATE_RESPONSE = (
|
|
||||||
'<BladeStateResponse xmlns="%s" '
|
|
||||||
'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">'
|
|
||||||
'<completionCode>Success</completionCode>'
|
|
||||||
'<apiVersion>1</apiVersion>'
|
|
||||||
'<statusDescription/>'
|
|
||||||
'<bladeNumber>1</bladeNumber>'
|
|
||||||
'<bladeState>ON</bladeState>'
|
|
||||||
'</BladeStateResponse>') % msftocsclient.WCSNS
|
|
||||||
|
|
||||||
|
|
||||||
class MSFTOCSClientApiTestCase(base.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(MSFTOCSClientApiTestCase, self).setUp()
|
|
||||||
self._fake_base_url = "http://fakehost:8000"
|
|
||||||
self._fake_username = "admin"
|
|
||||||
self._fake_password = 'fake'
|
|
||||||
self._fake_blade_id = 1
|
|
||||||
self._client = msftocsclient.MSFTOCSClientApi(
|
|
||||||
self._fake_base_url, self._fake_username, self._fake_password)
|
|
||||||
|
|
||||||
@mock.patch.object(requests, 'get', autospec=True)
|
|
||||||
def test__exec_cmd(self, mock_get):
|
|
||||||
fake_response_text = 'fake_response_text'
|
|
||||||
fake_rel_url = 'fake_rel_url'
|
|
||||||
mock_get.return_value.text = 'fake_response_text'
|
|
||||||
|
|
||||||
self.assertEqual(fake_response_text,
|
|
||||||
self._client._exec_cmd(fake_rel_url))
|
|
||||||
mock_get.assert_called_once_with(
|
|
||||||
self._fake_base_url + "/" + fake_rel_url, auth=mock.ANY)
|
|
||||||
|
|
||||||
@mock.patch.object(requests, 'get', autospec=True)
|
|
||||||
def test__exec_cmd_http_get_fail(self, mock_get):
|
|
||||||
fake_rel_url = 'fake_rel_url'
|
|
||||||
mock_get.side_effect = requests_exceptions.ConnectionError('x')
|
|
||||||
|
|
||||||
self.assertRaises(exception.MSFTOCSClientApiException,
|
|
||||||
self._client._exec_cmd,
|
|
||||||
fake_rel_url)
|
|
||||||
mock_get.assert_called_once_with(
|
|
||||||
self._fake_base_url + "/" + fake_rel_url, auth=mock.ANY)
|
|
||||||
|
|
||||||
def test__check_completion_code(self):
|
|
||||||
et = self._client._check_completion_code(FAKE_BOOT_RESPONSE)
|
|
||||||
self.assertEqual('{%s}BootResponse' % msftocsclient.WCSNS, et.tag)
|
|
||||||
|
|
||||||
def test__check_completion_code_fail(self):
|
|
||||||
self.assertRaises(exception.MSFTOCSClientApiException,
|
|
||||||
self._client._check_completion_code,
|
|
||||||
'<fake xmlns="%s"></fake>' % msftocsclient.WCSNS)
|
|
||||||
|
|
||||||
def test__check_completion_with_bad_completion_code_fail(self):
|
|
||||||
self.assertRaises(exception.MSFTOCSClientApiException,
|
|
||||||
self._client._check_completion_code,
|
|
||||||
'<fake xmlns="%s">'
|
|
||||||
'<completionCode>Fail</completionCode>'
|
|
||||||
'</fake>' % msftocsclient.WCSNS)
|
|
||||||
|
|
||||||
def test__check_completion_code_xml_parsing_fail(self):
|
|
||||||
self.assertRaises(exception.MSFTOCSClientApiException,
|
|
||||||
self._client._check_completion_code,
|
|
||||||
'bad_xml')
|
|
||||||
|
|
||||||
@mock.patch.object(
|
|
||||||
msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
|
|
||||||
def test_get_blade_state(self, mock_exec_cmd):
|
|
||||||
mock_exec_cmd.return_value = FAKE_BLADE_STATE_RESPONSE
|
|
||||||
self.assertEqual(
|
|
||||||
msftocsclient.POWER_STATUS_ON,
|
|
||||||
self._client.get_blade_state(self._fake_blade_id))
|
|
||||||
mock_exec_cmd.assert_called_once_with(
|
|
||||||
self._client, "GetBladeState?bladeId=%d" % self._fake_blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(
|
|
||||||
msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
|
|
||||||
def test_set_blade_on(self, mock_exec_cmd):
|
|
||||||
mock_exec_cmd.return_value = FAKE_BLADE_RESPONSE
|
|
||||||
self._client.set_blade_on(self._fake_blade_id)
|
|
||||||
mock_exec_cmd.assert_called_once_with(
|
|
||||||
self._client, "SetBladeOn?bladeId=%d" % self._fake_blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(
|
|
||||||
msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
|
|
||||||
def test_set_blade_off(self, mock_exec_cmd):
|
|
||||||
mock_exec_cmd.return_value = FAKE_BLADE_RESPONSE
|
|
||||||
self._client.set_blade_off(self._fake_blade_id)
|
|
||||||
mock_exec_cmd.assert_called_once_with(
|
|
||||||
self._client, "SetBladeOff?bladeId=%d" % self._fake_blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(
|
|
||||||
msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
|
|
||||||
def test_set_blade_power_cycle(self, mock_exec_cmd):
|
|
||||||
mock_exec_cmd.return_value = FAKE_BLADE_RESPONSE
|
|
||||||
self._client.set_blade_power_cycle(self._fake_blade_id)
|
|
||||||
mock_exec_cmd.assert_called_once_with(
|
|
||||||
self._client,
|
|
||||||
"SetBladeActivePowerCycle?bladeId=%d&offTime=0" %
|
|
||||||
self._fake_blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(
|
|
||||||
msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
|
|
||||||
def test_get_next_boot(self, mock_exec_cmd):
|
|
||||||
mock_exec_cmd.return_value = FAKE_BOOT_RESPONSE
|
|
||||||
self.assertEqual(
|
|
||||||
msftocsclient.BOOT_TYPE_FORCE_PXE,
|
|
||||||
self._client.get_next_boot(self._fake_blade_id))
|
|
||||||
mock_exec_cmd.assert_called_once_with(
|
|
||||||
self._client, "GetNextBoot?bladeId=%d" % self._fake_blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(
|
|
||||||
msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
|
|
||||||
def test_set_next_boot(self, mock_exec_cmd):
|
|
||||||
mock_exec_cmd.return_value = FAKE_BOOT_RESPONSE
|
|
||||||
self._client.set_next_boot(self._fake_blade_id,
|
|
||||||
msftocsclient.BOOT_TYPE_FORCE_PXE)
|
|
||||||
mock_exec_cmd.assert_called_once_with(
|
|
||||||
self._client,
|
|
||||||
"SetNextBoot?bladeId=%(blade_id)d&bootType=%(boot_type)d&"
|
|
||||||
"uefi=%(uefi)s&persistent=%(persistent)s" %
|
|
||||||
{"blade_id": self._fake_blade_id,
|
|
||||||
"boot_type": msftocsclient.BOOT_TYPE_FORCE_PXE,
|
|
||||||
"uefi": "true", "persistent": "true"})
|
|
@ -1,163 +0,0 @@
|
|||||||
# Copyright 2015 Cloudbase Solutions Srl
|
|
||||||
# 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 MSFT OCS PowerInterface
|
|
||||||
"""
|
|
||||||
|
|
||||||
import mock
|
|
||||||
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common import states
|
|
||||||
from ironic.conductor import task_manager
|
|
||||||
from ironic.drivers.modules.msftocs import common as msftocs_common
|
|
||||||
from ironic.drivers.modules.msftocs import msftocsclient
|
|
||||||
from ironic.tests.unit.conductor import mgr_utils
|
|
||||||
from ironic.tests.unit.db import base as db_base
|
|
||||||
from ironic.tests.unit.db import utils as db_utils
|
|
||||||
from ironic.tests.unit.objects import utils as obj_utils
|
|
||||||
|
|
||||||
INFO_DICT = db_utils.get_test_msftocs_info()
|
|
||||||
|
|
||||||
|
|
||||||
class MSFTOCSPowerTestCase(db_base.DbTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(MSFTOCSPowerTestCase, self).setUp()
|
|
||||||
mgr_utils.mock_the_extension_manager(driver='fake_msftocs')
|
|
||||||
self.info = INFO_DICT
|
|
||||||
self.node = obj_utils.create_test_node(self.context,
|
|
||||||
driver='fake_msftocs',
|
|
||||||
driver_info=self.info)
|
|
||||||
|
|
||||||
def test_get_properties(self):
|
|
||||||
expected = msftocs_common.REQUIRED_PROPERTIES
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
self.assertEqual(expected, task.driver.get_properties())
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True)
|
|
||||||
def test_validate(self, mock_drvinfo):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
task.driver.power.validate(task)
|
|
||||||
mock_drvinfo.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True)
|
|
||||||
def test_validate_fail(self, mock_drvinfo):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
mock_drvinfo.side_effect = exception.InvalidParameterValue('x')
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.power.validate,
|
|
||||||
task)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
|
|
||||||
def test_get_power_state(self, mock_gci):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
|
|
||||||
blade_id = task.node.driver_info['msftocs_blade_id']
|
|
||||||
mock_gci.return_value = (mock_c, blade_id)
|
|
||||||
mock_c.get_blade_state.return_value = msftocsclient.POWER_STATUS_ON
|
|
||||||
|
|
||||||
self.assertEqual(states.POWER_ON,
|
|
||||||
task.driver.power.get_power_state(task))
|
|
||||||
mock_gci.assert_called_once_with(task.node.driver_info)
|
|
||||||
mock_c.get_blade_state.assert_called_once_with(blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
|
|
||||||
def test_set_power_state_on(self, mock_gci):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
|
|
||||||
blade_id = task.node.driver_info['msftocs_blade_id']
|
|
||||||
mock_gci.return_value = (mock_c, blade_id)
|
|
||||||
|
|
||||||
task.driver.power.set_power_state(task, states.POWER_ON)
|
|
||||||
mock_gci.assert_called_once_with(task.node.driver_info)
|
|
||||||
mock_c.set_blade_on.assert_called_once_with(blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
|
|
||||||
def test_set_power_state_off(self, mock_gci):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
|
|
||||||
blade_id = task.node.driver_info['msftocs_blade_id']
|
|
||||||
mock_gci.return_value = (mock_c, blade_id)
|
|
||||||
|
|
||||||
task.driver.power.set_power_state(task, states.POWER_OFF)
|
|
||||||
mock_gci.assert_called_once_with(task.node.driver_info)
|
|
||||||
mock_c.set_blade_off.assert_called_once_with(blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
|
|
||||||
def test_set_power_state_blade_on_fail(self, mock_gci):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
|
|
||||||
blade_id = task.node.driver_info['msftocs_blade_id']
|
|
||||||
mock_gci.return_value = (mock_c, blade_id)
|
|
||||||
|
|
||||||
ex = exception.MSFTOCSClientApiException('x')
|
|
||||||
mock_c.set_blade_on.side_effect = ex
|
|
||||||
|
|
||||||
pstate = states.POWER_ON
|
|
||||||
self.assertRaises(exception.PowerStateFailure,
|
|
||||||
task.driver.power.set_power_state,
|
|
||||||
task, pstate)
|
|
||||||
mock_gci.assert_called_once_with(task.node.driver_info)
|
|
||||||
mock_c.set_blade_on.assert_called_once_with(blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
|
|
||||||
def test_set_power_state_invalid_parameter_fail(self, mock_gci):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
|
|
||||||
blade_id = task.node.driver_info['msftocs_blade_id']
|
|
||||||
mock_gci.return_value = (mock_c, blade_id)
|
|
||||||
|
|
||||||
pstate = states.ERROR
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.power.set_power_state,
|
|
||||||
task, pstate)
|
|
||||||
mock_gci.assert_called_once_with(task.node.driver_info)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
|
|
||||||
def test_reboot(self, mock_gci):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
|
|
||||||
blade_id = task.node.driver_info['msftocs_blade_id']
|
|
||||||
mock_gci.return_value = (mock_c, blade_id)
|
|
||||||
|
|
||||||
task.driver.power.reboot(task)
|
|
||||||
mock_gci.assert_called_once_with(task.node.driver_info)
|
|
||||||
mock_c.set_blade_power_cycle.assert_called_once_with(blade_id)
|
|
||||||
|
|
||||||
@mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
|
|
||||||
def test_reboot_fail(self, mock_gci):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
|
|
||||||
blade_id = task.node.driver_info['msftocs_blade_id']
|
|
||||||
mock_gci.return_value = (mock_c, blade_id)
|
|
||||||
|
|
||||||
ex = exception.MSFTOCSClientApiException('x')
|
|
||||||
mock_c.set_blade_power_cycle.side_effect = ex
|
|
||||||
|
|
||||||
self.assertRaises(exception.PowerStateFailure,
|
|
||||||
task.driver.power.reboot,
|
|
||||||
task)
|
|
||||||
mock_gci.assert_called_once_with(task.node.driver_info)
|
|
||||||
mock_c.set_blade_power_cycle.assert_called_once_with(blade_id)
|
|
@ -1,689 +0,0 @@
|
|||||||
# 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 SeaMicro driver."""
|
|
||||||
|
|
||||||
|
|
||||||
import mock
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
from seamicroclient import client as seamicro_client
|
|
||||||
from seamicroclient import exceptions as seamicro_client_exception
|
|
||||||
from six.moves import http_client
|
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
|
||||||
from ironic.common import driver_factory
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common import states
|
|
||||||
from ironic.conductor import task_manager
|
|
||||||
from ironic.drivers.modules import console_utils
|
|
||||||
from ironic.drivers.modules import seamicro
|
|
||||||
from ironic.tests.unit.conductor import mgr_utils
|
|
||||||
from ironic.tests.unit.db import base as db_base
|
|
||||||
from ironic.tests.unit.db import utils as db_utils
|
|
||||||
from ironic.tests.unit.objects import utils as obj_utils
|
|
||||||
|
|
||||||
INFO_DICT = db_utils.get_test_seamicro_info()
|
|
||||||
|
|
||||||
|
|
||||||
class Fake_Server(object):
|
|
||||||
def __init__(self, active=False, *args, **kwargs):
|
|
||||||
self.active = active
|
|
||||||
self.nic = {'0': {'untaggedVlan': ''}}
|
|
||||||
|
|
||||||
def power_on(self):
|
|
||||||
self.active = True
|
|
||||||
|
|
||||||
def power_off(self, force=False):
|
|
||||||
self.active = False
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
self.active = True
|
|
||||||
|
|
||||||
def set_untagged_vlan(self, vlan_id):
|
|
||||||
return
|
|
||||||
|
|
||||||
def attach_volume(self, volume_id):
|
|
||||||
return
|
|
||||||
|
|
||||||
def detach_volume(self):
|
|
||||||
return
|
|
||||||
|
|
||||||
def set_boot_order(self, boot_order):
|
|
||||||
return
|
|
||||||
|
|
||||||
def refresh(self, wait=0):
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Fake_Volume(object):
|
|
||||||
def __init__(self, id=None, *args, **kwargs):
|
|
||||||
if id is None:
|
|
||||||
self.id = "%s/%s/%s" % ("0", "ironic-p6-6",
|
|
||||||
uuidutils.generate_uuid())
|
|
||||||
else:
|
|
||||||
self.id = id
|
|
||||||
|
|
||||||
|
|
||||||
class Fake_Pool(object):
|
|
||||||
def __init__(self, freeSize=None, *args, **kwargs):
|
|
||||||
self.freeSize = freeSize
|
|
||||||
|
|
||||||
|
|
||||||
class SeaMicroValidateParametersTestCase(db_base.DbTestCase):
|
|
||||||
|
|
||||||
def test__parse_driver_info_good(self):
|
|
||||||
# make sure we get back the expected things
|
|
||||||
node = obj_utils.get_test_node(
|
|
||||||
self.context,
|
|
||||||
driver='fake_seamicro',
|
|
||||||
driver_info=INFO_DICT)
|
|
||||||
info = seamicro._parse_driver_info(node)
|
|
||||||
self.assertEqual('http://1.2.3.4', info['api_endpoint'])
|
|
||||||
self.assertEqual('admin', info['username'])
|
|
||||||
self.assertEqual('fake', info['password'])
|
|
||||||
self.assertEqual('0/0', info['server_id'])
|
|
||||||
self.assertEqual('1be26c0b-03f2-4d2e-ae87-c02d7f33c123',
|
|
||||||
info['uuid'])
|
|
||||||
|
|
||||||
def test__parse_driver_info_missing_api_endpoint(self):
|
|
||||||
# make sure error is raised when info is missing
|
|
||||||
info = dict(INFO_DICT)
|
|
||||||
del info['seamicro_api_endpoint']
|
|
||||||
node = obj_utils.get_test_node(self.context, driver_info=info)
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
seamicro._parse_driver_info,
|
|
||||||
node)
|
|
||||||
|
|
||||||
def test__parse_driver_info_missing_username(self):
|
|
||||||
# make sure error is raised when info is missing
|
|
||||||
info = dict(INFO_DICT)
|
|
||||||
del info['seamicro_username']
|
|
||||||
node = obj_utils.get_test_node(self.context, driver_info=info)
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
seamicro._parse_driver_info,
|
|
||||||
node)
|
|
||||||
|
|
||||||
def test__parse_driver_info_missing_password(self):
|
|
||||||
# make sure error is raised when info is missing
|
|
||||||
info = dict(INFO_DICT)
|
|
||||||
del info['seamicro_password']
|
|
||||||
node = obj_utils.get_test_node(self.context, driver_info=info)
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
seamicro._parse_driver_info,
|
|
||||||
node)
|
|
||||||
|
|
||||||
def test__parse_driver_info_missing_server_id(self):
|
|
||||||
# make sure error is raised when info is missing
|
|
||||||
info = dict(INFO_DICT)
|
|
||||||
del info['seamicro_server_id']
|
|
||||||
node = obj_utils.get_test_node(self.context, driver_info=info)
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
seamicro._parse_driver_info,
|
|
||||||
node)
|
|
||||||
|
|
||||||
def test__parse_driver_info_empty_terminal_port(self):
|
|
||||||
info = dict(INFO_DICT)
|
|
||||||
info['seamicro_terminal_port'] = ''
|
|
||||||
node = obj_utils.get_test_node(self.context, driver_info=info)
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
seamicro._parse_driver_info,
|
|
||||||
node)
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch('eventlet.greenthread.sleep', lambda n: None)
|
|
||||||
class SeaMicroPrivateMethodsTestCase(db_base.DbTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(SeaMicroPrivateMethodsTestCase, self).setUp()
|
|
||||||
n = {
|
|
||||||
'driver': 'fake_seamicro',
|
|
||||||
'driver_info': INFO_DICT
|
|
||||||
}
|
|
||||||
self.node = obj_utils.create_test_node(self.context, **n)
|
|
||||||
self.Server = Fake_Server
|
|
||||||
self.Volume = Fake_Volume
|
|
||||||
self.Pool = Fake_Pool
|
|
||||||
self.config(action_timeout=0, group='seamicro')
|
|
||||||
self.config(max_retry=2, group='seamicro')
|
|
||||||
|
|
||||||
self.info = seamicro._parse_driver_info(self.node)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro_client, "Client", autospec=True)
|
|
||||||
def test__get_client(self, mock_client):
|
|
||||||
args = {'username': self.info['username'],
|
|
||||||
'password': self.info['password'],
|
|
||||||
'auth_url': self.info['api_endpoint']}
|
|
||||||
seamicro._get_client(**self.info)
|
|
||||||
mock_client.assert_called_once_with(self.info['api_version'], **args)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro_client, "Client", autospec=True)
|
|
||||||
def test__get_client_fail(self, mock_client):
|
|
||||||
args = {'username': self.info['username'],
|
|
||||||
'password': self.info['password'],
|
|
||||||
'auth_url': self.info['api_endpoint']}
|
|
||||||
mock_client.side_effect = seamicro_client_exception.UnsupportedVersion
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
seamicro._get_client,
|
|
||||||
**self.info)
|
|
||||||
mock_client.assert_called_once_with(self.info['api_version'], **args)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_server", autospec=True)
|
|
||||||
def test__get_power_status_on(self, mock_get_server):
|
|
||||||
mock_get_server.return_value = self.Server(active=True)
|
|
||||||
pstate = seamicro._get_power_status(self.node)
|
|
||||||
self.assertEqual(states.POWER_ON, pstate)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_server", autospec=True)
|
|
||||||
def test__get_power_status_off(self, mock_get_server):
|
|
||||||
mock_get_server.return_value = self.Server(active=False)
|
|
||||||
pstate = seamicro._get_power_status(self.node)
|
|
||||||
self.assertEqual(states.POWER_OFF, pstate)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_server", autospec=True)
|
|
||||||
def test__get_power_status_error(self, mock_get_server):
|
|
||||||
mock_get_server.return_value = self.Server(active=None)
|
|
||||||
pstate = seamicro._get_power_status(self.node)
|
|
||||||
self.assertEqual(states.ERROR, pstate)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_server", autospec=True)
|
|
||||||
def test__power_on_good(self, mock_get_server):
|
|
||||||
mock_get_server.return_value = self.Server(active=False)
|
|
||||||
pstate = seamicro._power_on(self.node)
|
|
||||||
self.assertEqual(states.POWER_ON, pstate)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_server", autospec=True)
|
|
||||||
def test__power_on_fail(self, mock_get_server):
|
|
||||||
def fake_power_on():
|
|
||||||
return
|
|
||||||
|
|
||||||
server = self.Server(active=False)
|
|
||||||
server.power_on = fake_power_on
|
|
||||||
mock_get_server.return_value = server
|
|
||||||
pstate = seamicro._power_on(self.node)
|
|
||||||
self.assertEqual(states.ERROR, pstate)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_server", autospec=True)
|
|
||||||
def test__power_off_good(self, mock_get_server):
|
|
||||||
mock_get_server.return_value = self.Server(active=True)
|
|
||||||
pstate = seamicro._power_off(self.node)
|
|
||||||
self.assertEqual(states.POWER_OFF, pstate)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_server", autospec=True)
|
|
||||||
def test__power_off_fail(self, mock_get_server):
|
|
||||||
def fake_power_off():
|
|
||||||
return
|
|
||||||
server = self.Server(active=True)
|
|
||||||
server.power_off = fake_power_off
|
|
||||||
mock_get_server.return_value = server
|
|
||||||
pstate = seamicro._power_off(self.node)
|
|
||||||
self.assertEqual(states.ERROR, pstate)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_server", autospec=True)
|
|
||||||
def test__reboot_good(self, mock_get_server):
|
|
||||||
mock_get_server.return_value = self.Server(active=True)
|
|
||||||
pstate = seamicro._reboot(self.node)
|
|
||||||
self.assertEqual(states.POWER_ON, pstate)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_server", autospec=True)
|
|
||||||
def test__reboot_fail(self, mock_get_server):
|
|
||||||
def fake_reboot():
|
|
||||||
return
|
|
||||||
server = self.Server(active=False)
|
|
||||||
server.reset = fake_reboot
|
|
||||||
mock_get_server.return_value = server
|
|
||||||
pstate = seamicro._reboot(self.node)
|
|
||||||
self.assertEqual(states.ERROR, pstate)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_volume", autospec=True)
|
|
||||||
def test__validate_fail(self, mock_get_volume):
|
|
||||||
volume_id = "0/p6-6/vol1"
|
|
||||||
volume = self.Volume()
|
|
||||||
volume.id = volume_id
|
|
||||||
mock_get_volume.return_value = volume
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
seamicro._validate_volume, self.info, volume_id)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_volume", autospec=True)
|
|
||||||
def test__validate_good(self, mock_get_volume):
|
|
||||||
volume = self.Volume()
|
|
||||||
mock_get_volume.return_value = volume
|
|
||||||
valid = seamicro._validate_volume(self.info, volume.id)
|
|
||||||
self.assertTrue(valid)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_pools", autospec=True)
|
|
||||||
def test__create_volume_fail(self, mock_get_pools):
|
|
||||||
mock_get_pools.return_value = None
|
|
||||||
self.assertRaises(exception.IronicException,
|
|
||||||
seamicro._create_volume,
|
|
||||||
self.info, 2)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, "_get_pools", autospec=True)
|
|
||||||
@mock.patch.object(seamicro, "_get_client", autospec=True)
|
|
||||||
def test__create_volume_good(self, mock_get_client, mock_get_pools):
|
|
||||||
pools = [self.Pool(1), self.Pool(6), self.Pool(5)]
|
|
||||||
mock_seamicro_volumes = mock.MagicMock(spec_set=['create'])
|
|
||||||
mock_get_client.return_value = mock.MagicMock(
|
|
||||||
volumes=mock_seamicro_volumes, spec_set=['volumes'])
|
|
||||||
mock_get_pools.return_value = pools
|
|
||||||
seamicro._create_volume(self.info, 2)
|
|
||||||
|
|
||||||
|
|
||||||
class SeaMicroPowerDriverTestCase(db_base.DbTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(SeaMicroPowerDriverTestCase, self).setUp()
|
|
||||||
mgr_utils.mock_the_extension_manager(driver='fake_seamicro')
|
|
||||||
self.driver = driver_factory.get_driver('fake_seamicro')
|
|
||||||
self.node = obj_utils.create_test_node(self.context,
|
|
||||||
driver='fake_seamicro',
|
|
||||||
driver_info=INFO_DICT)
|
|
||||||
self.get_server_patcher = mock.patch.object(seamicro, '_get_server',
|
|
||||||
autospec=True)
|
|
||||||
|
|
||||||
self.get_server_mock = None
|
|
||||||
self.Server = Fake_Server
|
|
||||||
self.Volume = Fake_Volume
|
|
||||||
self.info = seamicro._parse_driver_info(self.node)
|
|
||||||
|
|
||||||
def test_get_properties(self):
|
|
||||||
expected = seamicro.COMMON_PROPERTIES
|
|
||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
|
||||||
shared=True) as task:
|
|
||||||
self.assertEqual(expected, task.driver.power.get_properties())
|
|
||||||
|
|
||||||
expected = (list(seamicro.COMMON_PROPERTIES) +
|
|
||||||
list(seamicro.CONSOLE_PROPERTIES))
|
|
||||||
console_properties = task.driver.console.get_properties().keys()
|
|
||||||
self.assertEqual(sorted(expected), sorted(console_properties))
|
|
||||||
self.assertEqual(sorted(expected),
|
|
||||||
sorted(task.driver.get_properties().keys()))
|
|
||||||
|
|
||||||
def test_vendor_routes(self):
|
|
||||||
expected = ['set_node_vlan_id', 'attach_volume']
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
vendor_routes = task.driver.vendor.vendor_routes
|
|
||||||
self.assertIsInstance(vendor_routes, dict)
|
|
||||||
self.assertEqual(sorted(expected), sorted(vendor_routes))
|
|
||||||
|
|
||||||
def test_driver_routes(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=True) as task:
|
|
||||||
driver_routes = task.driver.vendor.driver_routes
|
|
||||||
self.assertIsInstance(driver_routes, dict)
|
|
||||||
self.assertEqual({}, driver_routes)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_parse_driver_info', autospec=True)
|
|
||||||
def test_power_interface_validate_good(self, parse_drv_info_mock):
|
|
||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
|
||||||
shared=True) as task:
|
|
||||||
task.driver.power.validate(task)
|
|
||||||
self.assertEqual(1, parse_drv_info_mock.call_count)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_parse_driver_info', autospec=True)
|
|
||||||
def test_power_interface_validate_fails(self, parse_drv_info_mock):
|
|
||||||
side_effect = exception.InvalidParameterValue("Bad input")
|
|
||||||
parse_drv_info_mock.side_effect = side_effect
|
|
||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
|
||||||
shared=True) as task:
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.power.validate, task)
|
|
||||||
self.assertEqual(1, parse_drv_info_mock.call_count)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_reboot', autospec=True)
|
|
||||||
def test_reboot(self, mock_reboot):
|
|
||||||
mock_reboot.return_value = states.POWER_ON
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.power.reboot(task)
|
|
||||||
|
|
||||||
mock_reboot.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
def test_set_power_state_bad_state(self):
|
|
||||||
self.get_server_mock = self.get_server_patcher.start()
|
|
||||||
self.get_server_mock.return_value = self.Server()
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.IronicException,
|
|
||||||
task.driver.power.set_power_state,
|
|
||||||
task, "BAD_PSTATE")
|
|
||||||
self.get_server_patcher.stop()
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_power_on', autospec=True)
|
|
||||||
def test_set_power_state_on_good(self, mock_power_on):
|
|
||||||
mock_power_on.return_value = states.POWER_ON
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.power.set_power_state(task, states.POWER_ON)
|
|
||||||
|
|
||||||
mock_power_on.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_power_on', autospec=True)
|
|
||||||
def test_set_power_state_on_fail(self, mock_power_on):
|
|
||||||
mock_power_on.return_value = states.POWER_OFF
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.PowerStateFailure,
|
|
||||||
task.driver.power.set_power_state,
|
|
||||||
task, states.POWER_ON)
|
|
||||||
|
|
||||||
mock_power_on.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_power_off', autospec=True)
|
|
||||||
def test_set_power_state_off_good(self, mock_power_off):
|
|
||||||
mock_power_off.return_value = states.POWER_OFF
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.power.set_power_state(task, states.POWER_OFF)
|
|
||||||
|
|
||||||
mock_power_off.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_power_off', autospec=True)
|
|
||||||
def test_set_power_state_off_fail(self, mock_power_off):
|
|
||||||
mock_power_off.return_value = states.POWER_ON
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.PowerStateFailure,
|
|
||||||
task.driver.power.set_power_state,
|
|
||||||
task, states.POWER_OFF)
|
|
||||||
|
|
||||||
mock_power_off.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_parse_driver_info', autospec=True)
|
|
||||||
def test_vendor_passthru_validate_good(self, mock_info):
|
|
||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
|
||||||
shared=True) as task:
|
|
||||||
for method in task.driver.vendor.vendor_routes:
|
|
||||||
task.driver.vendor.validate(task, **{'method': method})
|
|
||||||
self.assertEqual(len(task.driver.vendor.vendor_routes),
|
|
||||||
mock_info.call_count)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_parse_driver_info', autospec=True)
|
|
||||||
def test_vendor_passthru_validate_parse_driver_info_fail(self, mock_info):
|
|
||||||
mock_info.side_effect = exception.InvalidParameterValue("bad")
|
|
||||||
with task_manager.acquire(self.context, self.node['uuid'],
|
|
||||||
shared=True) as task:
|
|
||||||
method = list(task.driver.vendor.vendor_routes)[0]
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.vendor.validate,
|
|
||||||
task, **{'method': method})
|
|
||||||
mock_info.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_get_server', autospec=True)
|
|
||||||
def test_set_node_vlan_id_good(self, mock_get_server):
|
|
||||||
vlan_id = "12"
|
|
||||||
mock_get_server.return_value = self.Server(active="true")
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
kwargs = {'vlan_id': vlan_id}
|
|
||||||
task.driver.vendor.set_node_vlan_id(task, **kwargs)
|
|
||||||
mock_get_server.assert_called_once_with(self.info)
|
|
||||||
|
|
||||||
def test_set_node_vlan_id_no_input(self):
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.vendor.set_node_vlan_id,
|
|
||||||
task, **{})
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_get_server', autospec=True)
|
|
||||||
def test_set_node_vlan_id_fail(self, mock_get_server):
|
|
||||||
def fake_set_untagged_vlan(self, **kwargs):
|
|
||||||
raise seamicro_client_exception.ClientException(
|
|
||||||
http_client.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
vlan_id = "12"
|
|
||||||
server = self.Server(active="true")
|
|
||||||
server.set_untagged_vlan = fake_set_untagged_vlan
|
|
||||||
mock_get_server.return_value = server
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
kwargs = {'vlan_id': vlan_id}
|
|
||||||
self.assertRaises(exception.IronicException,
|
|
||||||
task.driver.vendor.set_node_vlan_id,
|
|
||||||
task, **kwargs)
|
|
||||||
|
|
||||||
mock_get_server.assert_called_once_with(self.info)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_get_server', autospec=True)
|
|
||||||
@mock.patch.object(seamicro, '_validate_volume', autospec=True)
|
|
||||||
def test_attach_volume_with_volume_id_good(self, mock_validate_volume,
|
|
||||||
mock_get_server):
|
|
||||||
volume_id = '0/ironic-p6-1/vol1'
|
|
||||||
mock_validate_volume.return_value = True
|
|
||||||
mock_get_server.return_value = self.Server(active="true")
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
kwargs = {'volume_id': volume_id}
|
|
||||||
task.driver.vendor.attach_volume(task, **kwargs)
|
|
||||||
mock_get_server.assert_called_once_with(self.info)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_get_server', autospec=True)
|
|
||||||
@mock.patch.object(seamicro, '_get_volume', autospec=True)
|
|
||||||
def test_attach_volume_with_invalid_volume_id_fail(self,
|
|
||||||
mock_get_volume,
|
|
||||||
mock_get_server):
|
|
||||||
volume_id = '0/p6-1/vol1'
|
|
||||||
mock_get_volume.return_value = self.Volume(volume_id)
|
|
||||||
mock_get_server.return_value = self.Server(active="true")
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
kwargs = {'volume_id': volume_id}
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.vendor.attach_volume,
|
|
||||||
task, **kwargs)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_get_server', autospec=True)
|
|
||||||
@mock.patch.object(seamicro, '_validate_volume', autospec=True)
|
|
||||||
def test_attach_volume_fail(self, mock_validate_volume,
|
|
||||||
mock_get_server):
|
|
||||||
def fake_attach_volume(self, **kwargs):
|
|
||||||
raise seamicro_client_exception.ClientException(
|
|
||||||
http_client.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
volume_id = '0/p6-1/vol1'
|
|
||||||
mock_validate_volume.return_value = True
|
|
||||||
server = self.Server(active="true")
|
|
||||||
server.attach_volume = fake_attach_volume
|
|
||||||
mock_get_server.return_value = server
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
kwargs = {'volume_id': volume_id}
|
|
||||||
self.assertRaises(exception.IronicException,
|
|
||||||
task.driver.vendor.attach_volume,
|
|
||||||
task, **kwargs)
|
|
||||||
|
|
||||||
mock_get_server.assert_called_once_with(self.info)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_get_server', autospec=True)
|
|
||||||
@mock.patch.object(seamicro, '_validate_volume', autospec=True)
|
|
||||||
@mock.patch.object(seamicro, '_create_volume', autospec=True)
|
|
||||||
def test_attach_volume_with_volume_size_good(self, mock_create_volume,
|
|
||||||
mock_validate_volume,
|
|
||||||
mock_get_server):
|
|
||||||
volume_id = '0/ironic-p6-1/vol1'
|
|
||||||
volume_size = 2
|
|
||||||
mock_create_volume.return_value = volume_id
|
|
||||||
mock_validate_volume.return_value = True
|
|
||||||
mock_get_server.return_value = self.Server(active="true")
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
kwargs = {'volume_size': volume_size}
|
|
||||||
task.driver.vendor.attach_volume(task, **kwargs)
|
|
||||||
mock_get_server.assert_called_once_with(self.info)
|
|
||||||
mock_create_volume.assert_called_once_with(self.info, volume_size)
|
|
||||||
|
|
||||||
def test_attach_volume_with_no_input_fail(self):
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.vendor.attach_volume, task,
|
|
||||||
**{})
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_get_server', autospec=True)
|
|
||||||
def test_set_boot_device_good(self, mock_get_server):
|
|
||||||
boot_device = "disk"
|
|
||||||
mock_get_server.return_value = self.Server(active="true")
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.management.set_boot_device(task, boot_device)
|
|
||||||
mock_get_server.assert_called_once_with(self.info)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_get_server', autospec=True)
|
|
||||||
def test_set_boot_device_invalid_device_fail(self, mock_get_server):
|
|
||||||
boot_device = "invalid_device"
|
|
||||||
mock_get_server.return_value = self.Server(active="true")
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.management.set_boot_device,
|
|
||||||
task, boot_device)
|
|
||||||
|
|
||||||
@mock.patch.object(seamicro, '_get_server', autospec=True)
|
|
||||||
def test_set_boot_device_fail(self, mock_get_server):
|
|
||||||
def fake_set_boot_order(self, **kwargs):
|
|
||||||
raise seamicro_client_exception.ClientException(
|
|
||||||
http_client.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
boot_device = "pxe"
|
|
||||||
server = self.Server(active="true")
|
|
||||||
server.set_boot_order = fake_set_boot_order
|
|
||||||
mock_get_server.return_value = server
|
|
||||||
with task_manager.acquire(self.context, self.info['uuid'],
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.IronicException,
|
|
||||||
task.driver.management.set_boot_device,
|
|
||||||
task, boot_device)
|
|
||||||
|
|
||||||
mock_get_server.assert_called_once_with(self.info)
|
|
||||||
|
|
||||||
def test_management_interface_get_supported_boot_devices(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
|
||||||
expected = [boot_devices.PXE, boot_devices.DISK]
|
|
||||||
self.assertEqual(sorted(expected), sorted(task.driver.management.
|
|
||||||
get_supported_boot_devices(task)))
|
|
||||||
|
|
||||||
def test_management_interface_get_boot_device(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
|
||||||
expected = {'boot_device': None, 'persistent': None}
|
|
||||||
self.assertEqual(expected,
|
|
||||||
task.driver.management.get_boot_device(task))
|
|
||||||
|
|
||||||
def test_management_interface_validate_good(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
|
||||||
task.driver.management.validate(task)
|
|
||||||
|
|
||||||
def test_management_interface_validate_fail(self):
|
|
||||||
# Missing SEAMICRO driver_info information
|
|
||||||
node = obj_utils.create_test_node(self.context,
|
|
||||||
uuid=uuidutils.generate_uuid(),
|
|
||||||
driver='fake_seamicro')
|
|
||||||
with task_manager.acquire(self.context, node.uuid) as task:
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
task.driver.management.validate, task)
|
|
||||||
|
|
||||||
|
|
||||||
class SeaMicroDriverTestCase(db_base.DbTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(SeaMicroDriverTestCase, self).setUp()
|
|
||||||
mgr_utils.mock_the_extension_manager(driver='fake_seamicro')
|
|
||||||
self.driver = driver_factory.get_driver('fake_seamicro')
|
|
||||||
self.node = obj_utils.create_test_node(self.context,
|
|
||||||
driver='fake_seamicro',
|
|
||||||
driver_info=INFO_DICT)
|
|
||||||
self.get_server_patcher = mock.patch.object(seamicro, '_get_server',
|
|
||||||
autospec=True)
|
|
||||||
|
|
||||||
self.get_server_mock = None
|
|
||||||
self.Server = Fake_Server
|
|
||||||
self.Volume = Fake_Volume
|
|
||||||
self.info = seamicro._parse_driver_info(self.node)
|
|
||||||
|
|
||||||
@mock.patch.object(console_utils, 'start_shellinabox_console',
|
|
||||||
autospec=True)
|
|
||||||
def test_start_console(self, mock_exec):
|
|
||||||
mock_exec.return_value = None
|
|
||||||
with task_manager.acquire(self.context,
|
|
||||||
self.node.uuid) as task:
|
|
||||||
self.driver.console.start_console(task)
|
|
||||||
|
|
||||||
mock_exec.assert_called_once_with(self.info['uuid'],
|
|
||||||
self.info['port'],
|
|
||||||
mock.ANY)
|
|
||||||
|
|
||||||
@mock.patch.object(console_utils, 'start_shellinabox_console',
|
|
||||||
autospec=True)
|
|
||||||
def test_start_console_fail(self, mock_exec):
|
|
||||||
mock_exec.side_effect = exception.ConsoleSubprocessFailed(
|
|
||||||
error='error')
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context,
|
|
||||||
self.node.uuid) as task:
|
|
||||||
self.assertRaises(exception.ConsoleSubprocessFailed,
|
|
||||||
self.driver.console.start_console,
|
|
||||||
task)
|
|
||||||
|
|
||||||
@mock.patch.object(console_utils, 'stop_shellinabox_console',
|
|
||||||
autospec=True)
|
|
||||||
def test_stop_console(self, mock_exec):
|
|
||||||
mock_exec.return_value = None
|
|
||||||
with task_manager.acquire(self.context,
|
|
||||||
self.node.uuid) as task:
|
|
||||||
self.driver.console.stop_console(task)
|
|
||||||
|
|
||||||
mock_exec.assert_called_once_with(self.info['uuid'])
|
|
||||||
|
|
||||||
@mock.patch.object(console_utils, 'stop_shellinabox_console',
|
|
||||||
autospec=True)
|
|
||||||
def test_stop_console_fail(self, mock_stop):
|
|
||||||
mock_stop.side_effect = exception.ConsoleError()
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context,
|
|
||||||
self.node.uuid) as task:
|
|
||||||
self.assertRaises(exception.ConsoleError,
|
|
||||||
self.driver.console.stop_console,
|
|
||||||
task)
|
|
||||||
|
|
||||||
mock_stop.assert_called_once_with(self.node.uuid)
|
|
||||||
|
|
||||||
@mock.patch.object(console_utils, 'start_shellinabox_console',
|
|
||||||
autospec=True)
|
|
||||||
def test_start_console_fail_nodir(self, mock_exec):
|
|
||||||
mock_exec.side_effect = exception.ConsoleError()
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context,
|
|
||||||
self.node.uuid) as task:
|
|
||||||
self.assertRaises(exception.ConsoleError,
|
|
||||||
self.driver.console.start_console,
|
|
||||||
task)
|
|
||||||
mock_exec.assert_called_once_with(self.node.uuid, mock.ANY, mock.ANY)
|
|
||||||
|
|
||||||
@mock.patch.object(console_utils, 'get_shellinabox_console_url',
|
|
||||||
autospec=True)
|
|
||||||
def test_get_console(self, mock_exec):
|
|
||||||
url = 'http://localhost:4201'
|
|
||||||
mock_exec.return_value = url
|
|
||||||
expected = {'type': 'shellinabox', 'url': url}
|
|
||||||
|
|
||||||
with task_manager.acquire(self.context,
|
|
||||||
self.node.uuid) as task:
|
|
||||||
console_info = self.driver.console.get_console(task)
|
|
||||||
|
|
||||||
self.assertEqual(expected, console_info)
|
|
||||||
mock_exec.assert_called_once_with(self.info['port'])
|
|
@ -1,420 +0,0 @@
|
|||||||
# 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 VirtualBox Driver Modules."""
|
|
||||||
|
|
||||||
import mock
|
|
||||||
from pyremotevbox import exception as pyremotevbox_exc
|
|
||||||
from pyremotevbox import vbox as pyremotevbox_vbox
|
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
|
||||||
from ironic.common import exception
|
|
||||||
from ironic.common import states
|
|
||||||
from ironic.conductor import task_manager
|
|
||||||
from ironic.drivers.modules import virtualbox
|
|
||||||
from ironic.tests.unit.conductor import mgr_utils
|
|
||||||
from ironic.tests.unit.db import base as db_base
|
|
||||||
from ironic.tests.unit.objects import utils as obj_utils
|
|
||||||
|
|
||||||
INFO_DICT = {
|
|
||||||
'virtualbox_vmname': 'baremetal1',
|
|
||||||
'virtualbox_host': '10.0.2.2',
|
|
||||||
'virtualbox_username': 'username',
|
|
||||||
'virtualbox_password': 'password',
|
|
||||||
'virtualbox_port': 12345,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualBoxMethodsTestCase(db_base.DbTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(VirtualBoxMethodsTestCase, self).setUp()
|
|
||||||
driver_info = INFO_DICT.copy()
|
|
||||||
mgr_utils.mock_the_extension_manager(driver="fake_vbox")
|
|
||||||
self.node = obj_utils.create_test_node(self.context,
|
|
||||||
driver='fake_vbox',
|
|
||||||
driver_info=driver_info)
|
|
||||||
|
|
||||||
def test__parse_driver_info(self):
|
|
||||||
info = virtualbox._parse_driver_info(self.node)
|
|
||||||
self.assertEqual('baremetal1', info['vmname'])
|
|
||||||
self.assertEqual('10.0.2.2', info['host'])
|
|
||||||
self.assertEqual('username', info['username'])
|
|
||||||
self.assertEqual('password', info['password'])
|
|
||||||
self.assertEqual(12345, info['port'])
|
|
||||||
|
|
||||||
def test__parse_driver_info_missing_vmname(self):
|
|
||||||
del self.node.driver_info['virtualbox_vmname']
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
virtualbox._parse_driver_info, self.node)
|
|
||||||
|
|
||||||
def test__parse_driver_info_missing_host(self):
|
|
||||||
del self.node.driver_info['virtualbox_host']
|
|
||||||
self.assertRaises(exception.MissingParameterValue,
|
|
||||||
virtualbox._parse_driver_info, self.node)
|
|
||||||
|
|
||||||
def test__parse_driver_info_invalid_port(self):
|
|
||||||
self.node.driver_info['virtualbox_port'] = 'invalid-port'
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
virtualbox._parse_driver_info, self.node)
|
|
||||||
|
|
||||||
def test__parse_driver_info_missing_port(self):
|
|
||||||
del self.node.driver_info['virtualbox_port']
|
|
||||||
info = virtualbox._parse_driver_info(self.node)
|
|
||||||
self.assertEqual(18083, info['port'])
|
|
||||||
|
|
||||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True)
|
|
||||||
def test__run_virtualbox_method(self, host_mock):
|
|
||||||
host_object_mock = mock.MagicMock(spec_set=['find_vm'])
|
|
||||||
func_mock = mock.MagicMock(spec_set=[])
|
|
||||||
vm_object_mock = mock.MagicMock(spec_set=['foo'], foo=func_mock)
|
|
||||||
host_mock.return_value = host_object_mock
|
|
||||||
host_object_mock.find_vm.return_value = vm_object_mock
|
|
||||||
func_mock.return_value = 'return-value'
|
|
||||||
|
|
||||||
return_value = virtualbox._run_virtualbox_method(
|
|
||||||
self.node, 'some-ironic-method', 'foo', 'args', kwarg='kwarg')
|
|
||||||
|
|
||||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
|
||||||
host='10.0.2.2',
|
|
||||||
username='username',
|
|
||||||
password='password',
|
|
||||||
port=12345)
|
|
||||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
|
||||||
func_mock.assert_called_once_with('args', kwarg='kwarg')
|
|
||||||
self.assertEqual('return-value', return_value)
|
|
||||||
|
|
||||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True)
|
|
||||||
def test__run_virtualbox_method_get_host_fails(self, host_mock):
|
|
||||||
host_mock.side_effect = pyremotevbox_exc.PyRemoteVBoxException
|
|
||||||
|
|
||||||
self.assertRaises(exception.VirtualBoxOperationFailed,
|
|
||||||
virtualbox._run_virtualbox_method,
|
|
||||||
self.node, 'some-ironic-method', 'foo',
|
|
||||||
'args', kwarg='kwarg')
|
|
||||||
|
|
||||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True)
|
|
||||||
def test__run_virtualbox_method_find_vm_fails(self, host_mock):
|
|
||||||
host_object_mock = mock.MagicMock(spec_set=['find_vm'])
|
|
||||||
host_mock.return_value = host_object_mock
|
|
||||||
exc = pyremotevbox_exc.PyRemoteVBoxException
|
|
||||||
host_object_mock.find_vm.side_effect = exc
|
|
||||||
|
|
||||||
self.assertRaises(exception.VirtualBoxOperationFailed,
|
|
||||||
virtualbox._run_virtualbox_method,
|
|
||||||
self.node, 'some-ironic-method', 'foo', 'args',
|
|
||||||
kwarg='kwarg')
|
|
||||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
|
||||||
host='10.0.2.2',
|
|
||||||
username='username',
|
|
||||||
password='password',
|
|
||||||
port=12345)
|
|
||||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
|
||||||
|
|
||||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True)
|
|
||||||
def test__run_virtualbox_method_func_fails(self, host_mock):
|
|
||||||
host_object_mock = mock.MagicMock(spec_set=['find_vm'])
|
|
||||||
host_mock.return_value = host_object_mock
|
|
||||||
func_mock = mock.MagicMock()
|
|
||||||
vm_object_mock = mock.MagicMock(spec_set=['foo'], foo=func_mock)
|
|
||||||
host_object_mock.find_vm.return_value = vm_object_mock
|
|
||||||
func_mock.side_effect = pyremotevbox_exc.PyRemoteVBoxException
|
|
||||||
|
|
||||||
self.assertRaises(exception.VirtualBoxOperationFailed,
|
|
||||||
virtualbox._run_virtualbox_method,
|
|
||||||
self.node, 'some-ironic-method', 'foo',
|
|
||||||
'args', kwarg='kwarg')
|
|
||||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
|
||||||
host='10.0.2.2',
|
|
||||||
username='username',
|
|
||||||
password='password',
|
|
||||||
port=12345)
|
|
||||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
|
||||||
func_mock.assert_called_once_with('args', kwarg='kwarg')
|
|
||||||
|
|
||||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True)
|
|
||||||
def test__run_virtualbox_method_invalid_method(self, host_mock):
|
|
||||||
host_object_mock = mock.MagicMock(spec_set=['find_vm'])
|
|
||||||
host_mock.return_value = host_object_mock
|
|
||||||
vm_object_mock = mock.MagicMock(spec_set=[])
|
|
||||||
host_object_mock.find_vm.return_value = vm_object_mock
|
|
||||||
del vm_object_mock.foo
|
|
||||||
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
virtualbox._run_virtualbox_method,
|
|
||||||
self.node, 'some-ironic-method', 'foo',
|
|
||||||
'args', kwarg='kwarg')
|
|
||||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
|
||||||
host='10.0.2.2',
|
|
||||||
username='username',
|
|
||||||
password='password',
|
|
||||||
port=12345)
|
|
||||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
|
||||||
|
|
||||||
@mock.patch.object(pyremotevbox_vbox, 'VirtualBoxHost', autospec=True)
|
|
||||||
def test__run_virtualbox_method_vm_wrong_power_state(self, host_mock):
|
|
||||||
host_object_mock = mock.MagicMock(spec_set=['find_vm'])
|
|
||||||
host_mock.return_value = host_object_mock
|
|
||||||
func_mock = mock.MagicMock(spec_set=[])
|
|
||||||
vm_object_mock = mock.MagicMock(spec_set=['foo'], foo=func_mock)
|
|
||||||
host_object_mock.find_vm.return_value = vm_object_mock
|
|
||||||
func_mock.side_effect = pyremotevbox_exc.VmInWrongPowerState
|
|
||||||
|
|
||||||
# _run_virtualbox_method() doesn't catch VmInWrongPowerState and
|
|
||||||
# lets caller handle it.
|
|
||||||
self.assertRaises(pyremotevbox_exc.VmInWrongPowerState,
|
|
||||||
virtualbox._run_virtualbox_method,
|
|
||||||
self.node, 'some-ironic-method', 'foo',
|
|
||||||
'args', kwarg='kwarg')
|
|
||||||
host_mock.assert_called_once_with(vmname='baremetal1',
|
|
||||||
host='10.0.2.2',
|
|
||||||
username='username',
|
|
||||||
password='password',
|
|
||||||
port=12345)
|
|
||||||
host_object_mock.find_vm.assert_called_once_with('baremetal1')
|
|
||||||
func_mock.assert_called_once_with('args', kwarg='kwarg')
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualBoxPowerTestCase(db_base.DbTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(VirtualBoxPowerTestCase, self).setUp()
|
|
||||||
driver_info = INFO_DICT.copy()
|
|
||||||
mgr_utils.mock_the_extension_manager(driver="fake_vbox")
|
|
||||||
self.node = obj_utils.create_test_node(self.context,
|
|
||||||
driver='fake_vbox',
|
|
||||||
driver_info=driver_info)
|
|
||||||
|
|
||||||
def test_get_properties(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
properties = task.driver.power.get_properties()
|
|
||||||
|
|
||||||
self.assertIn('virtualbox_vmname', properties)
|
|
||||||
self.assertIn('virtualbox_host', properties)
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox, '_parse_driver_info', autospec=True)
|
|
||||||
def test_validate(self, parse_info_mock):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.power.validate(task)
|
|
||||||
parse_info_mock.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_get_power_state(self, run_method_mock):
|
|
||||||
run_method_mock.return_value = 'PoweredOff'
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
power_state = task.driver.power.get_power_state(task)
|
|
||||||
run_method_mock.assert_called_once_with(task.node,
|
|
||||||
'get_power_state',
|
|
||||||
'get_power_status')
|
|
||||||
self.assertEqual(states.POWER_OFF, power_state)
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_get_power_state_invalid_state(self, run_method_mock):
|
|
||||||
run_method_mock.return_value = 'invalid-state'
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
power_state = task.driver.power.get_power_state(task)
|
|
||||||
run_method_mock.assert_called_once_with(task.node,
|
|
||||||
'get_power_state',
|
|
||||||
'get_power_status')
|
|
||||||
self.assertEqual(states.ERROR, power_state)
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_set_power_state_off(self, run_method_mock):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.power.set_power_state(task, states.POWER_OFF)
|
|
||||||
run_method_mock.assert_called_once_with(task.node,
|
|
||||||
'set_power_state',
|
|
||||||
'stop')
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox.VirtualBoxManagement, 'set_boot_device')
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_set_power_state_on(self, run_method_mock, set_boot_device_mock):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.node.driver_internal_info['vbox_target_boot_device'] = 'pxe'
|
|
||||||
task.driver.power.set_power_state(task, states.POWER_ON)
|
|
||||||
run_method_mock.assert_called_once_with(task.node,
|
|
||||||
'set_power_state',
|
|
||||||
'start')
|
|
||||||
set_boot_device_mock.assert_called_once_with(task, 'pxe')
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox.VirtualBoxManagement, 'set_boot_device')
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_set_power_state_reboot(self, run_method_mock,
|
|
||||||
mock_set_boot_device):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.node.driver_internal_info['vbox_target_boot_device'] = 'pxe'
|
|
||||||
task.driver.power.set_power_state(task, states.REBOOT)
|
|
||||||
run_method_mock.assert_any_call(task.node,
|
|
||||||
'reboot',
|
|
||||||
'stop')
|
|
||||||
mock_set_boot_device.assert_called_once_with(task, 'pxe')
|
|
||||||
run_method_mock.assert_any_call(task.node,
|
|
||||||
'reboot',
|
|
||||||
'start')
|
|
||||||
|
|
||||||
def test_set_power_state_invalid_state(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.power.set_power_state,
|
|
||||||
task, 'invalid-state')
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_reboot(self, run_method_mock):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.power.reboot(task)
|
|
||||||
run_method_mock.assert_any_call(task.node,
|
|
||||||
'reboot',
|
|
||||||
'stop')
|
|
||||||
run_method_mock.assert_any_call(task.node,
|
|
||||||
'reboot',
|
|
||||||
'start')
|
|
||||||
|
|
||||||
|
|
||||||
class VirtualBoxManagementTestCase(db_base.DbTestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(VirtualBoxManagementTestCase, self).setUp()
|
|
||||||
driver_info = INFO_DICT.copy()
|
|
||||||
mgr_utils.mock_the_extension_manager(driver="fake_vbox")
|
|
||||||
self.node = obj_utils.create_test_node(self.context,
|
|
||||||
driver='fake_vbox',
|
|
||||||
driver_info=driver_info)
|
|
||||||
|
|
||||||
def test_get_properties(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
properties = task.driver.management.get_properties()
|
|
||||||
|
|
||||||
self.assertIn('virtualbox_vmname', properties)
|
|
||||||
self.assertIn('virtualbox_host', properties)
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox, '_parse_driver_info', autospec=True)
|
|
||||||
def test_validate(self, parse_info_mock):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.management.validate(task)
|
|
||||||
parse_info_mock.assert_called_once_with(task.node)
|
|
||||||
|
|
||||||
def test_get_supported_boot_devices(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
devices = task.driver.management.get_supported_boot_devices(task)
|
|
||||||
self.assertIn(boot_devices.PXE, devices)
|
|
||||||
self.assertIn(boot_devices.DISK, devices)
|
|
||||||
self.assertIn(boot_devices.CDROM, devices)
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox.VirtualBoxPower,
|
|
||||||
'get_power_state', autospec=True)
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method',
|
|
||||||
autospec=True)
|
|
||||||
def test_get_boot_device_VM_Poweroff_ok(self, run_method_mock,
|
|
||||||
get_power_state_mock):
|
|
||||||
run_method_mock.return_value = 'Network'
|
|
||||||
get_power_state_mock.return_value = states.POWER_OFF
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
ret_val = task.driver.management.get_boot_device(task)
|
|
||||||
run_method_mock.assert_called_once_with(task.node,
|
|
||||||
'get_boot_device',
|
|
||||||
'get_boot_device')
|
|
||||||
self.assertEqual(boot_devices.PXE, ret_val['boot_device'])
|
|
||||||
self.assertTrue(ret_val['persistent'])
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox.VirtualBoxPower,
|
|
||||||
'get_power_state', autospec=True)
|
|
||||||
def test_get_boot_device_VM_Poweron_ok(self, get_power_state_mock):
|
|
||||||
get_power_state_mock.return_value = states.POWER_ON
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.node.driver_internal_info['vbox_target_boot_device'] = 'pxe'
|
|
||||||
ret_val = task.driver.management.get_boot_device(task)
|
|
||||||
self.assertEqual(boot_devices.PXE, ret_val['boot_device'])
|
|
||||||
self.assertTrue(ret_val['persistent'])
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox.VirtualBoxPower,
|
|
||||||
'get_power_state', autospec=True)
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method',
|
|
||||||
autospec=True)
|
|
||||||
def test_get_boot_device_target_device_none_ok(self,
|
|
||||||
run_method_mock,
|
|
||||||
get_power_state_mock):
|
|
||||||
run_method_mock.return_value = 'Network'
|
|
||||||
get_power_state_mock.return_value = states.POWER_ON
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.node.driver_internal_info['vbox_target_boot_device'] = None
|
|
||||||
ret_val = task.driver.management.get_boot_device(task)
|
|
||||||
run_method_mock.assert_called_once_with(task.node,
|
|
||||||
'get_boot_device',
|
|
||||||
'get_boot_device')
|
|
||||||
self.assertEqual(boot_devices.PXE, ret_val['boot_device'])
|
|
||||||
self.assertTrue(ret_val['persistent'])
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_get_boot_device_invalid(self, run_method_mock):
|
|
||||||
run_method_mock.return_value = 'invalid-boot-device'
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
ret_val = task.driver.management.get_boot_device(task)
|
|
||||||
self.assertIsNone(ret_val['boot_device'])
|
|
||||||
self.assertIsNone(ret_val['persistent'])
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox.VirtualBoxPower,
|
|
||||||
'get_power_state', autospec=True)
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_set_boot_device_VM_Poweroff_ok(self, run_method_mock,
|
|
||||||
get_power_state_mock):
|
|
||||||
get_power_state_mock.return_value = states.POWER_OFF
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.management.set_boot_device(task, boot_devices.PXE)
|
|
||||||
run_method_mock.assert_called_with(task.node,
|
|
||||||
'set_boot_device',
|
|
||||||
'set_boot_device',
|
|
||||||
'Network')
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox.VirtualBoxPower,
|
|
||||||
'get_power_state', autospec=True)
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_set_boot_device_VM_Poweron_ok(self, run_method_mock,
|
|
||||||
get_power_state_mock):
|
|
||||||
get_power_state_mock.return_value = states.POWER_ON
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
task.driver.management.set_boot_device(task, boot_devices.PXE)
|
|
||||||
self.assertEqual('pxe',
|
|
||||||
task.node.driver_internal_info
|
|
||||||
['vbox_target_boot_device'])
|
|
||||||
|
|
||||||
@mock.patch.object(virtualbox, '_run_virtualbox_method', autospec=True)
|
|
||||||
def test_set_boot_device_invalid(self, run_method_mock):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
|
||||||
task.driver.management.set_boot_device,
|
|
||||||
task, 'invalid-boot-device')
|
|
||||||
|
|
||||||
def test_get_sensors_data(self):
|
|
||||||
with task_manager.acquire(self.context, self.node.uuid,
|
|
||||||
shared=False) as task:
|
|
||||||
self.assertRaises(NotImplementedError,
|
|
||||||
task.driver.management.get_sensors_data,
|
|
||||||
task)
|
|
@ -33,15 +33,11 @@ from ironic.drivers.modules import ipmitool
|
|||||||
from ironic.drivers.modules.irmc import management as irmc_management
|
from ironic.drivers.modules.irmc import management as irmc_management
|
||||||
from ironic.drivers.modules.irmc import power as irmc_power
|
from ironic.drivers.modules.irmc import power as irmc_power
|
||||||
from ironic.drivers.modules import iscsi_deploy
|
from ironic.drivers.modules import iscsi_deploy
|
||||||
from ironic.drivers.modules.msftocs import management as msftocs_management
|
|
||||||
from ironic.drivers.modules.msftocs import power as msftocs_power
|
|
||||||
from ironic.drivers.modules import pxe as pxe_module
|
from ironic.drivers.modules import pxe as pxe_module
|
||||||
from ironic.drivers.modules import seamicro
|
|
||||||
from ironic.drivers.modules import snmp
|
from ironic.drivers.modules import snmp
|
||||||
from ironic.drivers.modules import ssh
|
from ironic.drivers.modules import ssh
|
||||||
from ironic.drivers.modules.ucs import management as ucs_management
|
from ironic.drivers.modules.ucs import management as ucs_management
|
||||||
from ironic.drivers.modules.ucs import power as ucs_power
|
from ironic.drivers.modules.ucs import power as ucs_power
|
||||||
from ironic.drivers.modules import virtualbox
|
|
||||||
from ironic.drivers import pxe
|
from ironic.drivers import pxe
|
||||||
|
|
||||||
|
|
||||||
@ -83,28 +79,6 @@ class PXEDriversTestCase(testtools.TestCase):
|
|||||||
self.assertRaises(exception.DriverLoadError,
|
self.assertRaises(exception.DriverLoadError,
|
||||||
pxe.PXEAndIPMINativeDriver)
|
pxe.PXEAndIPMINativeDriver)
|
||||||
|
|
||||||
@mock.patch.object(pxe.importutils, 'try_import', spec_set=True,
|
|
||||||
autospec=True)
|
|
||||||
def test_pxe_seamicro_driver(self, try_import_mock):
|
|
||||||
try_import_mock.return_value = True
|
|
||||||
|
|
||||||
driver = pxe.PXEAndSeaMicroDriver()
|
|
||||||
|
|
||||||
self.assertIsInstance(driver.power, seamicro.Power)
|
|
||||||
self.assertIsInstance(driver.boot, pxe_module.PXEBoot)
|
|
||||||
self.assertIsInstance(driver.deploy, iscsi_deploy.ISCSIDeploy)
|
|
||||||
self.assertIsInstance(driver.management, seamicro.Management)
|
|
||||||
self.assertIsInstance(driver.vendor, seamicro.VendorPassthru)
|
|
||||||
self.assertIsInstance(driver.console, seamicro.ShellinaboxConsole)
|
|
||||||
|
|
||||||
@mock.patch.object(pxe.importutils, 'try_import', spec_set=True,
|
|
||||||
autospec=True)
|
|
||||||
def test_pxe_seamicro_driver_import_error(self, try_import_mock):
|
|
||||||
try_import_mock.return_value = False
|
|
||||||
|
|
||||||
self.assertRaises(exception.DriverLoadError,
|
|
||||||
pxe.PXEAndSeaMicroDriver)
|
|
||||||
|
|
||||||
@mock.patch.object(pxe.importutils, 'try_import', spec_set=True,
|
@mock.patch.object(pxe.importutils, 'try_import', spec_set=True,
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_pxe_ilo_driver(self, try_import_mock):
|
def test_pxe_ilo_driver(self, try_import_mock):
|
||||||
@ -173,41 +147,6 @@ class PXEDriversTestCase(testtools.TestCase):
|
|||||||
self.assertRaises(exception.DriverLoadError,
|
self.assertRaises(exception.DriverLoadError,
|
||||||
pxe.PXEAndIRMCDriver)
|
pxe.PXEAndIRMCDriver)
|
||||||
|
|
||||||
@mock.patch.object(pxe.importutils, 'try_import', spec_set=True,
|
|
||||||
autospec=True)
|
|
||||||
def test_pxe_vbox_driver(self, try_import_mock):
|
|
||||||
try_import_mock.return_value = True
|
|
||||||
|
|
||||||
driver = pxe.PXEAndVirtualBoxDriver()
|
|
||||||
|
|
||||||
self.assertIsInstance(driver.power, virtualbox.VirtualBoxPower)
|
|
||||||
self.assertIsInstance(driver.boot, pxe_module.PXEBoot)
|
|
||||||
self.assertIsInstance(driver.deploy, iscsi_deploy.ISCSIDeploy)
|
|
||||||
self.assertIsInstance(driver.management,
|
|
||||||
virtualbox.VirtualBoxManagement)
|
|
||||||
self.assertIsInstance(driver.raid, agent.AgentRAID)
|
|
||||||
|
|
||||||
@mock.patch.object(pxe.importutils, 'try_import', spec_set=True,
|
|
||||||
autospec=True)
|
|
||||||
def test_pxe_vbox_driver_import_error(self, try_import_mock):
|
|
||||||
try_import_mock.return_value = False
|
|
||||||
|
|
||||||
self.assertRaises(exception.DriverLoadError,
|
|
||||||
pxe.PXEAndVirtualBoxDriver)
|
|
||||||
|
|
||||||
@mock.patch.object(pxe.importutils, 'try_import', spec_set=True,
|
|
||||||
autospec=True)
|
|
||||||
def test_pxe_msftocs_driver(self, try_import_mock):
|
|
||||||
try_import_mock.return_value = True
|
|
||||||
|
|
||||||
driver = pxe.PXEAndMSFTOCSDriver()
|
|
||||||
|
|
||||||
self.assertIsInstance(driver.power, msftocs_power.MSFTOCSPower)
|
|
||||||
self.assertIsInstance(driver.boot, pxe_module.PXEBoot)
|
|
||||||
self.assertIsInstance(driver.deploy, iscsi_deploy.ISCSIDeploy)
|
|
||||||
self.assertIsInstance(driver.management,
|
|
||||||
msftocs_management.MSFTOCSManagement)
|
|
||||||
|
|
||||||
@mock.patch.object(pxe.importutils, 'try_import', spec_set=True,
|
@mock.patch.object(pxe.importutils, 'try_import', spec_set=True,
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_pxe_ucs_driver(self, try_import_mock):
|
def test_pxe_ucs_driver(self, try_import_mock):
|
||||||
|
@ -73,19 +73,6 @@ PYGHMI_IPMICMD_SPEC = (
|
|||||||
'Command',
|
'Command',
|
||||||
)
|
)
|
||||||
|
|
||||||
# pyremotevbox
|
|
||||||
PYREMOTEVBOX_SPEC = (
|
|
||||||
'exception',
|
|
||||||
'vbox',
|
|
||||||
)
|
|
||||||
PYREMOTEVBOX_EXC_SPEC = (
|
|
||||||
'PyRemoteVBoxException',
|
|
||||||
'VmInWrongPowerState',
|
|
||||||
)
|
|
||||||
PYREMOTEVBOX_VBOX_SPEC = (
|
|
||||||
'VirtualBoxHost',
|
|
||||||
)
|
|
||||||
|
|
||||||
# pywsman
|
# pywsman
|
||||||
PYWSMAN_SPEC = (
|
PYWSMAN_SPEC = (
|
||||||
'Client',
|
'Client',
|
||||||
@ -150,17 +137,3 @@ ONEVIEWCLIENT_STATES_SPEC = (
|
|||||||
'ONEVIEW_RESETTING',
|
'ONEVIEW_RESETTING',
|
||||||
'ONEVIEW_ERROR',
|
'ONEVIEW_ERROR',
|
||||||
)
|
)
|
||||||
|
|
||||||
# seamicro
|
|
||||||
SEAMICRO_SPEC = (
|
|
||||||
'client',
|
|
||||||
'exceptions',
|
|
||||||
)
|
|
||||||
# seamicro.client module
|
|
||||||
SEAMICRO_CLIENT_MOD_SPEC = (
|
|
||||||
'Client',
|
|
||||||
)
|
|
||||||
SEAMICRO_EXC_SPEC = (
|
|
||||||
'ClientException',
|
|
||||||
'UnsupportedVersion',
|
|
||||||
)
|
|
||||||
|
@ -22,7 +22,6 @@ respective external libraries' actually being present.
|
|||||||
Any external library required by a third-party driver should be mocked here.
|
Any external library required by a third-party driver should be mocked here.
|
||||||
Current list of mocked libraries:
|
Current list of mocked libraries:
|
||||||
|
|
||||||
- seamicroclient
|
|
||||||
- ipminative
|
- ipminative
|
||||||
- proliantutils
|
- proliantutils
|
||||||
- pysnmp
|
- pysnmp
|
||||||
@ -43,24 +42,6 @@ from ironic.tests.unit.drivers import third_party_driver_mock_specs \
|
|||||||
as mock_specs
|
as mock_specs
|
||||||
|
|
||||||
|
|
||||||
# attempt to load the external 'seamicroclient' library, which is
|
|
||||||
# required by the optional drivers.modules.seamicro module
|
|
||||||
seamicroclient = importutils.try_import("seamicroclient")
|
|
||||||
if not seamicroclient:
|
|
||||||
smc = mock.MagicMock(spec_set=mock_specs.SEAMICRO_SPEC)
|
|
||||||
smc.client = mock.MagicMock(spec_set=mock_specs.SEAMICRO_CLIENT_MOD_SPEC)
|
|
||||||
smc.exceptions = mock.MagicMock(spec_set=mock_specs.SEAMICRO_EXC_SPEC)
|
|
||||||
smc.exceptions.ClientException = Exception
|
|
||||||
smc.exceptions.UnsupportedVersion = Exception
|
|
||||||
sys.modules['seamicroclient'] = smc
|
|
||||||
sys.modules['seamicroclient.client'] = smc.client
|
|
||||||
sys.modules['seamicroclient.exceptions'] = smc.exceptions
|
|
||||||
|
|
||||||
# if anything has loaded the seamicro driver yet, reload it now that
|
|
||||||
# the external library has been mocked
|
|
||||||
if 'ironic.drivers.modules.seamicro' in sys.modules:
|
|
||||||
six.moves.reload_module(sys.modules['ironic.drivers.modules.seamicro'])
|
|
||||||
|
|
||||||
# IPMITool driver checks the system for presence of 'ipmitool' binary during
|
# IPMITool driver checks the system for presence of 'ipmitool' binary during
|
||||||
# __init__. We bypass that check in order to run the unit tests, which do not
|
# __init__. We bypass that check in order to run the unit tests, which do not
|
||||||
# depend on 'ipmitool' being on the system.
|
# depend on 'ipmitool' being on the system.
|
||||||
@ -217,21 +198,6 @@ irmc_boot.check_share_fs_mounted_patcher = mock.patch(
|
|||||||
irmc_boot.check_share_fs_mounted_patcher.return_value = None
|
irmc_boot.check_share_fs_mounted_patcher.return_value = None
|
||||||
|
|
||||||
|
|
||||||
pyremotevbox = importutils.try_import('pyremotevbox')
|
|
||||||
if not pyremotevbox:
|
|
||||||
pyremotevbox = mock.MagicMock(spec_set=mock_specs.PYREMOTEVBOX_SPEC)
|
|
||||||
pyremotevbox.exception = mock.MagicMock(
|
|
||||||
spec_set=mock_specs.PYREMOTEVBOX_EXC_SPEC)
|
|
||||||
pyremotevbox.exception.PyRemoteVBoxException = Exception
|
|
||||||
pyremotevbox.exception.VmInWrongPowerState = Exception
|
|
||||||
pyremotevbox.vbox = mock.MagicMock(
|
|
||||||
spec_set=mock_specs.PYREMOTEVBOX_VBOX_SPEC)
|
|
||||||
sys.modules['pyremotevbox'] = pyremotevbox
|
|
||||||
if 'ironic.drivers.modules.virtualbox' in sys.modules:
|
|
||||||
six.moves.reload_module(
|
|
||||||
sys.modules['ironic.drivers.modules.virtualbox'])
|
|
||||||
|
|
||||||
|
|
||||||
ironic_inspector_client = importutils.try_import('ironic_inspector_client')
|
ironic_inspector_client = importutils.try_import('ironic_inspector_client')
|
||||||
if not ironic_inspector_client:
|
if not ironic_inspector_client:
|
||||||
ironic_inspector_client = mock.MagicMock(
|
ironic_inspector_client = mock.MagicMock(
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
A number of drivers that were declared as unsupported in Newton release
|
||||||
|
have been removed from ironic tree. This includes drivers with
|
||||||
|
power and/or management driver interfaces based on:
|
||||||
|
|
||||||
|
- MSFT OCS
|
||||||
|
- SeaMicro client
|
||||||
|
- Virtualbox over pyremotevbox client
|
||||||
|
|
||||||
|
As a result, the following ironic drivers will no longer be available:
|
||||||
|
|
||||||
|
- agent_vbox
|
||||||
|
- fake_msftocs
|
||||||
|
- fake_seamicro
|
||||||
|
- fake_vbox
|
||||||
|
- pxe_msftocs
|
||||||
|
- pxe_seamicro
|
||||||
|
- pxe_vbox
|
||||||
|
|
||||||
|
After upgrading, if one or more of these drivers are in the
|
||||||
|
'enabled_drivers' configuration option,
|
||||||
|
the ironic-conductor service will fail to start.
|
||||||
|
Any existing ironic nodes with these drivers assigned will become
|
||||||
|
inoperational via ironic after ironic upgrade,
|
||||||
|
as it will be not possible to change any node state/properties
|
||||||
|
except changing the node driver.
|
||||||
|
Operators having one of the drivers listed above enabled are required to
|
||||||
|
either disable those drivers and assign another existing driver
|
||||||
|
to affected nodes as appropriate,
|
||||||
|
or install these drivers from elsewhere separately.
|
@ -49,7 +49,6 @@ ironic.drivers =
|
|||||||
agent_pxe_oneview = ironic.drivers.oneview:AgentPXEOneViewDriver
|
agent_pxe_oneview = ironic.drivers.oneview:AgentPXEOneViewDriver
|
||||||
agent_pyghmi = ironic.drivers.agent:AgentAndIPMINativeDriver
|
agent_pyghmi = ironic.drivers.agent:AgentAndIPMINativeDriver
|
||||||
agent_ssh = ironic.drivers.agent:AgentAndSSHDriver
|
agent_ssh = ironic.drivers.agent:AgentAndSSHDriver
|
||||||
agent_vbox = ironic.drivers.agent:AgentAndVirtualBoxDriver
|
|
||||||
agent_ucs = ironic.drivers.agent:AgentAndUcsDriver
|
agent_ucs = ironic.drivers.agent:AgentAndUcsDriver
|
||||||
fake = ironic.drivers.fake:FakeDriver
|
fake = ironic.drivers.fake:FakeDriver
|
||||||
fake_soft_power = ironic.drivers.fake:FakeSoftPowerDriver
|
fake_soft_power = ironic.drivers.fake:FakeSoftPowerDriver
|
||||||
@ -60,13 +59,10 @@ ironic.drivers =
|
|||||||
fake_ipminative = ironic.drivers.fake:FakeIPMINativeDriver
|
fake_ipminative = ironic.drivers.fake:FakeIPMINativeDriver
|
||||||
fake_ssh = ironic.drivers.fake:FakeSSHDriver
|
fake_ssh = ironic.drivers.fake:FakeSSHDriver
|
||||||
fake_pxe = ironic.drivers.fake:FakePXEDriver
|
fake_pxe = ironic.drivers.fake:FakePXEDriver
|
||||||
fake_seamicro = ironic.drivers.fake:FakeSeaMicroDriver
|
|
||||||
fake_ilo = ironic.drivers.fake:FakeIloDriver
|
fake_ilo = ironic.drivers.fake:FakeIloDriver
|
||||||
fake_drac = ironic.drivers.fake:FakeDracDriver
|
fake_drac = ironic.drivers.fake:FakeDracDriver
|
||||||
fake_snmp = ironic.drivers.fake:FakeSNMPDriver
|
fake_snmp = ironic.drivers.fake:FakeSNMPDriver
|
||||||
fake_irmc = ironic.drivers.fake:FakeIRMCDriver
|
fake_irmc = ironic.drivers.fake:FakeIRMCDriver
|
||||||
fake_vbox = ironic.drivers.fake:FakeVirtualBoxDriver
|
|
||||||
fake_msftocs = ironic.drivers.fake:FakeMSFTOCSDriver
|
|
||||||
fake_ucs = ironic.drivers.fake:FakeUcsDriver
|
fake_ucs = ironic.drivers.fake:FakeUcsDriver
|
||||||
fake_cimc = ironic.drivers.fake:FakeCIMCDriver
|
fake_cimc = ironic.drivers.fake:FakeCIMCDriver
|
||||||
fake_oneview = ironic.drivers.fake:FakeOneViewDriver
|
fake_oneview = ironic.drivers.fake:FakeOneViewDriver
|
||||||
@ -77,14 +73,11 @@ ironic.drivers =
|
|||||||
pxe_ipmitool_socat = ironic.drivers.ipmi:PXEAndIPMIToolAndSocatDriver
|
pxe_ipmitool_socat = ironic.drivers.ipmi:PXEAndIPMIToolAndSocatDriver
|
||||||
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
|
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
|
||||||
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver
|
pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver
|
||||||
pxe_vbox = ironic.drivers.pxe:PXEAndVirtualBoxDriver
|
|
||||||
pxe_seamicro = ironic.drivers.pxe:PXEAndSeaMicroDriver
|
|
||||||
pxe_ilo = ironic.drivers.pxe:PXEAndIloDriver
|
pxe_ilo = ironic.drivers.pxe:PXEAndIloDriver
|
||||||
pxe_drac = ironic.drivers.drac:PXEDracDriver
|
pxe_drac = ironic.drivers.drac:PXEDracDriver
|
||||||
pxe_drac_inspector = ironic.drivers.drac:PXEDracInspectorDriver
|
pxe_drac_inspector = ironic.drivers.drac:PXEDracInspectorDriver
|
||||||
pxe_snmp = ironic.drivers.pxe:PXEAndSNMPDriver
|
pxe_snmp = ironic.drivers.pxe:PXEAndSNMPDriver
|
||||||
pxe_irmc = ironic.drivers.pxe:PXEAndIRMCDriver
|
pxe_irmc = ironic.drivers.pxe:PXEAndIRMCDriver
|
||||||
pxe_msftocs = ironic.drivers.pxe:PXEAndMSFTOCSDriver
|
|
||||||
pxe_ucs = ironic.drivers.pxe:PXEAndUcsDriver
|
pxe_ucs = ironic.drivers.pxe:PXEAndUcsDriver
|
||||||
pxe_iscsi_cimc = ironic.drivers.pxe:PXEAndCIMCDriver
|
pxe_iscsi_cimc = ironic.drivers.pxe:PXEAndCIMCDriver
|
||||||
pxe_agent_cimc = ironic.drivers.agent:AgentAndCIMCDriver
|
pxe_agent_cimc = ironic.drivers.agent:AgentAndCIMCDriver
|
||||||
|
Loading…
x
Reference in New Issue
Block a user