diff --git a/ironic_staging_drivers/intel_nm/__init__.py b/ironic_staging_drivers/intel_nm/__init__.py new file mode 100644 index 0000000..f6b6cf1 --- /dev/null +++ b/ironic_staging_drivers/intel_nm/__init__.py @@ -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') diff --git a/ironic_staging_drivers/intel_nm/control_schema.json b/ironic_staging_drivers/intel_nm/control_schema.json new file mode 100644 index 0000000..a899044 --- /dev/null +++ b/ironic_staging_drivers/intel_nm/control_schema.json @@ -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 +} + diff --git a/ironic_staging_drivers/intel_nm/get_cap_schema.json b/ironic_staging_drivers/intel_nm/get_cap_schema.json new file mode 100644 index 0000000..5e06a0b --- /dev/null +++ b/ironic_staging_drivers/intel_nm/get_cap_schema.json @@ -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 +} + diff --git a/ironic_staging_drivers/intel_nm/ipmi.py b/ironic_staging_drivers/intel_nm/ipmi.py new file mode 100644 index 0000000..18a25be --- /dev/null +++ b/ironic_staging_drivers/intel_nm/ipmi.py @@ -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) diff --git a/ironic_staging_drivers/intel_nm/main_ids_schema.json b/ironic_staging_drivers/intel_nm/main_ids_schema.json new file mode 100644 index 0000000..87d2de9 --- /dev/null +++ b/ironic_staging_drivers/intel_nm/main_ids_schema.json @@ -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 +} diff --git a/ironic_staging_drivers/intel_nm/nm_commands.py b/ironic_staging_drivers/intel_nm/nm_commands.py new file mode 100644 index 0000000..c73badb --- /dev/null +++ b/ironic_staging_drivers/intel_nm/nm_commands.py @@ -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('=1.1.0 # Apache-2.0 pbr>=1.6 # Apache-2.0 +oslo.concurrency>=3.5.0 # Apache-2.0 +oslo.config>=3.7.0 # Apache-2.0 oslo.i18n>=2.1.0 # Apache-2.0 +oslo.log>=1.14.0 # Apache-2.0 oslo.utils>=3.5.0 # Apache-2.0 +six>=1.9.0 # MIT +jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT diff --git a/setup.cfg b/setup.cfg index f718dae..23f8fb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,8 @@ ironic.drivers = fake_wol_fake = ironic_staging_drivers.wol:FakeWakeOnLanFakeDriver pxe_wol_iscsi = ironic_staging_drivers.wol:PXEWakeOnLanISCSIDriver pxe_wol_agent = ironic_staging_drivers.wol:PXEWakeOnLanAgentDriver + agent_ipmitool_nm = ironic_staging_drivers.intel_nm:AgentAndIPMIToolIntelNMDriver + fake_nm = ironic_staging_drivers.intel_nm:FakeIntelNMDriver [build_sphinx] source-dir = doc/source