Merge "Add XClarity Driver"
This commit is contained in:
commit
83e3afbdee
@ -13,6 +13,7 @@ python-ilorest-library>=2.1.0
|
||||
hpOneView>=4.4.0
|
||||
UcsSdk==0.8.2.2
|
||||
python-dracclient>=1.3.0
|
||||
python-xclarityclient>=0.1.6
|
||||
|
||||
# The CIMC drivers use the Cisco IMC SDK version 0.7.2 or greater
|
||||
ImcSdk>=0.7.2
|
||||
|
@ -4233,3 +4233,24 @@
|
||||
# for endpoint URL discovery. Mutually exclusive with
|
||||
# min_version and max_version (string value)
|
||||
#version = <None>
|
||||
|
||||
|
||||
[xclarity]
|
||||
|
||||
#
|
||||
# From ironic
|
||||
#
|
||||
|
||||
# IP address of XClarity controller. (string value)
|
||||
#manager_ip = <None>
|
||||
|
||||
# Username to access the XClarity controller. (string value)
|
||||
#username = <None>
|
||||
|
||||
# Password for XClarity controller username. (string value)
|
||||
#password = <None>
|
||||
|
||||
# Port to be used for XClarity operations. (port value)
|
||||
# Minimum value: 0
|
||||
# Maximum value: 65535
|
||||
#port = 443
|
||||
|
@ -44,6 +44,7 @@ from ironic.conf import redfish
|
||||
from ironic.conf import service_catalog
|
||||
from ironic.conf import snmp
|
||||
from ironic.conf import swift
|
||||
from ironic.conf import xclarity
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
@ -76,3 +77,4 @@ redfish.register_opts(CONF)
|
||||
service_catalog.register_opts(CONF)
|
||||
snmp.register_opts(CONF)
|
||||
swift.register_opts(CONF)
|
||||
xclarity.register_opts(CONF)
|
||||
|
@ -61,6 +61,7 @@ _opts = [
|
||||
('service_catalog', ironic.conf.service_catalog.list_opts()),
|
||||
('snmp', ironic.conf.snmp.opts),
|
||||
('swift', ironic.conf.swift.list_opts()),
|
||||
('xclarity', ironic.conf.xclarity.opts),
|
||||
]
|
||||
|
||||
|
||||
|
33
ironic/conf/xclarity.py
Normal file
33
ironic/conf/xclarity.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Copyright 2017 LENOVO Development Company, LP
|
||||
#
|
||||
# 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.StrOpt('manager_ip',
|
||||
help=_('IP address of XClarity controller.')),
|
||||
cfg.StrOpt('username',
|
||||
help=_('Username to access the XClarity controller.')),
|
||||
cfg.StrOpt('password',
|
||||
help=_('Password for XClarity controller username.')),
|
||||
cfg.PortOpt('port',
|
||||
default=443,
|
||||
help=_('Port to be used for XClarity operations.')),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_opts(opts, group='xclarity')
|
0
ironic/drivers/modules/xclarity/__init__.py
Normal file
0
ironic/drivers/modules/xclarity/__init__.py
Normal file
138
ironic/drivers/modules/xclarity/common.py
Normal file
138
ironic/drivers/modules/xclarity/common.py
Normal file
@ -0,0 +1,138 @@
|
||||
# Copyright 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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_log import log as logging
|
||||
from oslo_utils import importutils
|
||||
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import states
|
||||
from ironic.conf import CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
client = importutils.try_import('xclarity_client.client')
|
||||
xclarity_client_constants = importutils.try_import('xclarity_client.constants')
|
||||
xclarity_client_exceptions = importutils.try_import(
|
||||
'xclarity_client.exceptions')
|
||||
|
||||
REQUIRED_ON_DRIVER_INFO = {
|
||||
'xclarity_hardware_id': _("XClarity Server Hardware ID. "
|
||||
"Required in driver_info."),
|
||||
}
|
||||
|
||||
COMMON_PROPERTIES = {
|
||||
'xclarity_address': _("IP address of the XClarity node."),
|
||||
'xclarity_username': _("Username for the XClarity with administrator "
|
||||
"privileges."),
|
||||
'xclarity_password': _("Password for xclarity_username."),
|
||||
'xclarity_port': _("Port to be used for xclarity_username."),
|
||||
}
|
||||
|
||||
COMMON_PROPERTIES.update(REQUIRED_ON_DRIVER_INFO)
|
||||
|
||||
|
||||
def get_properties():
|
||||
return COMMON_PROPERTIES
|
||||
|
||||
|
||||
def get_xclarity_client():
|
||||
"""Generates an instance of the XClarity client.
|
||||
|
||||
Generates an instance of the XClarity client using the imported
|
||||
xclarity_client library.
|
||||
|
||||
:returns: an instance of the XClarity client
|
||||
:raises: XClarityError if can't get to the XClarity client
|
||||
"""
|
||||
try:
|
||||
xclarity_client = client.Client(
|
||||
ip=CONF.xclarity.manager_ip,
|
||||
username=CONF.xclarity.username,
|
||||
password=CONF.xclarity.password,
|
||||
port=CONF.xclarity.port
|
||||
)
|
||||
except xclarity_client_exceptions.XClarityError as exc:
|
||||
msg = (_("Error getting connection to XClarity manager IP: %(ip)s. "
|
||||
"Error: %(exc)s"), {'ip': CONF.xclarity.manager_ip,
|
||||
'exc': exc})
|
||||
raise XClarityError(error=msg)
|
||||
return xclarity_client
|
||||
|
||||
|
||||
def get_server_hardware_id(node):
|
||||
"""Validates node configuration and returns xclarity hardware id.
|
||||
|
||||
Validates whether node configutation is consistent with XClarity and
|
||||
returns the XClarity Hardware ID for a specific node.
|
||||
:param: node: node object to get information from
|
||||
:returns: the XClarity Hardware ID for a specific node
|
||||
:raises: MissingParameterValue if unable to validate XClarity Hardware ID
|
||||
|
||||
"""
|
||||
xclarity_hardware_id = node.driver_info.get('xclarity_hardware_id')
|
||||
if not xclarity_hardware_id:
|
||||
msg = (_("Error validating node driver info, "
|
||||
"server uuid: %s missing xclarity_hardware_id") %
|
||||
node.uuid)
|
||||
raise exception.MissingParameterValue(error=msg)
|
||||
return xclarity_hardware_id
|
||||
|
||||
|
||||
def translate_xclarity_power_state(power_state):
|
||||
"""Translates XClarity's power state strings to be consistent with Ironic.
|
||||
|
||||
:param: power_state: power state string to be translated
|
||||
:returns: the translated power state
|
||||
"""
|
||||
power_states_map = {
|
||||
xclarity_client_constants.STATE_POWER_ON: states.POWER_ON,
|
||||
xclarity_client_constants.STATE_POWER_OFF: states.POWER_OFF,
|
||||
}
|
||||
|
||||
return power_states_map.get(power_state, states.ERROR)
|
||||
|
||||
|
||||
def translate_xclarity_power_action(power_action):
|
||||
"""Translates ironic's power action strings to XClarity's format.
|
||||
|
||||
:param: power_action: power action string to be translated
|
||||
:returns: the power action translated
|
||||
"""
|
||||
|
||||
power_action_map = {
|
||||
states.POWER_ON: xclarity_client_constants.ACTION_POWER_ON,
|
||||
states.POWER_OFF: xclarity_client_constants.ACTION_POWER_OFF,
|
||||
states.REBOOT: xclarity_client_constants.ACTION_REBOOT
|
||||
}
|
||||
|
||||
return power_action_map[power_action]
|
||||
|
||||
|
||||
def is_node_managed_by_xclarity(xclarity_client, node):
|
||||
"""Determines whether dynamic allocation is enabled for a specifc node.
|
||||
|
||||
:param: xclarity_client: an instance of the XClarity client
|
||||
:param: node: node object to get information from
|
||||
:returns: Boolean depending on whether node is managed by XClarity
|
||||
"""
|
||||
try:
|
||||
hardware_id = get_server_hardware_id(node)
|
||||
return xclarity_client.is_node_managed(hardware_id)
|
||||
except exception.MissingParameterValue:
|
||||
return False
|
||||
|
||||
|
||||
class XClarityError(exception.IronicException):
|
||||
_msg_fmt = _("XClarity exception occurred. Error: %(error)s")
|
219
ironic/drivers/modules/xclarity/management.py
Normal file
219
ironic/drivers/modules/xclarity/management.py
Normal file
@ -0,0 +1,219 @@
|
||||
# Copyright 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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_lib import metrics_utils
|
||||
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 _
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers import base
|
||||
from ironic.drivers.modules.xclarity import common
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
||||
xclarity_client_exceptions = importutils.try_import(
|
||||
'xclarity_client.exceptions')
|
||||
|
||||
BOOT_DEVICE_MAPPING_TO_XCLARITY = {
|
||||
boot_devices.PXE: 'PXE Network',
|
||||
boot_devices.DISK: 'Hard Disk 0',
|
||||
boot_devices.CDROM: 'CD/DVD Rom',
|
||||
boot_devices.BIOS: 'Boot To F1'
|
||||
}
|
||||
|
||||
SUPPORTED_BOOT_DEVICES = [
|
||||
boot_devices.PXE,
|
||||
boot_devices.DISK,
|
||||
boot_devices.CDROM,
|
||||
boot_devices.BIOS,
|
||||
]
|
||||
|
||||
|
||||
class XClarityManagement(base.ManagementInterface):
|
||||
def __init__(self):
|
||||
super(XClarityManagement, self).__init__()
|
||||
self.xclarity_client = common.get_xclarity_client()
|
||||
|
||||
def get_properties(self):
|
||||
return common.COMMON_PROPERTIES
|
||||
|
||||
@METRICS.timer('XClarityManagement.validate')
|
||||
def validate(self, task):
|
||||
"""It validates if the node is being used by XClarity.
|
||||
|
||||
:param task: a task from TaskManager.
|
||||
"""
|
||||
common.is_node_managed_by_xclarity(self.xclarity_client, task.node)
|
||||
|
||||
@METRICS.timer('XClarityManagement.get_supported_boot_devices')
|
||||
def get_supported_boot_devices(self, task):
|
||||
"""Gets 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 SUPPORTED_BOOT_DEVICES
|
||||
|
||||
def _validate_supported_boot_device(self, task, boot_device):
|
||||
"""It validates if the boot device is supported by XClarity.
|
||||
|
||||
:param task: a task from TaskManager.
|
||||
:param boot_device: the boot device, one of [PXE, DISK, CDROM, BIOS]
|
||||
:raises: InvalidParameterValue if the boot device is not supported.
|
||||
"""
|
||||
if boot_device not in SUPPORTED_BOOT_DEVICES:
|
||||
raise exception.InvalidParameterValue(
|
||||
_("Unsupported boot device %(device)s for node: %(node)s ")
|
||||
% {"device": boot_device, "node": task.node.uuid}
|
||||
)
|
||||
|
||||
@METRICS.timer('XClarityManagement.get_boot_device')
|
||||
def get_boot_device(self, task):
|
||||
"""Get the current boot device for the task's node.
|
||||
|
||||
:param task: a task from TaskManager.
|
||||
:returns: a dictionary containing:
|
||||
:boot_device: the boot device, one of [PXE, DISK, CDROM, BIOS]
|
||||
:persistent: Whether the boot device will persist or not
|
||||
:raises: InvalidParameterValue if the boot device is unknown
|
||||
:raises: XClarityError if the communication with XClarity fails
|
||||
"""
|
||||
server_hardware_id = common.get_server_hardware_id(task.node)
|
||||
try:
|
||||
boot_info = (
|
||||
self.xclarity_client.get_node_all_boot_info(
|
||||
server_hardware_id)
|
||||
)
|
||||
except xclarity_client_exceptions.XClarityError as xclarity_exc:
|
||||
LOG.error(
|
||||
"Error getting boot device from XClarity for node %(node)s. "
|
||||
"Error: %(error)s", {'node': task.node.uuid,
|
||||
'error': xclarity_exc})
|
||||
raise common.XClarityError(error=xclarity_exc)
|
||||
|
||||
persistent = False
|
||||
primary = None
|
||||
boot_order = boot_info['bootOrder']['bootOrderList']
|
||||
for item in boot_order:
|
||||
current = item.get('currentBootOrderDevices', None)
|
||||
boot_type = item.get('bootType', None)
|
||||
if boot_type == "SingleUse":
|
||||
persistent = False
|
||||
primary = current[0]
|
||||
if primary != 'None':
|
||||
boot_device = {'boot_device': primary,
|
||||
'persistent': persistent}
|
||||
self._validate_whether_supported_boot_device(primary)
|
||||
return boot_device
|
||||
elif boot_type == "Permanent":
|
||||
persistent = True
|
||||
boot_device = {'boot_device': current[0],
|
||||
'persistent': persistent}
|
||||
self._validate_supported_boot_device(task, primary)
|
||||
return boot_device
|
||||
|
||||
@METRICS.timer('XClarityManagement.set_boot_device')
|
||||
@task_manager.require_exclusive_lock
|
||||
def set_boot_device(self, task, device, persistent=False):
|
||||
"""Sets the boot device for a node.
|
||||
|
||||
:param task: a task from TaskManager.
|
||||
:param device: the boot device, one of the supported devices
|
||||
listed in :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.
|
||||
:raises: InvalidParameterValue if an invalid boot device is
|
||||
specified.
|
||||
:raises: XClarityError if the communication with XClarity fails
|
||||
"""
|
||||
self._validate_supported_boot_device(task=task, boot_device=device)
|
||||
|
||||
server_hardware_id = task.node.driver_info.get('server_hardware_id')
|
||||
LOG.debug("Setting boot device to %(device)s for node %(node)s",
|
||||
{"device": device, "node": task.node.uuid})
|
||||
self._set_boot_device(task, server_hardware_id, device,
|
||||
singleuse=not persistent)
|
||||
|
||||
@METRICS.timer('XClarityManagement.get_sensors_data')
|
||||
def get_sensors_data(self, task):
|
||||
"""Get sensors data.
|
||||
|
||||
:param task: a TaskManager instance.
|
||||
:raises: NotImplementedError
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _translate_ironic_to_xclarity(self, boot_device):
|
||||
"""Translates Ironic boot options to Xclarity boot options.
|
||||
|
||||
:param boot_device: Ironic boot_device
|
||||
:returns: Translated XClarity boot_device.
|
||||
|
||||
"""
|
||||
return BOOT_DEVICE_MAPPING_TO_XCLARITY.get(boot_device)
|
||||
|
||||
def _set_boot_device(self, task, server_hardware_id,
|
||||
new_primary_boot_device, singleuse=False):
|
||||
"""Set the current boot device for xclarity
|
||||
|
||||
:param server_hardware_id: the uri of the server hardware in XClarity
|
||||
:param new_primary_boot_device: boot device to be set
|
||||
:param task: a TaskManager instance.
|
||||
:param singleuse: if this device will be used only once at next boot
|
||||
"""
|
||||
boot_info = self.xclarity_client.get_node_all_boot_info(
|
||||
server_hardware_id)
|
||||
xclarity_boot_device = self._translate_ironic_to_xclarity(
|
||||
new_primary_boot_device)
|
||||
current = []
|
||||
LOG.debug(
|
||||
("Setting boot device to %(device)s for XClarity "
|
||||
"node %(node)s"),
|
||||
{'device': xclarity_boot_device, 'node': task.node.uuid}
|
||||
)
|
||||
for item in boot_info['bootOrder']['bootOrderList']:
|
||||
if singleuse and item['bootType'] == 'SingleUse':
|
||||
item['currentBootOrderDevices'][0] = xclarity_boot_device
|
||||
elif not singleuse and item['bootType'] == 'Permanent':
|
||||
current = item['currentBootOrderDevices']
|
||||
if xclarity_boot_device == current[0]:
|
||||
return
|
||||
if xclarity_boot_device in current:
|
||||
current.remove(xclarity_boot_device)
|
||||
current.insert(0, xclarity_boot_device)
|
||||
item['currentBootOrderDevices'] = current
|
||||
|
||||
try:
|
||||
self.xclarity_client.set_node_boot_info(server_hardware_id,
|
||||
boot_info,
|
||||
xclarity_boot_device,
|
||||
singleuse)
|
||||
except xclarity_client_exceptions.XClarityError as xclarity_exc:
|
||||
LOG.error(
|
||||
('Error setting boot device %(boot_device)s for the XClarity '
|
||||
'node %(node)s. Error: %(error)s'),
|
||||
{'boot_device': xclarity_boot_device, 'node': task.node.uuid,
|
||||
'error': xclarity_exc}
|
||||
)
|
||||
raise common.XClarityError(error=xclarity_exc)
|
112
ironic/drivers/modules/xclarity/power.py
Normal file
112
ironic/drivers/modules/xclarity/power.py
Normal file
@ -0,0 +1,112 @@
|
||||
# Copyright 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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_lib import metrics_utils
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import importutils
|
||||
|
||||
from ironic.common import states
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers import base
|
||||
from ironic.drivers.modules.xclarity import common
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
METRICS = metrics_utils.get_metrics_logger(__name__)
|
||||
|
||||
xclarity_client_exceptions = importutils.try_import(
|
||||
'xclarity_client.exceptions')
|
||||
|
||||
|
||||
class XClarityPower(base.PowerInterface):
|
||||
def __init__(self):
|
||||
super(XClarityPower, self).__init__()
|
||||
self.xclarity_client = common.get_xclarity_client()
|
||||
|
||||
def get_properties(self):
|
||||
return common.get_properties()
|
||||
|
||||
@METRICS.timer('XClarityPower.validate')
|
||||
def validate(self, task):
|
||||
"""It validates if the node is being used by XClarity.
|
||||
|
||||
:param task: a task from TaskManager.
|
||||
"""
|
||||
|
||||
common.is_node_managed_by_xclarity(self.xclarity_client, task.node)
|
||||
|
||||
@METRICS.timer('XClarityPower.get_power_state')
|
||||
def get_power_state(self, task):
|
||||
"""Gets the current power state.
|
||||
|
||||
:param task: a TaskManager instance.
|
||||
:returns: one of :mod:`ironic.common.states` POWER_OFF,
|
||||
POWER_ON or ERROR.
|
||||
:raises: XClarityError if fails to retrieve power state of XClarity
|
||||
resource
|
||||
"""
|
||||
server_hardware_id = common.get_server_hardware_id(task.node)
|
||||
try:
|
||||
power_state = self.xclarity_client.get_node_power_status(
|
||||
server_hardware_id)
|
||||
except xclarity_client_exceptions.XClarityException as xclarity_exc:
|
||||
LOG.error(
|
||||
("Error getting power state for node %(node)s. Error: "
|
||||
"%(error)s"),
|
||||
{'node': task.node.uuid, 'error': xclarity_exc}
|
||||
)
|
||||
raise common.XClarityError(error=xclarity_exc)
|
||||
return common.translate_xclarity_power_state(power_state)
|
||||
|
||||
@METRICS.timer('XClarityPower.set_power_state')
|
||||
@task_manager.require_exclusive_lock
|
||||
def set_power_state(self, task, power_state):
|
||||
"""Turn the current power state on or off.
|
||||
|
||||
:param task: a TaskManager instance.
|
||||
:param power_state: The desired power state POWER_ON, POWER_OFF or
|
||||
REBOOT from :mod:`ironic.common.states`.
|
||||
:raises: InvalidParameterValue if an invalid power state was specified.
|
||||
:raises: XClarityError if XClarity fails setting the power state.
|
||||
"""
|
||||
|
||||
if power_state == states.REBOOT:
|
||||
target_power_state = self.get_power_state(task)
|
||||
if target_power_state == states.POWER_OFF:
|
||||
power_state = states.POWER_ON
|
||||
|
||||
server_hardware_id = common.get_server_hardware_id(task.node)
|
||||
LOG.debug("Setting power state of node %(node_uuid)s to "
|
||||
"%(power_state)s",
|
||||
{'node_uuid': task.node.uuid, 'power_state': power_state})
|
||||
|
||||
try:
|
||||
self.xclarity_client.set_node_power_status(server_hardware_id,
|
||||
power_state)
|
||||
except xclarity_client_exceptions.XClarityError as xclarity_exc:
|
||||
LOG.error(
|
||||
"Error setting power state of node %(node_uuid)s to "
|
||||
"%(power_state)s",
|
||||
{'node_uuid': task.node.uuid, 'power_state': power_state})
|
||||
raise common.XClarityError(error=xclarity_exc)
|
||||
|
||||
@METRICS.timer('XClarityPower.reboot')
|
||||
@task_manager.require_exclusive_lock
|
||||
def reboot(self, task):
|
||||
"""Reboot the node
|
||||
|
||||
:param task: a TaskManager instance.
|
||||
"""
|
||||
|
||||
self.set_power_state(task, states.REBOOT)
|
35
ironic/drivers/xclarity.py
Normal file
35
ironic/drivers/xclarity.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Copyright 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
XClarity Driver and supporting meta-classes.
|
||||
"""
|
||||
|
||||
from ironic.drivers import generic
|
||||
from ironic.drivers.modules.xclarity import management
|
||||
from ironic.drivers.modules.xclarity import power
|
||||
|
||||
|
||||
class XClarityHardware(generic.GenericHardware):
|
||||
"""XClarity hardware type. """
|
||||
|
||||
@property
|
||||
def supported_management_interfaces(self):
|
||||
"""List of supported management interfaces."""
|
||||
return [management.XClarityManagement]
|
||||
|
||||
@property
|
||||
def supported_power_interfaces(self):
|
||||
"""List of supported power interfaces."""
|
||||
return [power.XClarityPower]
|
@ -491,6 +491,21 @@ def create_test_node_tag(**kw):
|
||||
return dbapi.add_node_tag(tag['node_id'], tag['tag'])
|
||||
|
||||
|
||||
def get_test_xclarity_properties():
|
||||
return {
|
||||
"cpu_arch": "x86_64",
|
||||
"cpus": "8",
|
||||
"local_gb": "10",
|
||||
"memory_mb": "4096",
|
||||
}
|
||||
|
||||
|
||||
def get_test_xclarity_driver_info():
|
||||
return {
|
||||
'xclarity_hardware_id': 'fake_sh_id',
|
||||
}
|
||||
|
||||
|
||||
def get_test_node_trait(**kw):
|
||||
return {
|
||||
# TODO(mgoddard): Replace None below with the NodeTrait RPC object
|
||||
|
65
ironic/tests/unit/drivers/modules/xclarity/test_common.py
Normal file
65
ironic/tests/unit/drivers/modules/xclarity/test_common.py
Normal file
@ -0,0 +1,65 @@
|
||||
# Copyright 2017 Lenovo, Inc.
|
||||
# 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 mock
|
||||
|
||||
from oslo_utils import importutils
|
||||
|
||||
from ironic.drivers.modules.xclarity import common
|
||||
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
|
||||
|
||||
xclarity_exceptions = importutils.try_import('xclarity_client.exceptions')
|
||||
xclarity_constants = importutils.try_import('xclarity_client.constants')
|
||||
|
||||
|
||||
class XClarityCommonTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(XClarityCommonTestCase, self).setUp()
|
||||
|
||||
self.config(manager_ip='1.2.3.4', group='xclarity')
|
||||
self.config(username='user', group='xclarity')
|
||||
self.config(password='password', group='xclarity')
|
||||
|
||||
self.node = obj_utils.create_test_node(
|
||||
self.context, driver='fake-xclarity',
|
||||
properties=db_utils.get_test_xclarity_properties(),
|
||||
driver_info=db_utils.get_test_xclarity_driver_info(),
|
||||
)
|
||||
|
||||
def test_get_server_hardware_id(self):
|
||||
driver_info = self.node.driver_info
|
||||
driver_info['xclarity_hardware_id'] = 'test'
|
||||
self.node.driver_info = driver_info
|
||||
result = common.get_server_hardware_id(self.node)
|
||||
self.assertEqual(result, 'test')
|
||||
|
||||
@mock.patch.object(common, 'get_server_hardware_id',
|
||||
spec_set=True, autospec=True)
|
||||
@mock.patch.object(common, 'get_xclarity_client',
|
||||
spec_set=True, autospec=True)
|
||||
def test_check_node_managed_by_xclarity(self, mock_xc_client,
|
||||
mock_validate_driver_info):
|
||||
driver_info = self.node.driver_info
|
||||
driver_info['xclarity_hardware_id'] = 'abcd'
|
||||
self.node.driver_info = driver_info
|
||||
|
||||
xclarity_client = mock_xc_client()
|
||||
mock_validate_driver_info.return_value = '12345'
|
||||
common.is_node_managed_by_xclarity(xclarity_client,
|
||||
self.node)
|
||||
xclarity_client.is_node_managed.assert_called_once_with('12345')
|
125
ironic/tests/unit/drivers/modules/xclarity/test_management.py
Normal file
125
ironic/tests/unit/drivers/modules/xclarity/test_management.py
Normal file
@ -0,0 +1,125 @@
|
||||
# Copyright 2017 Lenovo, Inc.
|
||||
# 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 sys
|
||||
|
||||
import six
|
||||
|
||||
import mock
|
||||
|
||||
from oslo_utils import importutils
|
||||
|
||||
from ironic.common import boot_devices
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers.modules.xclarity import common
|
||||
from ironic.drivers.modules.xclarity import management
|
||||
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
|
||||
|
||||
|
||||
xclarity_client_exceptions = importutils.try_import(
|
||||
'xclarity_client.exceptions')
|
||||
|
||||
|
||||
@mock.patch.object(common, 'get_xclarity_client', spect_set=True,
|
||||
autospec=True)
|
||||
class XClarityManagementDriverTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(XClarityManagementDriverTestCase, self).setUp()
|
||||
self.config(enabled_hardware_types=['xclarity'],
|
||||
enabled_power_interfaces=['xclarity'],
|
||||
enabled_management_interfaces=['xclarity'])
|
||||
mgr_utils.mock_the_extension_manager(
|
||||
driver='xclarity', namespace='ironic.hardware.types')
|
||||
self.node = obj_utils.create_test_node(
|
||||
self.context,
|
||||
driver='xclarity',
|
||||
driver_info=db_utils.get_test_xclarity_driver_info())
|
||||
|
||||
@mock.patch.object(common, 'get_server_hardware_id',
|
||||
spect_set=True, autospec=True)
|
||||
def test_validate(self, mock_validate, mock_get_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.driver.management.validate(task)
|
||||
common.get_server_hardware_id(task.node)
|
||||
mock_validate.assert_called_with(task.node)
|
||||
|
||||
def test_get_properties(self, mock_get_xc_client):
|
||||
|
||||
expected = common.REQUIRED_ON_DRIVER_INFO
|
||||
self.assertItemsEqual(expected,
|
||||
self.node.driver_info)
|
||||
|
||||
@mock.patch.object(management.XClarityManagement, 'get_boot_device',
|
||||
return_value='pxe')
|
||||
def test_set_boot_device(self, mock_get_boot_device,
|
||||
mock_get_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.driver.management.set_boot_device(task, 'pxe')
|
||||
result = task.driver.management.get_boot_device(task)
|
||||
self.assertEqual(result, 'pxe')
|
||||
|
||||
def test_set_boot_device_fail(self, mock_get_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
xclarity_client_exceptions.XClarityError = Exception
|
||||
sys.modules['xclarity_client.exceptions'] = (
|
||||
xclarity_client_exceptions)
|
||||
if 'ironic.drivers.modules.xclarity' in sys.modules:
|
||||
six.moves.reload_module(
|
||||
sys.modules['ironic.drivers.modules.xclarity'])
|
||||
ex = common.XClarityError('E')
|
||||
mock_get_xc_client.return_value.set_node_boot_info.side_effect = ex
|
||||
self.assertRaises(common.XClarityError,
|
||||
task.driver.management.set_boot_device,
|
||||
task,
|
||||
"pxe")
|
||||
|
||||
def test_get_supported_boot_devices(self, mock_get_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
expected = [boot_devices.PXE, boot_devices.BIOS,
|
||||
boot_devices.DISK, boot_devices.CDROM]
|
||||
self.assertItemsEqual(
|
||||
expected,
|
||||
task.driver.management.get_supported_boot_devices(task))
|
||||
|
||||
@mock.patch.object(
|
||||
management.XClarityManagement,
|
||||
'get_boot_device',
|
||||
return_value={'boot_device': 'pxe', 'persistent': False})
|
||||
def test_get_boot_device(self, mock_get_boot_device, mock_get_xc_client):
|
||||
reference = {'boot_device': 'pxe', 'persistent': False}
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
expected_boot_device = task.driver.management.get_boot_device(
|
||||
task=task)
|
||||
|
||||
self.assertEqual(reference, expected_boot_device)
|
||||
|
||||
def test_get_boot_device_fail(self, mock_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
xclarity_client_exceptions.XClarityError = Exception
|
||||
sys.modules['xclarity_client.exceptions'] = (
|
||||
xclarity_client_exceptions)
|
||||
if 'ironic.drivers.modules.xclarity' in sys.modules:
|
||||
six.moves.reload_module(
|
||||
sys.modules['ironic.drivers.modules.xclarity'])
|
||||
ex = common.XClarityError('E')
|
||||
mock_xc_client.return_value.get_node_all_boot_info.side_effect = ex
|
||||
self.assertRaises(
|
||||
common.XClarityError,
|
||||
task.driver.management.get_boot_device,
|
||||
task)
|
113
ironic/tests/unit/drivers/modules/xclarity/test_power.py
Normal file
113
ironic/tests/unit/drivers/modules/xclarity/test_power.py
Normal file
@ -0,0 +1,113 @@
|
||||
# Copyright 2017 Lenovo, Inc.
|
||||
# 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.
|
||||
|
||||
STATE_POWER_ON = "power on"
|
||||
STATE_POWER_OFF = "power off"
|
||||
STATE_POWERING_ON = "power on"
|
||||
STATE_POWERING_OFF = "power on"
|
||||
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
import mock
|
||||
|
||||
from oslo_utils import importutils
|
||||
|
||||
from ironic.common import states
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers.modules.xclarity import common
|
||||
from ironic.drivers.modules.xclarity import power
|
||||
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
|
||||
|
||||
xclarity_constants = importutils.try_import('xclarity_client.constants')
|
||||
xclarity_client_exceptions = importutils.try_import(
|
||||
'xclarity_client.exceptions')
|
||||
|
||||
|
||||
@mock.patch.object(common, 'get_xclarity_client',
|
||||
spect_set=True, autospec=True)
|
||||
class XClarityPowerDriverTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(XClarityPowerDriverTestCase, self).setUp()
|
||||
self.config(enabled_hardware_types=['xclarity'],
|
||||
enabled_power_interfaces=['xclarity'],
|
||||
enabled_management_interfaces=['xclarity'])
|
||||
mgr_utils.mock_the_extension_manager(
|
||||
driver='xclarity', namespace='ironic.hardware.types')
|
||||
self.node = obj_utils.create_test_node(
|
||||
self.context,
|
||||
driver='xclarity',
|
||||
driver_info=db_utils.get_test_xclarity_driver_info())
|
||||
|
||||
def test_get_properties(self, mock_get_xc_client):
|
||||
expected = common.REQUIRED_ON_DRIVER_INFO
|
||||
self.assertItemsEqual(expected,
|
||||
self.node.driver_info)
|
||||
|
||||
@mock.patch.object(common, 'get_server_hardware_id',
|
||||
spect_set=True, autospec=True)
|
||||
def test_validate(self, mock_validate_driver_info, mock_get_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.driver.power.validate(task)
|
||||
common.get_server_hardware_id(task.node)
|
||||
mock_validate_driver_info.assert_called_with(task.node)
|
||||
|
||||
@mock.patch.object(power.XClarityPower, 'get_power_state',
|
||||
return_value=STATE_POWER_ON)
|
||||
def test_get_power_state(self, mock_get_power_state, mock_get_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
result = power.XClarityPower.get_power_state(task)
|
||||
self.assertEqual(STATE_POWER_ON, result)
|
||||
|
||||
def test_get_power_state_fail(self, mock_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
xclarity_client_exceptions.XClarityError = Exception
|
||||
sys.modules['xclarity_client.exceptions'] = (
|
||||
xclarity_client_exceptions)
|
||||
if 'ironic.drivers.modules.xclarity' in sys.modules:
|
||||
six.moves.reload_module(
|
||||
sys.modules['ironic.drivers.modules.xclarity'])
|
||||
ex = common.XClarityError('E')
|
||||
mock_xc_client.return_value.get_node_power_status.side_effect = ex
|
||||
self.assertRaises(common.XClarityError,
|
||||
task.driver.power.get_power_state,
|
||||
task)
|
||||
|
||||
@mock.patch.object(power.XClarityPower, 'get_power_state',
|
||||
return_value=states.POWER_ON)
|
||||
def test_set_power(self, mock_set_power_state, mock_get_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
task.driver.power.set_power_state(task, states.POWER_ON)
|
||||
expected = task.driver.power.get_power_state(task)
|
||||
self.assertEqual(expected, states.POWER_ON)
|
||||
|
||||
def test_set_power_fail(self, mock_xc_client):
|
||||
with task_manager.acquire(self.context, self.node.uuid) as task:
|
||||
xclarity_client_exceptions.XClarityError = Exception
|
||||
sys.modules['xclarity_client.exceptions'] = (
|
||||
xclarity_client_exceptions)
|
||||
if 'ironic.drivers.modules.xclarity' in sys.modules:
|
||||
six.moves.reload_module(
|
||||
sys.modules['ironic.drivers.modules.xclarity'])
|
||||
ex = common.XClarityError('E')
|
||||
mock_xc_client.return_value.set_node_power_status.side_effect = ex
|
||||
self.assertRaises(common.XClarityError,
|
||||
task.driver.power.set_power_state,
|
||||
task, states.POWER_OFF)
|
49
ironic/tests/unit/drivers/test_xclarity.py
Normal file
49
ironic/tests/unit/drivers/test_xclarity.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Copyright 2017 Lenovo, Inc.
|
||||
#
|
||||
# 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 XClarity Driver
|
||||
"""
|
||||
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers.modules import agent
|
||||
from ironic.drivers.modules import iscsi_deploy
|
||||
from ironic.drivers.modules import pxe
|
||||
from ironic.drivers.xclarity import management as xc_management
|
||||
from ironic.drivers.xclarity import power as xc_power
|
||||
|
||||
from ironic.tests.unit.db import base as db_base
|
||||
from ironic.tests.unit.objects import utils as obj_utils
|
||||
|
||||
|
||||
class XClarityHardwareTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(XClarityHardwareTestCase, self).setUp()
|
||||
self.config(enabled_hardware_types=['xclarity'],
|
||||
enabled_power_interfaces=['xclarity'],
|
||||
enabled_management_interfaces=['xclarity'])
|
||||
|
||||
def test_default_interfaces(self):
|
||||
node = obj_utils.create_test_node(self.context, driver='xclarity')
|
||||
with task_manager.acquire(self.context, node.id) as task:
|
||||
self.assertIsInstance(task.driver.boot,
|
||||
pxe.PXEBoot)
|
||||
self.assertIsInstance(task.driver.deploy,
|
||||
iscsi_deploy.ISCSIDeploy,
|
||||
agent.AgentDeploy)
|
||||
self.assertIsInstance(task.driver.management,
|
||||
xc_management.XClarityManagement)
|
||||
self.assertIsInstance(task.driver.power,
|
||||
xc_power.XClarityPower)
|
@ -180,3 +180,21 @@ SUSHY_CONSTANTS_SPEC = (
|
||||
'BOOT_SOURCE_ENABLED_CONTINUOUS',
|
||||
'BOOT_SOURCE_ENABLED_ONCE',
|
||||
)
|
||||
|
||||
XCLARITY_SPEC = (
|
||||
'client',
|
||||
'states',
|
||||
'exceptions',
|
||||
'models',
|
||||
'utils',
|
||||
)
|
||||
|
||||
XCLARITY_CLIENT_CLS_SPEC = (
|
||||
)
|
||||
|
||||
XCLARITY_STATES_SPEC = (
|
||||
'STATE_POWERING_OFF',
|
||||
'STATE_POWERING_ON',
|
||||
'STATE_POWER_OFF',
|
||||
'STATE_POWER_ON',
|
||||
)
|
||||
|
@ -270,3 +270,21 @@ if not sushy:
|
||||
if 'ironic.drivers.modules.redfish' in sys.modules:
|
||||
six.moves.reload_module(
|
||||
sys.modules['ironic.drivers.modules.redfish'])
|
||||
|
||||
xclarity_client = importutils.try_import('xclarity_client')
|
||||
if not xclarity_client:
|
||||
xclarity_client = mock.MagicMock(spec_set=mock_specs.XCLARITY_SPEC)
|
||||
sys.modules['xclarity_client'] = xclarity_client
|
||||
sys.modules['xclarity_client.client'] = xclarity_client.client
|
||||
states = mock.MagicMock(
|
||||
spec_set=mock_specs.XCLARITY_STATES_SPEC,
|
||||
STATE_POWER_ON="power on",
|
||||
STATE_POWER_OFF="power off",
|
||||
STATE_POWERING_ON="powering_on",
|
||||
STATE_POWERING_OFF="powering_off")
|
||||
sys.modules['xclarity_client.states'] = states
|
||||
sys.modules['xclarity_client.exceptions'] = xclarity_client.exceptions
|
||||
sys.modules['xclarity_client.utils'] = xclarity_client.utils
|
||||
xclarity_client.exceptions.XClarityException = type('XClarityException',
|
||||
(Exception,), {})
|
||||
sys.modules['xclarity_client.models'] = xclarity_client.models
|
||||
|
9
releasenotes/notes/xclarity-driver-622800d17459e3f9.yaml
Normal file
9
releasenotes/notes/xclarity-driver-622800d17459e3f9.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
|
||||
features:
|
||||
- |
|
||||
Adds the new ``xclarity`` hardware type for managing Lenovo server
|
||||
hardware with the following interfaces:
|
||||
|
||||
* management: ``xclarity``
|
||||
* power: ``xclarity``
|
@ -127,6 +127,7 @@ ironic.hardware.interfaces.management =
|
||||
oneview = ironic.drivers.modules.oneview.management:OneViewManagement
|
||||
redfish = ironic.drivers.modules.redfish.management:RedfishManagement
|
||||
ucsm = ironic.drivers.modules.ucs.management:UcsManagement
|
||||
xclarity = ironic.drivers.modules.xclarity.management:XClarityManagement
|
||||
|
||||
ironic.hardware.interfaces.network =
|
||||
flat = ironic.drivers.modules.network.flat:FlatNetwork
|
||||
@ -144,6 +145,7 @@ ironic.hardware.interfaces.power =
|
||||
redfish = ironic.drivers.modules.redfish.power:RedfishPower
|
||||
snmp = ironic.drivers.modules.snmp:SNMPPower
|
||||
ucsm = ironic.drivers.modules.ucs.power:Power
|
||||
xclarity = ironic.drivers.modules.xclarity.power:XClarityPower
|
||||
|
||||
ironic.hardware.interfaces.raid =
|
||||
agent = ironic.drivers.modules.agent:AgentRAID
|
||||
@ -178,6 +180,7 @@ ironic.hardware.types =
|
||||
oneview = ironic.drivers.oneview:OneViewHardware
|
||||
redfish = ironic.drivers.redfish:RedfishHardware
|
||||
snmp = ironic.drivers.snmp:SNMPHardware
|
||||
xclarity = ironic.drivers.xclarity:XClarityHardware
|
||||
|
||||
ironic.database.migration_backend =
|
||||
sqlalchemy = ironic.db.sqlalchemy.migration
|
||||
|
Loading…
Reference in New Issue
Block a user