Add Intel Node Manager driver

This patch adds two new entry points with Intel Node Manager
vendor interface: "agent_ipmitool_nm" and "fake_nm".
New vendor interface supports Intel Node Manager policies.

Change-Id: Iedbb3b906cef7bd5b2d768e926a59820ccd8c196
This commit is contained in:
Yuriy Zveryanskyy 2016-02-22 18:54:07 +02:00
parent 5ad7c7c925
commit d5f031527f
15 changed files with 1690 additions and 0 deletions

View File

@ -0,0 +1,62 @@
# 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.drivers import base
from ironic.drivers.modules import agent
from ironic.drivers.modules import fake
from ironic.drivers.modules import inspector
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules import pxe
from ironic.drivers import utils
from ironic_staging_drivers.intel_nm import nm_vendor
class FakeIntelNMDriver(base.BaseDriver):
"""Fake Intel NM driver."""
def __init__(self):
self.power = fake.FakePower()
self.deploy = fake.FakeDeploy()
self.vendor = nm_vendor.IntelNMVendorPassthru()
class AgentAndIPMIToolIntelNMDriver(base.BaseDriver):
"""Agent + IPMITool driver with Intel NM policies."""
def __init__(self):
self.power = ipmitool.IPMIPower()
self.boot = pxe.PXEBoot()
self.deploy = agent.AgentDeploy()
self.management = ipmitool.IPMIManagement()
self.console = ipmitool.IPMIShellinaboxConsole()
self.agent_vendor = agent.AgentVendorInterface()
self.ipmi_vendor = ipmitool.VendorPassthru()
self.nm_vendor = nm_vendor.IntelNMVendorPassthru()
self.mapping = {'send_raw': self.ipmi_vendor,
'bmc_reset': self.ipmi_vendor,
'heartbeat': self.agent_vendor,
'control_nm_policy': self.nm_vendor,
'set_nm_policy': self.nm_vendor,
'get_nm_policy': self.nm_vendor,
'remove_nm_policy': self.nm_vendor,
'set_nm_policy_suspend': self.nm_vendor,
'get_nm_policy_suspend': self.nm_vendor,
'remove_nm_policy_suspend': self.nm_vendor,
'get_nm_capabilities': self.nm_vendor,
'get_nm_version': self.nm_vendor}
self.driver_passthru_mapping = {'lookup': self.agent_vendor}
self.vendor = utils.MixinVendorInterface(
self.mapping,
driver_passthru_mapping=self.driver_passthru_mapping)
self.raid = agent.AgentRAID()
self.inspect = inspector.Inspector.create_if_enabled(
'AgentAndIPMIToolDriver')

View File

@ -0,0 +1,25 @@
{
"title": "Intel Node Manager policies control schema",
"type": "object",
"properties": {
"scope": {
"type": "string",
"enum": ["global", "domain", "policy"]
},
"enable": {
"type": "boolean"
},
"domain_id": {
"type": "string",
"enum": ["platform", "cpu", "memory", "io"]
},
"policy_id": {
"type": "integer",
"minimum": 0,
"maximum": 255
}
},
"required": ["scope", "enable"],
"additionalProperties": false
}

View File

@ -0,0 +1,21 @@
{
"title": "Intel Node Manager get capabilities schema",
"type": "object",
"properties": {
"domain_id": {
"type": "string",
"enum": ["platform", "cpu", "memory", "io"]
},
"policy_trigger": {
"type": "string",
"enum": ["none", "temperature", "power", "reset", "boot"]
},
"power_domain": {
"type": "string",
"enum": ["primary", "secondary"]
}
},
"required": ["domain_id", "policy_trigger", "power_domain" ],
"additionalProperties": false
}

View File

@ -0,0 +1,85 @@
# 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 exception
from ironic.conductor import task_manager
from ironic.drivers.modules import ipmitool
from oslo_concurrency import processutils
from oslo_log import log
from ironic_staging_drivers.common.i18n import _LE
LOG = log.getLogger(__name__)
# NOTE(yuriyz): there are extended version of send_raw() from Ironic ipmitool
# driver and dump_sdr(). These functions depends on ipmitool driver internals,
# and should be moved to Ironic for use from out-of-tree drivers.
@task_manager.require_exclusive_lock
def send_raw(task, raw_bytes):
"""Send raw bytes to the BMC. Bytes should be a string of bytes.
:param task: a TaskManager instance.
:param raw_bytes: a string of raw bytes to send, e.g. '0x00 0x01'
:returns: a tuple with stdout and stderr.
:raises: IPMIFailure on an error from ipmitool.
:raises: MissingParameterValue if a required parameter is missing.
:raises: InvalidParameterValue when an invalid value is specified.
"""
node_uuid = task.node.uuid
LOG.debug('Sending node %(node)s raw bytes %(bytes)s',
{'bytes': raw_bytes, 'node': node_uuid})
driver_info = ipmitool._parse_driver_info(task.node)
cmd = 'raw %s' % raw_bytes
try:
out, err = ipmitool._exec_ipmitool(driver_info, cmd)
LOG.debug('send raw bytes returned stdout: %(stdout)s, stderr:'
' %(stderr)s', {'stdout': out, 'stderr': err})
except (exception.PasswordFileFailedToCreate,
processutils.ProcessExecutionError) as e:
LOG.exception(_LE('IPMI "raw bytes" failed for node %(node_id)s '
'with error: %(error)s.'),
{'node_id': node_uuid, 'error': e})
raise exception.IPMIFailure(cmd=cmd)
return out, err
def dump_sdr(task, file_path):
"""Dump SDR data to a file.
:param task: a TaskManager instance.
:param file_path: the path to SDR dump file.
:raises: IPMIFailure on an error from ipmitool.
:raises: MissingParameterValue if a required parameter is missing.
:raises: InvalidParameterValue when an invalid value is specified.
"""
node_uuid = task.node.uuid
LOG.debug('Dump SDR data for node %(node)s to file %(name)s',
{'name': file_path, 'node': node_uuid})
driver_info = ipmitool._parse_driver_info(task.node)
cmd = 'sdr dump %s' % file_path
try:
out, err = ipmitool._exec_ipmitool(driver_info, cmd)
LOG.debug('dump SDR returned stdout: %(stdout)s, stderr:'
' %(stderr)s', {'stdout': out, 'stderr': err})
except (exception.PasswordFileFailedToCreate,
processutils.ProcessExecutionError) as e:
LOG.exception(_LE('IPMI "sdr dump" failed for node %(node_id)s '
'with error: %(error)s.'),
{'node_id': node_uuid, 'error': e})
raise exception.IPMIFailure(cmd=cmd)

View File

@ -0,0 +1,17 @@
{
"title": "Intel Node Manager domain and policy id json schema",
"type": "object",
"properties": {
"domain_id": {
"type": "string",
"enum": ["platform", "cpu", "memory", "io"]
},
"policy_id": {
"type": "integer",
"minimum": 0,
"maximum": 255
}
},
"required": ["domain_id", "policy_id"],
"additionalProperties": false
}

View File

@ -0,0 +1,429 @@
# 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 binascii
import collections
import struct
from ironic.common import exception
import six
from ironic_staging_drivers.common.i18n import _
INTEL_NM_DOMAINS = {
'platform': 0x00,
'cpu': 0x01,
'memory': 0x02,
'protection': 0x03,
'io': 0x04
}
INTEL_NM_TRIGGERS = {
'none': 0x00,
'temperature': 0x01,
'power': 0x02,
'reset': 0x03,
'boot': 0x04
}
INTEL_NM_CPU_CORRECTION = {
'auto': 0x00,
'unagressive': 0x20,
'aggressive': 0x40,
}
INTEL_NM_STORAGE = {
'persistent': 0x00,
'volatile': 0x80,
}
INTEL_NM_ACTIONS = {
'alert': 0x00,
'shutdown': 0x01,
}
INTEL_NM_POWER_DOMAIN = {
'primary': 0x00,
'secondary': 0x80,
}
INTEL_NM_BOOT_MODE = {
'power': 0x00,
'performance': 0x01,
}
INTEL_NM_DAYS = collections.OrderedDict([('monday', 0x01),
('tuesday', 0x02),
('wednesday', 0x04),
('thursday', 0x08),
('friday', 0x10),
('saturday', 0x20),
('sunday', 0x40)])
VERSIONS = {
0x01: '1.0',
0x02: '1.5',
0x03: '2.0',
0x04: '2.5',
0x05: '3.0'
}
IPMI_VERSIONS = {
0x01: '1.0',
0x02: '2.0',
0x03: '3.0'
}
def _reverse_dict(d):
return {v: k for k, v in d.items()}
INTEL_NM_DOMAINS_REV = _reverse_dict(INTEL_NM_DOMAINS)
INTEL_NM_TRIGGERS_REV = _reverse_dict(INTEL_NM_TRIGGERS)
INTEL_NM_CPU_CORRECTION_REV = _reverse_dict(INTEL_NM_CPU_CORRECTION)
INTEL_NM_STORAGE_REV = _reverse_dict(INTEL_NM_STORAGE)
INTEL_NM_ACTIONS_REV = _reverse_dict(INTEL_NM_ACTIONS)
INTEL_NM_POWER_DOMAIN_REV = _reverse_dict(INTEL_NM_POWER_DOMAIN)
# OEM group extension code defined in IPMI spec
INTEL_NM_NETFN = '0x2E'
# Intel manufacturer ID for OEM extension, LS byte first
INTEL_NM_ID = ('0x57', '0x01', '0x00')
# Intel NM commands
INTEL_NM_POLICY_CONTROL = '0xC0'
INTEL_NM_POLICY_SET = '0xC1'
INTEL_NM_POLICY_GET = '0xC2'
INTEL_NM_SUSPEND_SET = '0xC5'
INTEL_NM_SUSPEND_GET = '0xC6'
INTEL_NM_CAPABILITIES_GET = '0xC9'
INTEL_NM_VERSION_GET = '0xCA'
def _handle_parsing_error(func):
"""Decorator for handling errors in raw output data."""
@six.wraps(func)
def wrapper(raw_data):
msg = _('Data from Intel Node Manager %s')
try:
return func(raw_data)
except (IndexError, struct.error):
raise exception.IPMIFailure(msg % _('has wrong length.'))
except KeyError:
raise exception.IPMIFailure(msg % _('is corrupted.'))
except ValueError:
raise exception.IPMIFailure(msg % _('cannot be converted.'))
return wrapper
def _hex(x):
"""Formatting integer as two digit hex value."""
return '0x{:02X}'.format(x)
def _raw_to_int(raw_data):
"""Converting list of raw hex values as strings to integers."""
return [int(x, 16) for x in raw_data]
def _bytehex(data):
"""Iterate by one byte with hexlify() output."""
for i in range(0, len(data), 2):
yield data[i:i + 2]
def _hexarray(data):
"""Converting binary data to list of hex bytes as strings."""
return ['0x' + x.decode() for x in _bytehex(binascii.hexlify(data))]
def _append_to_command(cmd, data):
"""Append list or single value to command."""
if not isinstance(data, (list, tuple)):
data = [data]
cmd.extend(data)
def _add_to_dict(data_dict, values, names):
"""Add to dict values with corresponding names."""
data_dict.update(dict(zip(names, values)))
def _create_command_head(command):
"""Create first part of Intel NM command."""
cmd = [INTEL_NM_NETFN, command]
_append_to_command(cmd, INTEL_NM_ID)
return cmd
def _add_domain_policy_id(cmd, data):
"""Add domain id and policy id to command."""
_append_to_command(cmd, _hex(INTEL_NM_DOMAINS[data['domain_id']]))
_append_to_command(cmd, _hex(data['policy_id']))
def _days_compose(days):
"""Converting list of days to binary representation."""
pattern = 0
for day in days:
pattern |= INTEL_NM_DAYS[day]
return pattern
def _days_parse(pattern):
"""Parse binary data with days of week."""
return [day for day in INTEL_NM_DAYS if pattern & INTEL_NM_DAYS[day]]
def set_policy(policy):
"""Return hex data for policy set command."""
# NM defaults
if 'cpu_power_correction' not in policy:
policy['cpu_power_correction'] = 'auto'
if 'storage' not in policy:
policy['storage'] = 'persistent'
if policy['policy_trigger'] in ('none', 'boot'):
policy['trigger_limit'] = 0
cmd = _create_command_head(INTEL_NM_POLICY_SET)
_append_to_command(cmd, _hex(INTEL_NM_DOMAINS[policy['domain_id']] |
0x10 if policy['enable'] else 0x00))
_append_to_command(cmd, _hex(policy['policy_id']))
# 0x10 is policy add flag
_append_to_command(cmd, _hex(INTEL_NM_TRIGGERS[policy['policy_trigger']] |
INTEL_NM_CPU_CORRECTION[policy['cpu_power_correction']]
| INTEL_NM_STORAGE[policy['storage']] | 0x10))
_append_to_command(cmd, _hex(INTEL_NM_ACTIONS[policy['action']] |
INTEL_NM_POWER_DOMAIN[policy['power_domain']]))
if isinstance(policy['target_limit'], int):
limit = policy['target_limit']
else:
mode = 0x00 if policy['target_limit']['boot_mode'] == 'power' else 0x01
cores_disabled = policy['target_limit']['cores_disabled'] << 1
limit = mode | cores_disabled
policy_values = struct.pack('<HIHH', limit, policy['correction_time'],
policy['trigger_limit'],
policy['reporting_period'])
_append_to_command(cmd, _hexarray(policy_values))
return cmd
@_handle_parsing_error
def parse_policy(raw_data):
"""Parse policy data."""
policy = {}
raw_int = _raw_to_int(raw_data)
policy['domain_id'] = INTEL_NM_DOMAINS_REV[raw_int[3] & 0x0F]
policy['enabled'] = bool(raw_int[3] & 0x10)
policy['per_domain_enabled'] = bool(raw_int[3] & 0x20)
policy['global_enabled'] = bool(raw_int[3] & 0x40)
policy['created_by_nm'] = not bool(raw_int[3] & 0x80)
policy['policy_trigger'] = INTEL_NM_TRIGGERS_REV[raw_int[4] & 0x0F]
policy['power_policy'] = bool(raw_int[4] & 0x10)
power_correction = INTEL_NM_CPU_CORRECTION_REV[raw_int[4] & 0x60]
policy['cpu_power_correction'] = power_correction
policy['storage'] = INTEL_NM_STORAGE_REV[raw_int[4] & 0x80]
policy['action'] = INTEL_NM_ACTIONS_REV[raw_int[5] & 0x01]
policy['power_domain'] = INTEL_NM_POWER_DOMAIN_REV[raw_int[5] & 0x80]
policy_values = struct.unpack('<HIHH', bytearray(raw_int[6:]))
policy_names = ('target_limit', 'correction_time', 'trigger_limit',
'reporting_period')
_add_to_dict(policy, policy_values, policy_names)
return policy
def set_policy_suspend(suspend):
"""Return hex data for policy suspend set command."""
cmd = _create_command_head(INTEL_NM_SUSPEND_SET)
_add_domain_policy_id(cmd, suspend)
periods = suspend['periods']
_append_to_command(cmd, _hex(len(periods)))
for period in periods:
_append_to_command(cmd, _hex(period['start']))
_append_to_command(cmd, _hex(period['stop']))
_append_to_command(cmd, _hex(_days_compose(period['days'])))
return cmd
@_handle_parsing_error
def parse_policy_suspend(raw_data):
"""Parse policy suspend data."""
suspends = []
raw_int = _raw_to_int(raw_data)
policy_num = raw_int[3]
for num in range(policy_num):
base = num * 3 + 4
suspend = {
"start": raw_int[base],
"stop": raw_int[base + 1],
"days": _days_parse(raw_int[base + 2])
}
suspends.append(suspend)
return suspends
def get_capabilities(data):
"""Return hex data for capabilities get command."""
cmd = _create_command_head(INTEL_NM_CAPABILITIES_GET)
_append_to_command(cmd, _hex(INTEL_NM_DOMAINS[data['domain_id']]))
power_policy = 0x10
_append_to_command(cmd, _hex(INTEL_NM_TRIGGERS[data['policy_trigger']] |
power_policy |
INTEL_NM_POWER_DOMAIN[data['power_domain']]))
return cmd
@_handle_parsing_error
def parse_capabilities(raw_data):
"""Parse capabilities data."""
capabilities = {}
raw_int = _raw_to_int(raw_data)
capabilities['max_policies'] = raw_int[3]
capabilities_values = struct.unpack('<HHIIHH', bytearray(
raw_int[4:20]))
capabilities_names = ('max_limit_value', 'min_limit_value',
'min_correction_time', 'max_correction_time',
'min_reporting_period', 'max_reporting_period')
_add_to_dict(capabilities, capabilities_values, capabilities_names)
capabilities['domain_id'] = INTEL_NM_DOMAINS_REV[raw_int[20] & 0x0F]
power_domain = INTEL_NM_POWER_DOMAIN_REV[raw_int[20] & 0x80]
capabilities['power_domain'] = power_domain
return capabilities
def control_policies(control_data):
"""Return hex data for enable or disable policy command."""
cmd = _create_command_head(INTEL_NM_POLICY_CONTROL)
enable = control_data['enable']
scope = control_data['scope']
if scope == 'global':
flags = '0x01' if enable else '0x00'
domain_id = 0
policy_id = 0
elif scope == 'domain':
flags = '0x03' if enable else '0x02'
domain_id = INTEL_NM_DOMAINS[control_data['domain_id']]
policy_id = 0
elif scope == 'policy':
flags = '0x05' if enable else '0x04'
domain_id = INTEL_NM_DOMAINS[control_data['domain_id']]
policy_id = control_data['policy_id']
_append_to_command(cmd, flags)
_append_to_command(cmd, _hex(domain_id))
_append_to_command(cmd, _hex(policy_id))
return cmd
def get_policy(data):
"""Return hex data for policy get command."""
cmd = _create_command_head(INTEL_NM_POLICY_GET)
_add_domain_policy_id(cmd, data)
return cmd
def remove_policy(data):
"""Return hex data for policy remove command."""
cmd = _create_command_head(INTEL_NM_POLICY_SET)
_add_domain_policy_id(cmd, data)
# first 0 is remove policy, extra will be ignored
_append_to_command(cmd, ('0x00',) * 12)
return cmd
def get_policy_suspend(data):
"""Return hex data for policy get suspend command."""
cmd = _create_command_head(INTEL_NM_SUSPEND_GET)
_add_domain_policy_id(cmd, data)
return cmd
def remove_policy_suspend(data):
"""Return hex data for policy remove suspend command."""
cmd = _create_command_head(INTEL_NM_SUSPEND_SET)
_add_domain_policy_id(cmd, data)
# remove suspend
_append_to_command(cmd, '0x00')
return cmd
def get_version(data):
"""Return hex data for version get command."""
cmd = _create_command_head(INTEL_NM_VERSION_GET)
return cmd
@_handle_parsing_error
def parse_version(raw_data):
"""Parse versions data."""
version = {}
raw_int = _raw_to_int(raw_data)
version['nm'] = VERSIONS.get(raw_int[3], 'unknown')
version['ipmi'] = IPMI_VERSIONS.get(raw_int[4], 'unknown')
version['patch'] = str(raw_int[5])
version['firmware'] = str(raw_int[6]) + '.' + str(raw_int[7])
return version
# Code below taken from Ceilometer
# Copyright 2014 Intel Corporation.
def parse_slave_and_channel(file_path):
"""Parse the dumped file to get slave address and channel number.
:param file_path: file path of dumped SDR file.
:return: slave address and channel number of target device.
"""
prefix = '5701000d01'
# According to Intel Node Manager spec, section 4.5, for Intel NM
# discovery OEM SDR records are type C0h. It contains manufacture ID
# and OEM data in the record body.
# 0-2 bytes are OEM ID, byte 3 is 0Dh and byte 4 is 01h. Byte 5, 6
# is Intel NM device slave address and channel number/sensor owner LUN.
with open(file_path, 'rb') as bin_fp:
data_str = binascii.hexlify(bin_fp.read())
if six.PY3:
data_str = data_str.decode()
oem_id_index = data_str.find(prefix)
if oem_id_index != -1:
ret = data_str[oem_id_index + len(prefix):
oem_id_index + len(prefix) + 4]
# Byte 5 is slave address. [7:4] from byte 6 is channel
# number, so just pick ret[2] here.
return ('0x' + ret[0:2], '0x0' + ret[2])

View File

@ -0,0 +1,294 @@
# 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 json
import os
from ironic.common import exception
from ironic.drivers import base
from ironic_lib import utils as ironic_utils
import jsonschema
from jsonschema import exceptions as json_schema_exc
from oslo_config import cfg
from oslo_log import log
from oslo_utils import excutils
import six
from ironic_staging_drivers.common.i18n import _
from ironic_staging_drivers.common.i18n import _LE
from ironic_staging_drivers.common.i18n import _LI
from ironic_staging_drivers.intel_nm import ipmi
from ironic_staging_drivers.intel_nm import nm_commands
CONF = cfg.CONF
CONF.import_opt('tempdir', 'ironic.common.utils')
LOG = log.getLogger(__name__)
SCHEMAS = ('control_schema', 'get_cap_schema', 'main_ids_schema',
'policy_schema', 'suspend_schema')
def _command_to_string(cmd):
"""Convert a list with command raw bytes to string."""
return ' '.join(cmd)
def _get_nm_address(task):
"""Get Intel Node Manager target channel and address.
:param task: a TaskManager instance.
:raises: IPMIFailure if Intel Node Manager is not detected on a node or if
an error happens during detection.
:returns: a tuple with IPMI channel and address of Intel Node Manager.
"""
node = task.node
driver_internal_info = node.driver_internal_info
def _save_to_node(channel, address):
driver_internal_info['intel_nm_channel'] = channel
driver_internal_info['intel_nm_address'] = address
node.driver_internal_info = driver_internal_info
node.save()
channel = driver_internal_info.get('intel_nm_channel')
address = driver_internal_info.get('intel_nm_address')
if channel and address:
return channel, address
if channel is False and address is False:
raise exception.IPMIFailure(_('Driver data indicates that Intel '
'Node Manager detection failed.'))
LOG.info(_LI('Start detection of Intel Node Manager on node %s'),
node.uuid)
sdr_filename = os.path.join(CONF.tempdir, node.uuid + '.sdr')
res = None
try:
ipmi.dump_sdr(task, sdr_filename)
res = nm_commands.parse_slave_and_channel(sdr_filename)
finally:
ironic_utils.unlink_without_raise(sdr_filename)
if res is None:
_save_to_node(False, False)
raise exception.IPMIFailure(_('Intel Node Manager is not detected.'))
address, channel = res
LOG.debug('Intel Node Manager sensors present in SDR on node %(node)s, '
'channel %(channel)s address %(address)s.',
{'node': node.uuid, 'channel': channel, 'address': address})
# SDR can contain wrong info, try simple command
node.driver_info['ipmi_bridging'] = 'single'
node.driver_info['ipmi_target_channel'] = channel
node.driver_info['ipmi_target_address'] = address
try:
ipmi.send_raw(task, _command_to_string(nm_commands.get_version(None)))
_save_to_node(channel, address)
return channel, address
except exception.IPMIFailure:
_save_to_node(False, False)
raise exception.IPMIFailure(_('Intel Node Manager sensors record '
'present in SDR but Node Manager is not '
'responding.'))
def _execute_nm_command(task, data, command_func, parse_func=None):
"""Execute Intel Node Manager command via send_raw().
:param task: a TaskManager instance.
:param data: a dict with data passed to vendor's method.
:param command_func: a function that returns raw command bytes.
:param parse_func: a function that parses returned raw bytes.
:raises: IPMIFailure if Intel Node Manager is not detected on a node or if
an error happens during command execution.
:returns: a dict with parsed output or None if command does not return
user's info.
"""
try:
channel, address = _get_nm_address(task)
except exception.IPMIFailure as e:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Can not obtain Intel Node Manager address for '
'node %(node)s: %(err)s'),
{'node': task.node.uuid, 'err': six.text_type(e)})
driver_info = task.node.driver_info
driver_info['ipmi_bridging'] = 'single'
driver_info['ipmi_target_channel'] = channel
driver_info['ipmi_target_address'] = address
cmd = _command_to_string(command_func(data))
out = ipmi.send_raw(task, cmd)[0]
if parse_func:
try:
return parse_func(out.split())
except exception.IPMIFailure as e:
with excutils.save_and_reraise_exception():
LOG.exception(_LE('Error in returned data for node %(node)s: '
'%(err)s'), {'node': task.node.uuid,
'err': six.text_type(e)})
class IntelNMVendorPassthru(base.VendorInterface):
"""Intel Node Manager policies vendor interface."""
def __init__(self):
schemas_dir = os.path.dirname(__file__)
for schema in SCHEMAS:
filename = os.path.join(schemas_dir, schema + '.json')
with open(filename, 'r') as sf:
setattr(self, schema, json.load(sf))
def get_properties(self):
"""Returns the properties of the interface.."""
return {}
def validate(self, task, method, http_method, **kwargs):
"""Validates the vendor method's parameters.
This method validates whether the supplied data contains the required
information for the driver.
:param task: a TaskManager instance.
:param method: name of vendor method.
:param http_method: HTTP method.
:param kwargs: data passed to vendor's method.
:raises: InvalidParameterValue if supplied data is not valid.
:raises: MissingParameterValue if parameters missing in supplied data.
"""
try:
if method in ('get_nm_policy', 'remove_nm_policy',
'get_nm_policy_suspend', 'remove_nm_policy_suspend'):
jsonschema.validate(kwargs, self.main_ids_schema)
elif method == 'control_nm_policy':
jsonschema.validate(kwargs, self.control_schema)
no_domain = _('Missing "domain_id"')
no_policy = _('Missing "policy_id"')
if kwargs['scope'] == 'domain' and not kwargs.get('domain_id'):
raise exception.MissingParameterValue(no_domain)
if kwargs['scope'] == 'policy':
if not kwargs.get('domain_id'):
raise exception.MissingParameterValue(no_domain)
if not kwargs.get('policy_id'):
raise exception.MissingParameterValue(no_policy)
elif method == 'set_nm_policy':
jsonschema.validate(kwargs, self.policy_schema)
if kwargs['policy_trigger'] == 'boot':
if not isinstance(kwargs['target_limit'], dict):
raise exception.InvalidParameterValue(_('Invalid boot '
'policy'))
elif method == 'set_nm_policy_suspend':
jsonschema.validate(kwargs, self.suspend_schema)
elif method == 'get_nm_capabilities':
jsonschema.validate(kwargs, self.get_cap_schema)
except json_schema_exc.ValidationError as e:
raise exception.InvalidParameterValue(_('Input data validation '
'error: %s') % e)
@base.passthru(['PUT'])
def control_nm_policy(self, task, **kwargs):
"""Enable or disable Intel Node Manager policy control.
:param task: a TaskManager instance.
:param kwargs: data passed to method.
:raises: IPMIFailure on an error.
"""
_execute_nm_command(task, kwargs, nm_commands.control_policies)
@base.passthru(['PUT'])
def set_nm_policy(self, task, **kwargs):
"""Set Intel Node Manager policy.
:param task: a TaskManager instance.
:param kwargs: data passed to method.
:raises: IPMIFailure on an error.
"""
_execute_nm_command(task, kwargs, nm_commands.set_policy)
@base.passthru(['GET'], async=False)
def get_nm_policy(self, task, **kwargs):
"""Get Intel Node Manager policy.
:param task: a TaskManager instance.
:param kwargs: data passed to method.
:raises: IPMIFailure on an error.
:returns: a dictionary containing policy settings.
"""
return _execute_nm_command(task, kwargs, nm_commands.get_policy,
nm_commands.parse_policy)
@base.passthru(['DELETE'])
def remove_nm_policy(self, task, **kwargs):
"""Remove Intel Node Manager policy.
:param task: a TaskManager instance.
:param kwargs: data passed to method.
:raises: IPMIFailure on an error.
"""
_execute_nm_command(task, kwargs, nm_commands.remove_policy)
@base.passthru(['PUT'])
def set_nm_policy_suspend(self, task, **kwargs):
"""Set Intel Node Manager policy suspend periods.
:param task: a TaskManager instance.
:param kwargs: data passed to method.
:raises: IPMIFailure on an error.
"""
_execute_nm_command(task, kwargs, nm_commands.set_policy_suspend)
@base.passthru(['GET'], async=False)
def get_nm_policy_suspend(self, task, **kwargs):
"""Get Intel Node Manager policy suspend periods.
:param task: a TaskManager instance.
:param kwargs: data passed to method.
:raises: IPMIFailure on an error.
:returns: a dictionary containing suspend info for a policy.
"""
return _execute_nm_command(task, kwargs,
nm_commands.get_policy_suspend,
nm_commands.parse_policy_suspend)
@base.passthru(['DELETE'])
def remove_nm_policy_suspend(self, task, **kwargs):
"""Remove Intel Node Manager policy suspend periods.
:param task: a TaskManager instance.
:param kwargs: data passed to method.
:raises: IPMIFailure on an error.
"""
_execute_nm_command(task, kwargs, nm_commands.remove_policy_suspend)
@base.passthru(['GET'], async=False)
def get_nm_capabilities(self, task, **kwargs):
"""Get Intel Node Manager capabilities.
:param task: a TaskManager instance.
:param kwargs: data passed to method.
:raises: IPMIFailure on an error.
:returns: a dictionary containing Intel NM capabilities.
"""
return _execute_nm_command(task, kwargs, nm_commands.get_capabilities,
nm_commands.parse_capabilities)
@base.passthru(['GET'], async=False)
def get_nm_version(self, task, **kwargs):
"""Get Intel Node Manager version.
:param task: a TaskManager instance.
:param kwargs: data passed to method.
:raises: IPMIFailure on an error.
:returns: a dictionary containing Intel NM version.
"""
return _execute_nm_command(task, kwargs, nm_commands.get_version,
nm_commands.parse_version)

View File

@ -0,0 +1,79 @@
{
"title": "Intel Node Manager policy json schema",
"type": "object",
"properties": {
"domain_id": {
"type": "string",
"enum": ["platform", "cpu", "memory", "io"]
},
"enable": {
"type": "boolean"
},
"policy_id": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"policy_trigger": {
"type": "string",
"enum": ["none", "temperature", "power", "reset", "boot"]
},
"cpu_power_correction": {
"type": "string",
"enum": ["auto", "unagressive", "aggressive"]
},
"storage": {
"type": "string",
"enum": ["persistent", "volatile"]
},
"action": {
"type": "string",
"enum": ["alert", "shutdown"]
},
"power_domain": {
"type": "string",
"enum": ["primary", "secondary"]
},
"target_limit": {
"anyOf": [{
"type": "integer",
"minimum": 0,
"maximum": 65535
},
{
"type": "object",
"properties": {
"boot_mode": {
"type": "string",
"enum": ["power", "performance"]
},
"cores_disabled": {
"type": "integer",
"minimum": 0,
"maximum": 127
}
},
"required": ["boot_mode", "cores_disabled"],
"additionalProperties": false
}]
},
"correction_time": {
"type": "integer",
"minimum": 0
},
"trigger_limit": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"reporting_period": {
"type": "integer",
"minimum": 0,
"maximum": 65535
}
},
"required": ["domain_id", "enable", "policy_id", "policy_trigger", "action", "power_domain", "target_limit", "correction_time", "reporting_period" ],
"additionalProperties": false
}

View File

@ -0,0 +1,48 @@
{
"title": "Intel Node Manager policy suspend periods json schema",
"type": "object",
"properties": {
"domain_id": {
"type": "string",
"enum": ["platform", "cpu", "memory", "io"]
},
"policy_id": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"periods": {
"type": "array",
"items": {
"type": "object",
"properties": {
"start": {
"type": "integer",
"minimum": 0,
"maximum": 240
},
"stop": {
"type": "integer",
"minimum": 0,
"maximum": 240
},
"days": {
"type": "array",
"items": {
"type": "string",
"enum": ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
},
"uniqueItems": true,
"minItems": 1
}
},
"required": ["start", "stop", "days" ],
"additionalProperties": false
},
"minItems": 1,
"maxItems": 5
}
},
"required": ["domain_id", "policy_id", "periods" ],
"additionalProperties": false
}

View File

@ -0,0 +1,265 @@
# 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.
"""
Tests for Intel NM policies commands
"""
import tempfile
from ironic.common import exception
from ironic.tests import base
from ironic_staging_drivers.intel_nm import nm_commands as commands
@commands._handle_parsing_error
def fake_parse(fake_data):
return fake_data
@commands._handle_parsing_error
def fake_parse_exc(d):
raise IndexError()
class ParsingErrorDecoratorTestCase(base.TestCase):
def test_parse_no_errors(self):
self.assertEqual('foo', fake_parse('foo'))
def test_parse_handled_exception(self):
self.assertRaises(exception.IPMIFailure, fake_parse_exc, 'foo')
class IntelNMPoliciesCommandTestCase(base.TestCase):
def test_set_policy(self):
policy = {'domain_id': 'platform', 'enable': True, 'policy_id': 123,
'policy_trigger': 'temperature',
'cpu_power_correction': 'auto', 'storage': 'persistent',
'action': 'alert', 'power_domain': 'primary',
'target_limit': 1000, 'correction_time': 2000,
'trigger_limit': 100, 'reporting_period': 600}
expected = ['0x2E', '0xC1', '0x57', '0x01', '0x00', '0x10', '0x7B',
'0x11', '0x00', '0xe8', '0x03', '0xd0', '0x07', '0x00',
'0x00', '0x64', '0x00', '0x58', '0x02']
result = commands.set_policy(policy)
self.assertEqual(expected, result)
def test_set_policy_with_defaults(self):
policy = {'domain_id': 'platform', 'enable': True, 'policy_id': 123,
'policy_trigger': 'none', 'action': 'alert',
'power_domain': 'primary', 'target_limit': 1000,
'correction_time': 2000, 'reporting_period': 600}
expected = ['0x2E', '0xC1', '0x57', '0x01', '0x00', '0x10', '0x7B',
'0x10', '0x00', '0xe8', '0x03', '0xd0', '0x07', '0x00',
'0x00', '0x00', '0x00', '0x58', '0x02']
result = commands.set_policy(policy)
self.assertEqual(expected, result)
def test_set_policy_boot(self):
policy = {'domain_id': 'platform', 'enable': True, 'policy_id': 123,
'policy_trigger': 'boot', 'cpu_power_correction': 'auto',
'storage': 'persistent', 'action': 'alert',
'power_domain': 'primary',
'target_limit': {'boot_mode': 'power', 'cores_disabled': 2},
'correction_time': 2000, 'trigger_limit': 100,
'reporting_period': 600}
expected = ['0x2E', '0xC1', '0x57', '0x01', '0x00', '0x10', '0x7B',
'0x14', '0x00', '0x04', '0x00', '0xd0', '0x07', '0x00',
'0x00', '0x00', '0x00', '0x58', '0x02']
result = commands.set_policy(policy)
self.assertEqual(expected, result)
def test_set_policy_suspend(self):
suspend = {'domain_id': 'platform', 'policy_id': 123,
'periods': [{'start': 20, 'stop': 100,
'days': ['monday', 'tuesday']},
{'start': 30, 'stop': 150,
'days': ['friday', 'sunday']}]}
result = commands.set_policy_suspend(suspend)
expected = ['0x2E', '0xC5', '0x57', '0x01', '0x00', '0x00', '0x7B',
'0x02', '0x14', '0x64', '0x03', '0x1E', '0x96', '0x50']
self.assertEqual(expected, result)
def test_get_capabilities(self):
cap_data = {'domain_id': 'platform', 'policy_trigger': 'none',
'power_domain': 'primary'}
result = commands.get_capabilities(cap_data)
expected = ['0x2E', '0xC9', '0x57', '0x01', '0x00', '0x00', '0x10']
self.assertEqual(expected, result)
def test_control_policies(self):
control_data = {'scope': 'policy', 'enable': True,
'domain_id': 'platform', 'policy_id': 123}
result = commands.control_policies(control_data)
expected = ['0x2E', '0xC0', '0x57', '0x01', '0x00', '0x05', '0x00',
'0x7B']
self.assertEqual(expected, result)
def test_get_policy(self):
data = {'domain_id': 'platform', 'policy_id': 123}
result = commands.get_policy(data)
expected = ['0x2E', '0xC2', '0x57', '0x01', '0x00', '0x00', '0x7B']
self.assertEqual(expected, result)
def test_remove_policy(self):
data = {'domain_id': 'platform', 'policy_id': 123}
expected = (['0x2E', '0xC1', '0x57', '0x01', '0x00', '0x00', '0x7B'] +
['0x00'] * 12)
result = commands.remove_policy(data)
self.assertEqual(expected, result)
def test_get_policy_suspend(self):
data = {'domain_id': 'platform', 'policy_id': 123}
expected = ['0x2E', '0xC6', '0x57', '0x01', '0x00', '0x00', '0x7B']
result = commands.get_policy_suspend(data)
self.assertEqual(expected, result)
def test_remove_policy_suspend(self):
data = {'domain_id': 'platform', 'policy_id': 123}
expected = ['0x2E', '0xC5', '0x57', '0x01', '0x00', '0x00', '0x7B',
'0x00']
result = commands.remove_policy_suspend(data)
self.assertEqual(expected, result)
def test_get_version(self):
result = commands.get_version(None)
expected = ['0x2E', '0xCA', '0x57', '0x01', '0x00']
self.assertEqual(expected, result)
def test_parse_policy(self):
raw_data = ['0x00', '0x00', '0x00', '0x70', '0x00', '0x00', '0x02',
'0xFF', '0x00', '0x01', '0x02', '0x00', '0x01', '0x20',
'0x40', '0x01']
expected = {'action': 'alert', 'correction_time': 131328,
'cpu_power_correction': 'auto', 'created_by_nm': True,
'domain_id': 'platform', 'enabled': True,
'global_enabled': True, 'per_domain_enabled': True,
'policy_trigger': 'none', 'power_domain': 'primary',
'power_policy': False, 'reporting_period': 320,
'storage': 'persistent', 'target_limit': 65282,
'trigger_limit': 8193}
result = commands.parse_policy(raw_data)
self.assertEqual(expected, result)
def test_parse_policy_invalid_length(self):
raw_data = ['0x00', '0x00', '0x00', '0x70', '0x00', '0x00', '0x02',
'0xFF', '0x00', '0x01', '0x02', '0x00', '0x01', '0x20']
self.assertRaises(exception.IPMIFailure, commands.parse_policy,
raw_data)
def test_parse_policy_corrupted_data(self):
raw_data = ['0x00', '0x00', '0x00', '0x7F', '0x00', '0x00', '0x02',
'0xFF', '0x00', '0x01', '0x02', '0x00', '0x01', '0x20',
'0x40', '0x01']
self.assertRaises(exception.IPMIFailure, commands.parse_policy,
raw_data)
def test_parse_policy_conversion_error(self):
raw_data = ['0x00', '0x00', '0x00', 'boo', '0x00', '0x00', '0x02',
'0xFF', '0x00', '0x01', '0x02', '0x00', '0x01', '0x20',
'0x40', '0x01']
self.assertRaises(exception.IPMIFailure, commands.parse_policy,
raw_data)
def test_parse_policy_suspend(self):
raw_data = ['0x00', '0x00', '0x00', '0x02', '0x08', '0x18', '0x03',
'0x20', '0x50', '0x18']
expected = [{'days': ['monday', 'tuesday'], 'start': 8, 'stop': 24},
{'days': ['thursday', 'friday'], 'start': 32, 'stop': 80}]
result = commands.parse_policy_suspend(raw_data)
self.assertEqual(expected, result)
def test_parse_policy_suspend_invalid_lenght(self):
raw_data = ['0x00', '0x00', '0x00', '0x22', '0x08', '0x18', '0x03']
self.assertRaises(exception.IPMIFailure, commands.parse_policy_suspend,
raw_data)
def test_parse_policy_suspend_conversion_error(self):
raw_data = ['0x00', '0x00', '0x00', '0x02', 'boo', '0x18', '0x03',
'0x20', '0x50', '0x18']
self.assertRaises(exception.IPMIFailure, commands.parse_policy_suspend,
raw_data)
def test_parse_capabilities(self):
raw_data = ['0x00', '0x00', '0x00', '0x10', '0x00', '0x10', '0x00',
'0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00',
'0x80', '0x00', '0x00', '0x00', '0x00', '0x80', '0x00']
expected = {'domain_id': 'platform', 'max_correction_time': 8388608,
'max_limit_value': 4096, 'max_policies': 16,
'max_reporting_period': 32768, 'min_correction_time': 0,
'min_limit_value': 0, 'min_reporting_period': 0,
'power_domain': 'primary'}
result = commands.parse_capabilities(raw_data)
self.assertEqual(expected, result)
def test_parse_capabilities_invalid_lenght(self):
raw_data = ['0x00', '0x00', '0x00', '0x10', '0x00', '0x10', '0x00']
self.assertRaises(exception.IPMIFailure, commands.parse_capabilities,
raw_data)
def test_parse_capabilities_corrupted_data(self):
raw_data = ['0x00', '0x00', '0x00', '0x10', '0x00', '0x10', '0x00',
'0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00',
'0x80', '0x00', '0x00', '0x00', '0x00', '0x80', '0xFF']
self.assertRaises(exception.IPMIFailure, commands.parse_capabilities,
raw_data)
def test_parse_capabilities_conversion_error(self):
raw_data = ['0x00', '0x00', '0x00', '0x10', '0x00', '0x10', '0x00',
'0x00', '0x00', '0x00', '0x00', '0x00', '0x00', '0x00',
'0x80', '0x00', '0x00', '0x00', 'boo', '0x80', '0x00']
self.assertRaises(exception.IPMIFailure, commands.parse_capabilities,
raw_data)
def test_parse_version(self):
raw_data = ['0x00', '0x00', '0x00', '0x05', '0x03', '0x07', '0x01',
'0x02']
expected = {'firmware': '1.2', 'ipmi': '3.0', 'nm': '3.0',
'patch': '7'}
result = commands.parse_version(raw_data)
self.assertEqual(expected, result)
def test_parse_version_invalid_lenght(self):
raw_data = ['0x00', '0x00', '0x00', '0x05', '0x03', '0x07', '0x01']
self.assertRaises(exception.IPMIFailure, commands.parse_version,
raw_data)
def test_parse_version_conversion_error(self):
raw_data = ['0x00', '0x00', '0x00', '0x05', '0x03', '0x07', '0x01',
'boo']
self.assertRaises(exception.IPMIFailure, commands.parse_version,
raw_data)
class ParsingFromFileTestCase(base.TestCase):
def setUp(self):
super(ParsingFromFileTestCase, self).setUp()
self.temp_file = tempfile.NamedTemporaryFile().name
def test_parsing_found(self):
data = b'\x00\xFF\x00\xFF\x57\x01\x00\x0D\x01\x6A\xB2\x00\xFF'
with open(self.temp_file, 'wb') as f:
f.write(data)
result = commands.parse_slave_and_channel(self.temp_file)
self.assertEqual(('0x6a', '0x0b'), result)
def test_parsing_not_found(self):
data = b'\x00\xFF\x00\xFF\x52\x01\x80\x0D\x01\x6A\xB7\x00\xFF'
with open(self.temp_file, 'wb') as f:
f.write(data)
result = commands.parse_slave_and_channel(self.temp_file)
self.assertIsNone(result)

View File

@ -0,0 +1,354 @@
# 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.
"""
Tests for Intel NM vendor interface
"""
import os
from ironic.common import exception
from ironic.conductor import task_manager
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
from ironic_lib import utils as ironic_utils
import mock
from oslo_config import cfg
from ironic_staging_drivers.intel_nm import ipmi
from ironic_staging_drivers.intel_nm import nm_commands
from ironic_staging_drivers.intel_nm import nm_vendor
CONF = cfg.CONF
_MAIN_IDS = {'domain_id': 'platform', 'policy_id': 111}
_POLICY = {'domain_id': 'platform', 'enable': True, 'policy_id': 111,
'policy_trigger': 'none', 'action': 'alert',
'power_domain': 'primary', 'target_limit': 100,
'correction_time': 200, 'reporting_period': 600}
_SUSPEND = {'domain_id': 'platform', 'policy_id': 121,
'periods': [{'start': 10, 'stop': 30, 'days': ['monday']}]}
_GET_CAP = {'domain_id': 'platform', 'policy_trigger': 'none',
'power_domain': 'primary'}