ironic-staging-drivers/ironic_staging_drivers/intel_nm/nm_commands.py

543 lines
16 KiB
Python

# 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 datetime
import struct
from ironic.common import exception as ironic_exception
from oslo_log import log
import six
from ironic_staging_drivers.common import exception
from ironic_staging_drivers.common.i18n import _
from ironic_staging_drivers.common.i18n import _LW
LOG = log.getLogger(__name__)
DOMAINS = {
'platform': 0x00,
'cpu': 0x01,
'memory': 0x02,
'protection': 0x03,
'io': 0x04
}
TRIGGERS = {
'none': 0x00,
'temperature': 0x01,
'power': 0x02,
'reset': 0x03,
'boot': 0x04
}
CPU_CORRECTION = {
'auto': 0x00,
'unagressive': 0x20,
'aggressive': 0x40,
}
STORAGE = {
'persistent': 0x00,
'volatile': 0x80,
}
ACTIONS = {
'alert': 0x00,
'shutdown': 0x01,
}
POWER_DOMAIN = {
'primary': 0x00,
'secondary': 0x80,
}
BOOT_MODE = {
'power': 0x00,
'performance': 0x01,
}
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'
}
STATISTICS = {
'global': {
'power': 0x01,
'temperature': 0x02,
'throttling': 0x03,
'airflow': 0x04,
'airflow_temperature': 0x05,
'chassis_power': 0x06,
'unhandled_requests': 0x1B,
'response_time': 0x1C,
'cpu_throttling': 0x1D, # deprecated
'memory_throttling': 0x1E, # deprecated
'communication_failures': 0x1F
},
'policy': {
'power': 0x11,
'trigger': 0x12,
'throttling': 0x13
}
}
def _reverse_dict(d):
return {v: k for k, v in d.items()}
DOMAINS_REV = _reverse_dict(DOMAINS)
TRIGGERS_REV = _reverse_dict(TRIGGERS)
CPU_CORRECTION_REV = _reverse_dict(CPU_CORRECTION)
STORAGE_REV = _reverse_dict(STORAGE)
ACTIONS_REV = _reverse_dict(ACTIONS)
POWER_DOMAIN_REV = _reverse_dict(POWER_DOMAIN)
# OEM group extension code defined in IPMI spec
NETFN = '0x2E'
# Intel manufacturer ID for OEM extension, LS byte first
INTEL_ID = ('0x57', '0x01', '0x00')
# Intel NM commands
POLICY_CONTROL = '0xC0'
POLICY_SET = '0xC1'
POLICY_GET = '0xC2'
SUSPEND_SET = '0xC5'
SUSPEND_GET = '0xC6'
CAPABILITIES_GET = '0xC9'
VERSION_GET = '0xCA'
STATISTICS_RESET = '0xC7'
STATISTICS_GET = '0xC8'
_INVALID_TIME = datetime.datetime.utcfromtimestamp(0).isoformat()
_UNSPECIFIED_TIMESTAMP = 0xFFFFFFFF
_INIT_TIMESTAMP_MAX = 0x20000000
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 ironic_exception.IPMIFailure(msg % _('has wrong length.'))
except KeyError:
raise ironic_exception.IPMIFailure(msg % _('is corrupted.'))
except ValueError:
raise ironic_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 = [NETFN, command]
_append_to_command(cmd, INTEL_ID)
return cmd
def _add_domain_policy_id(cmd, data):
"""Add domain id and policy id to command."""
_append_to_command(cmd, _hex(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 |= DAYS[day]
return pattern
def _days_parse(pattern):
"""Parse binary data with days of week."""
return [day for day in DAYS if pattern & DAYS[day]]
def _ipmi_timestamp_to_isotime(timestamp):
"""Convert IPMI timestamp to iso8601."""
if timestamp == _UNSPECIFIED_TIMESTAMP:
raise exception.InvalidIPMITimestamp(_('IPMI timestamp is invalid or '
'unspecified'))
if timestamp <= _INIT_TIMESTAMP_MAX:
raise exception.InvalidIPMITimestamp(_('IPMI initialization is not '
'completed, relative time is '
'%d second') % timestamp)
return datetime.datetime.utcfromtimestamp(timestamp).isoformat()
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(POLICY_SET)
_append_to_command(cmd, _hex(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(TRIGGERS[policy['policy_trigger']] |
CPU_CORRECTION[policy['cpu_power_correction']]
| STORAGE[policy['storage']] | 0x10))
_append_to_command(cmd, _hex(ACTIONS[policy['action']] |
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
# correction time does not apply to boot time policy
policy['correction_time'] = 0
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'] = 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'] = TRIGGERS_REV[raw_int[4] & 0x0F]
policy['power_policy'] = bool(raw_int[4] & 0x10)
power_correction = CPU_CORRECTION_REV[raw_int[4] & 0x60]
policy['cpu_power_correction'] = power_correction
policy['storage'] = STORAGE_REV[raw_int[4] & 0x80]
policy['action'] = ACTIONS_REV[raw_int[5] & 0x01]
policy['power_domain'] = 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(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(CAPABILITIES_GET)
_append_to_command(cmd, _hex(DOMAINS[data['domain_id']]))
power_policy = 0x10
_append_to_command(cmd, _hex(TRIGGERS[data['policy_trigger']] |
power_policy |
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'] = DOMAINS_REV[raw_int[20] & 0x0F]
power_domain = 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(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 = DOMAINS[control_data['domain_id']]
policy_id = 0
elif scope == 'policy':
flags = '0x05' if enable else '0x04'
domain_id = 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(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(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(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(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(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
def reset_statistics(data):
"""Return hex data for reset statistics command."""
cmd = _create_command_head(STATISTICS_RESET)
global_scope = data['scope'] == 'global'
if 'parameter_name' in data:
# statistics parameter is set, get corresponding value
mode = STATISTICS['global'][data['parameter_name']]
# domain id should be always 0x00 for global reset by parameter name
data['domain_id'] = 'platform'
else:
mode = 0x00 if global_scope else 0x01
_append_to_command(cmd, _hex(mode))
if global_scope:
data['policy_id'] = 0x00 # will be ignored
_add_domain_policy_id(cmd, data)
return cmd
def get_statistics(data):
"""Return hex data for get statistics command."""
cmd = _create_command_head(STATISTICS_GET)
scope = data['scope']
_append_to_command(cmd, _hex(STATISTICS[scope][data['parameter_name']]))
if scope == 'global':
data['policy_id'] = 0x00 # will be ignored
# case for "special" Node Manager global parameters (Mode 0x1B - 0x1F)
if 'domain_id' not in data:
data['domain_id'] = 'platform' # 0x00
_add_domain_policy_id(cmd, data)
return cmd
@_handle_parsing_error
def parse_statistics(raw_data):
"""Parse statistics data."""
statistics = {}
raw_int = _raw_to_int(raw_data)
statistics_values = struct.unpack('<HHHHII', bytearray(
raw_int[3:19]))
statistics_names = ('current_value', 'minimum_value',
'maximum_value', 'average_value',
'timestamp', 'reporting_period')
_add_to_dict(statistics, statistics_values, statistics_names)
try:
isotime = _ipmi_timestamp_to_isotime(statistics['timestamp'])
except exception.InvalidIPMITimestamp as e:
# there is not "bad time" in standard, reset to start the epoch
statistics['timestamp'] = _INVALID_TIME
LOG.warning(_LW('Invalid timestamp in Node Nanager statistics '
'data: %s'), six.text_type(e))
else:
statistics['timestamp'] = isotime
statistics['domain_id'] = DOMAINS_REV[raw_int[19] & 0x0F]
statistics['administrative_enabled'] = bool(raw_int[19] & 0x10)
statistics['operational_state'] = bool(raw_int[19] & 0x20)
statistics['measurement_state'] = bool(raw_int[19] & 0x40)
statistics['activation_state'] = bool(raw_int[19] & 0x80)
return statistics
# 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])