diff --git a/config-generator.conf b/config-generator.conf index 054f5f6ae..f3c5a581e 100644 --- a/config-generator.conf +++ b/config-generator.conf @@ -3,6 +3,7 @@ output_file = example.conf namespace = ironic_inspector namespace = ironic_inspector.common.ironic namespace = ironic_inspector.common.swift +namespace = ironic_inspector.plugins.capabilities namespace = ironic_inspector.plugins.discovery namespace = keystonemiddleware.auth_token namespace = oslo.db diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 23f49d612..2ee84f5fc 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -184,15 +184,22 @@ introspection data. Note that order does matter in this option. These are plugins that are enabled by default and should not be disabled, unless you understand what you're doing: -``ramdisk_error`` - reports error, if ``error`` field is set by the ramdisk, also optionally - stores logs from ``logs`` field, see :ref:`api` for details. ``scheduler`` validates and updates basic hardware scheduling properties: CPU number and architecture, memory and disk size. ``validate_interfaces`` validates network interfaces information. +The following plugins are enabled by default, but can be disabled if not +needed: + +``ramdisk_error`` + reports error, if ``error`` field is set by the ramdisk, also optionally + stores logs from ``logs`` field, see :ref:`api` for details. +``capabilities`` + detect node capabilities: CPU, boot mode, etc. See `Capabilities + Detection`_ for more details. + Here are some plugins that can be additionally enabled: ``example`` @@ -330,3 +337,42 @@ Limitations: * the unprocessed data is never cleaned from the store * check for stored data presence is performed in background; missing data situation still results in a ``202`` response + +Capabilities Detection +~~~~~~~~~~~~~~~~~~~~~~ + +Starting with the Newton release, **Ironic Inspector** can optionally discover +several node capabilities. A recent (Newton or newer) IPA image is required +for it to work. + +Boot mode +^^^^^^^^^ + +The current boot mode (BIOS or UEFI) can be detected and recorded as +``boot_mode`` capability in Ironic. It will make some drivers to change their +behaviour to account for this capability. Set the ``[capabilities]boot_mode`` +configuration option to ``True`` to enable. + +CPU capabilities +^^^^^^^^^^^^^^^^ + +Several CPU flags are detected by default and recorded as following +capabilities: + +* ``cpu_aes`` AES instructions. + +* ``cpu_vt`` virtualization support. + +* ``cpu_txt`` TXT support. + +* ``cpu_hugepages`` huge pages (2 MiB) support. + +* ``cpu_hugepages_1g`` huge pages (1 GiB) support. + +It is possible to define your own rules for detecting CPU capabilities. +Set the ``[capabilities]cpu_flags`` configuration option to a mapping between +a CPU flag and a capability, for example:: + + cpu_flags = aes:cpu_aes,svm:cpu_vt,vmx:cpu_vt + +See the default value of this option for a more detail example. diff --git a/example.conf b/example.conf index 0e05ac55e..64c8037fa 100644 --- a/example.conf +++ b/example.conf @@ -76,10 +76,11 @@ # If set to true, the logging level will be set to DEBUG instead of # the default INFO level. (boolean value) +# Note: This option can be changed without restarting. #debug = false -# If set to false, the logging level will be set to WARNING instead of -# the default INFO level. (boolean value) +# DEPRECATED: If set to false, the logging level will be set to +# WARNING instead of the default INFO level. (boolean value) # This option is deprecated for removal. # Its value may be silently ignored in the future. #verbose = true @@ -168,6 +169,20 @@ #fatal_deprecations = false +[capabilities] + +# +# From ironic_inspector.plugins.capabilities +# + +# Whether to store the boot mode (BIOS or UEFI). (boolean value) +#boot_mode = false + +# Mapping between a CPU flag and a capability to set if this flag is +# present. (dict value) +#cpu_flags = aes:cpu_aes,pdpe1gb:cpu_hugepages_1g,pse:cpu_hugepages,smx:cpu_txt,svm:cpu_vt,vmx:cpu_vt + + [cors] # @@ -175,7 +190,9 @@ # # Indicate whether this resource may be shared with the domain -# received in the requests "origin" header. (list value) +# received in the requests "origin" header. Format: +# "://[:]", no trailing slash. Example: +# https://horizon.example.com (list value) #allowed_origin = # Indicate that the actual request can include user credentials @@ -184,7 +201,7 @@ # Indicate which headers are safe to expose to the API. Defaults to # HTTP Simple Headers. (list value) -#expose_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma +#expose_headers = # Maximum cache age of CORS preflight requests. (integer value) #max_age = 3600 @@ -205,7 +222,9 @@ # # Indicate whether this resource may be shared with the domain -# received in the requests "origin" header. (list value) +# received in the requests "origin" header. Format: +# "://[:]", no trailing slash. Example: +# https://horizon.example.com (list value) #allowed_origin = # Indicate that the actual request can include user credentials @@ -214,7 +233,7 @@ # Indicate which headers are safe to expose to the API. Defaults to # HTTP Simple Headers. (list value) -#expose_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma +#expose_headers = # Maximum cache age of CORS preflight requests. (integer value) #max_age = 3600 @@ -340,8 +359,8 @@ # From ironic_inspector # -# SQLite3 database to store nodes under introspection, required. Do -# not use :memory: here, it won't work. DEPRECATED: use +# DEPRECATED: SQLite3 database to store nodes under introspection, +# required. Do not use :memory: here, it won't work. DEPRECATED: use # [database]/connection. (string value) # This option is deprecated for removal. # Its value may be silently ignored in the future. @@ -423,8 +442,9 @@ # Domain name to scope to (unknown value) #domain_name = -# Keystone admin endpoint. DEPRECATED: Use [keystone_authtoken] -# section for keystone token validation. (string value) +# DEPRECATED: Keystone admin endpoint. DEPRECATED: Use +# [keystone_authtoken] section for keystone token validation. (string +# value) # Deprecated group/name - [discoverd]/identity_uri # This option is deprecated for removal. # Its value may be silently ignored in the future. @@ -445,9 +465,9 @@ # (integer value) #max_retries = 30 -# Keystone authentication endpoint for accessing Ironic API. Use -# [keystone_authtoken] section for keystone token validation. (string -# value) +# DEPRECATED: Keystone authentication endpoint for accessing Ironic +# API. Use [keystone_authtoken] section for keystone token validation. +# (string value) # Deprecated group/name - [discoverd]/os_auth_url # This option is deprecated for removal. # Its value may be silently ignored in the future. @@ -457,8 +477,9 @@ # Ironic endpoint type. (string value) #os_endpoint_type = internalURL -# Password for accessing Ironic API. Use [keystone_authtoken] section -# for keystone token validation. (string value) +# DEPRECATED: Password for accessing Ironic API. Use +# [keystone_authtoken] section for keystone token validation. (string +# value) # Deprecated group/name - [discoverd]/os_password # This option is deprecated for removal. # Its value may be silently ignored in the future. @@ -471,16 +492,18 @@ # Ironic service type. (string value) #os_service_type = baremetal -# Tenant name for accessing Ironic API. Use [keystone_authtoken] -# section for keystone token validation. (string value) +# DEPRECATED: Tenant name for accessing Ironic API. Use +# [keystone_authtoken] section for keystone token validation. (string +# value) # Deprecated group/name - [discoverd]/os_tenant_name # This option is deprecated for removal. # Its value may be silently ignored in the future. # Reason: Use options presented by configured keystone auth plugin. #os_tenant_name = -# User name for accessing Ironic API. Use [keystone_authtoken] section -# for keystone token validation. (string value) +# DEPRECATED: User name for accessing Ironic API. Use +# [keystone_authtoken] section for keystone token validation. (string +# value) # Deprecated group/name - [discoverd]/os_username # This option is deprecated for removal. # Its value may be silently ignored in the future. @@ -598,7 +621,8 @@ # Determines the frequency at which the list of revoked tokens is # retrieved from the Identity service (in seconds). A high number of # revocation events combined with a low cache duration may -# significantly reduce performance. (integer value) +# significantly reduce performance. Only valid for PKI tokens. +# (integer value) #revocation_cache_time = 10 # (Optional) If defined, indicate whether token data should be @@ -718,7 +742,7 @@ # the Nova scheduler. Hook 'validate_interfaces' ensures that valid # NIC data was provided by the ramdisk.Do not exclude these two unless # you really know what you're doing. (string value) -#default_processing_hooks = ramdisk_error,root_disk_selection,scheduler,validate_interfaces +#default_processing_hooks = ramdisk_error,root_disk_selection,scheduler,validate_interfaces,capabilities # Comma-separated list of enabled hooks for processing pipeline. The # default for this is $default_processing_hooks, hooks can be added @@ -815,13 +839,13 @@ # (integer value) #max_retries = 2 -# Keystone authentication URL (string value) +# DEPRECATED: Keystone authentication URL (string value) # This option is deprecated for removal. # Its value may be silently ignored in the future. # Reason: Use options presented by configured keystone auth plugin. #os_auth_url = -# Keystone authentication API version (string value) +# DEPRECATED: Keystone authentication API version (string value) # This option is deprecated for removal. # Its value may be silently ignored in the future. # Reason: Use options presented by configured keystone auth plugin. diff --git a/ironic_inspector/conf.py b/ironic_inspector/conf.py index 5fd437d6f..8f8748a79 100644 --- a/ironic_inspector/conf.py +++ b/ironic_inspector/conf.py @@ -79,7 +79,7 @@ PROCESSING_OPTS = [ deprecated_group='discoverd'), cfg.StrOpt('default_processing_hooks', default='ramdisk_error,root_disk_selection,scheduler,' - 'validate_interfaces', + 'validate_interfaces,capabilities', help='Comma-separated list of default hooks for processing ' 'pipeline. Hook \'scheduler\' updates the node with the ' 'minimum properties required by the Nova scheduler. ' diff --git a/ironic_inspector/plugins/capabilities.py b/ironic_inspector/plugins/capabilities.py new file mode 100644 index 000000000..dc9d5b3b6 --- /dev/null +++ b/ironic_inspector/plugins/capabilities.py @@ -0,0 +1,101 @@ +# 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. + +"""Gather capabilities from inventory.""" + +from oslo_config import cfg + +from ironic_inspector.common.i18n import _LI, _LW +from ironic_inspector.plugins import base +from ironic_inspector import utils + + +DEFAULT_CPU_FLAGS_MAPPING = { + 'vmx': 'cpu_vt', + 'svm': 'cpu_vt', + 'aes': 'cpu_aes', + 'pse': 'cpu_hugepages', + 'pdpe1gb': 'cpu_hugepages_1g', + 'smx': 'cpu_txt', +} + +CAPABILITIES_OPTS = [ + cfg.BoolOpt('boot_mode', + default=False, + help='Whether to store the boot mode (BIOS or UEFI).'), + cfg.DictOpt('cpu_flags', + default=DEFAULT_CPU_FLAGS_MAPPING, + help='Mapping between a CPU flag and a capability to set ' + 'if this flag is present.'), +] + + +def list_opts(): + return [ + ('capabilities', CAPABILITIES_OPTS) + ] + +CONF = cfg.CONF +CONF.register_opts(CAPABILITIES_OPTS, group='capabilities') +LOG = utils.getProcessingLogger(__name__) + + +class CapabilitiesHook(base.ProcessingHook): + """Processing hook for detecting capabilities.""" + + def _detect_boot_mode(self, inventory, node_info, data=None): + boot_mode = inventory.get('boot', {}).get('current_boot_mode') + if boot_mode is not None: + LOG.info(_LI('Boot mode was %s'), boot_mode, + data=data, node_info=node_info) + return {'boot_mode': boot_mode} + else: + LOG.warning(_LW('No boot mode information available'), + data=data, node_info=node_info) + return {} + + def _detect_cpu_flags(self, inventory, node_info, data=None): + flags = inventory['cpu'].get('flags') + if not flags: + LOG.warning(_LW('No CPU flags available, please update your ' + 'introspection ramdisk'), + data=data, node_info=node_info) + return {} + + flags = set(flags) + caps = {} + for flag, name in CONF.capabilities.cpu_flags.items(): + if flag in flags: + caps[name] = 'true' + + LOG.info(_LI('CPU capabilities: %s'), list(caps), + data=data, node_info=node_info) + return caps + + def before_update(self, introspection_data, node_info, **kwargs): + inventory = utils.get_inventory(introspection_data) + caps = {} + if CONF.capabilities.boot_mode: + caps.update(self._detect_boot_mode(inventory, node_info, + introspection_data)) + + caps.update(self._detect_cpu_flags(inventory, node_info, + introspection_data)) + + if caps: + LOG.debug('New capabilities: %s', caps, node_info=node_info, + data=introspection_data) + node_info.update_capabilities(**caps) + else: + LOG.debug('No new capabilities detected', node_info=node_info, + data=introspection_data) diff --git a/ironic_inspector/test/unit/test_plugins_capabilities.py b/ironic_inspector/test/unit/test_plugins_capabilities.py new file mode 100644 index 000000000..41eafaf82 --- /dev/null +++ b/ironic_inspector/test/unit/test_plugins_capabilities.py @@ -0,0 +1,77 @@ +# 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_config import cfg + +from ironic_inspector import node_cache +from ironic_inspector.plugins import base +from ironic_inspector.plugins import capabilities +from ironic_inspector.test import base as test_base + + +CONF = cfg.CONF + + +@mock.patch.object(node_cache.NodeInfo, 'update_capabilities', autospec=True) +class TestCapabilitiesHook(test_base.NodeTest): + hook = capabilities.CapabilitiesHook() + + def test_loadable_by_name(self, mock_caps): + base.CONF.set_override('processing_hooks', 'capabilities', + 'processing') + ext = base.processing_hooks_manager()['capabilities'] + self.assertIsInstance(ext.obj, capabilities.CapabilitiesHook) + + def test_no_data(self, mock_caps): + self.hook.before_update(self.data, self.node_info) + self.assertFalse(mock_caps.called) + + def test_boot_mode(self, mock_caps): + CONF.set_override('boot_mode', True, 'capabilities') + self.inventory['boot'] = {'current_boot_mode': 'uefi'} + + self.hook.before_update(self.data, self.node_info) + mock_caps.assert_called_once_with(self.node_info, boot_mode='uefi') + + def test_boot_mode_disabled(self, mock_caps): + self.inventory['boot'] = {'current_boot_mode': 'uefi'} + + self.hook.before_update(self.data, self.node_info) + self.assertFalse(mock_caps.called) + + def test_cpu_flags(self, mock_caps): + self.inventory['cpu']['flags'] = ['fpu', 'vmx', 'aes', 'pse', 'smx'] + + self.hook.before_update(self.data, self.node_info) + mock_caps.assert_called_once_with(self.node_info, + cpu_vt='true', + cpu_hugepages='true', + cpu_txt='true', + cpu_aes='true') + + def test_cpu_no_known_flags(self, mock_caps): + self.inventory['cpu']['flags'] = ['fpu'] + + self.hook.before_update(self.data, self.node_info) + self.assertFalse(mock_caps.called) + + def test_cpu_flags_custom(self, mock_caps): + CONF.set_override('cpu_flags', {'fpu': 'new_cap'}, + 'capabilities') + self.inventory['cpu']['flags'] = ['fpu', 'vmx', 'aes', 'pse'] + + self.hook.before_update(self.data, self.node_info) + mock_caps.assert_called_once_with(self.node_info, + new_cap='true') diff --git a/releasenotes/notes/capabilities-15cc2268d661f0a0.yaml b/releasenotes/notes/capabilities-15cc2268d661f0a0.yaml new file mode 100644 index 000000000..f2b28d6c7 --- /dev/null +++ b/releasenotes/notes/capabilities-15cc2268d661f0a0.yaml @@ -0,0 +1,4 @@ +--- +features: + - Added a new "capabilities" processing hook detecting the CPU and boot mode + capabilities (the latter disabled by default). diff --git a/setup.cfg b/setup.cfg index 8e0338f3f..b56a3e27b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ ironic_inspector.hooks.processing = example = ironic_inspector.plugins.example:ExampleProcessingHook extra_hardware = ironic_inspector.plugins.extra_hardware:ExtraHardwareHook raid_device = ironic_inspector.plugins.raid_device:RaidDeviceDetection + capabilities = ironic_inspector.plugins.capabilities:CapabilitiesHook # Deprecated name for raid_device, don't confuse with root_disk_selection root_device_hint = ironic_inspector.plugins.raid_device:RootDeviceHintHook ironic_inspector.hooks.node_not_found = @@ -58,6 +59,7 @@ oslo.config.opts = ironic_inspector.common.ironic = ironic_inspector.common.ironic:list_opts ironic_inspector.common.swift = ironic_inspector.common.swift:list_opts ironic_inspector.plugins.discovery = ironic_inspector.plugins.discovery:list_opts + ironic_inspector.plugins.capabilities = ironic_inspector.plugins.capabilities:list_opts oslo.config.opts.defaults = ironic_inspector = ironic_inspector.conf:set_config_defaults