summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2020-08-07 15:55:29 +0000
committerGerrit Code Review <review@openstack.org>2020-08-07 15:55:29 +0000
commit36c5c324660d08cd0ac3cd3a43bff17f62c064e6 (patch)
tree260a37574627185ba4653b456cabf0f0930ce0f8
parent008316e7e306cafec19470e70da22a97d296471d (diff)
parent7fb098aa0b282a42e64f832bfe540110416cc71b (diff)
downloadironic-python-agent-36c5c324660d08cd0ac3cd3a43bff17f62c064e6.tar.gz
Merge "Import example hardware managers from ipa-example-hardware-managers"
-rw-r--r--doc/source/contributor/hardware_managers.rst5
-rw-r--r--examples/README.rst43
-rw-r--r--examples/business-logic/example_business_logic.py98
-rw-r--r--examples/business-logic/setup.cfg19
-rw-r--r--examples/business-logic/setup.py6
-rw-r--r--examples/vendor-device/example_device.py152
-rw-r--r--examples/vendor-device/setup.cfg20
-rw-r--r--examples/vendor-device/setup.py6
-rw-r--r--tox.ini9
-rw-r--r--zuul.d/ironic-python-agent-jobs.yaml20
-rw-r--r--zuul.d/project.yaml3
11 files changed, 377 insertions, 4 deletions
diff --git a/doc/source/contributor/hardware_managers.rst b/doc/source/contributor/hardware_managers.rst
index 8fc04733..282acd5b 100644
--- a/doc/source/contributor/hardware_managers.rst
+++ b/doc/source/contributor/hardware_managers.rst
@@ -46,6 +46,11 @@ may want to implement are list_hardware_info(), to add additional hardware
the GenericHardwareManager is unable to identify and erase_devices(), to
erase devices in ways other than ATA secure erase or shredding.
+The examples_ directory has two example hardware managers that can be copied
+and adapter for your use case.
+
+.. _examples: https://opendev.org/openstack/ironic-python-agent/src/branch/master/examples
+
Custom HardwareManagers and Cleaning
------------------------------------
One of the reasons to build a custom hardware manager is to expose extra steps
diff --git a/examples/README.rst b/examples/README.rst
new file mode 100644
index 00000000..73f5be6b
--- /dev/null
+++ b/examples/README.rst
@@ -0,0 +1,43 @@
+Example Hardware Managers
+=========================
+
+``vendor-device``
+-----------------
+
+This example manager is meant to demonstrate good patterns for developing a
+device-specific hardware manager, such as for a specific version of NIC or
+disk.
+
+Use Cases include:
+
+* Adding device-specific clean-steps, such as to flash firmware or
+ verify it's still properly working after being provisioned.
+* Implementing erase_device() using a vendor-provided utility for a given
+ disk model.
+
+``business-logic``
+------------------
+
+This example manager is meant to demonstrate how cleaning and the agent can
+use the node object and the node itself to enforce business logic and node
+consistency.
+
+Use Cases include:
+
+* Quality control on hardware by ensuring no component is beyond its useful
+ life.
+* Asserting truths about the node; such as number of disks or total RAM.
+* Reporting metrics about the node's hardware state.
+* Overriding logic of get_os_install_device().
+* Inserting additional deploy steps.
+
+Make your own Manager based on these
+------------------------------------
+
+To make your own hardware manager based on these examples, copy a relevant
+example out of this directory. Modify class names and entrypoints in setup.cfg
+to be not-examples.
+
+Since the entrypoints are defined in setup.cfg, simply installing your new
+python package alongside IPA in a custom ramdisk should be enough to enable
+the new hardware manager.
diff --git a/examples/business-logic/example_business_logic.py b/examples/business-logic/example_business_logic.py
new file mode 100644
index 00000000..611b698a
--- /dev/null
+++ b/examples/business-logic/example_business_logic.py
@@ -0,0 +1,98 @@
+# Copyright 2015 Rackspace, 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.
+
+import time
+
+from oslo_log import log
+
+from ironic_python_agent import errors
+from ironic_python_agent import hardware
+
+LOG = log.getLogger()
+
+
+class ExampleBusinessLogicHardwareManager(hardware.HardwareManager):
+ """Example hardware manager to enforce business logic"""
+
+ # All hardware managers have a name and a version.
+ # Version should be bumped anytime a change is introduced. This will
+ # signal to Ironic that if automatic node cleaning is in progress to
+ # restart it from the beginning, to ensure consistency. The value can
+ # be anything; it's checked for equality against previously seen
+ # name:manager pairs.
+ HARDWARE_MANAGER_NAME = 'ExampleBusinessLogicHardwareManager'
+ HARDWARE_MANAGER_VERSION = '1'
+
+ def evaluate_hardware_support(self):
+ """Declare level of hardware support provided.
+
+ Since this example is explicitly about enforcing business logic during
+ cleaning, we want to return a static value.
+
+ :returns: HardwareSupport level for this manager.
+ """
+ return hardware.HardwareSupport.SERVICE_PROVIDER
+
+ def get_clean_steps(self, node, ports):
+ """Get a list of clean steps with priority.
+
+ Define any clean steps added by this manager here. These will be mixed
+ with other loaded managers that support this hardware, and ordered by
+ priority. Higher priority steps run earlier.
+
+ Note that out-of-band clean steps may also be provided by Ironic.
+ These will follow the same priority ordering even though they are not
+ executed by IPA.
+
+ There is *no guarantee whatsoever* that steps defined here will be
+ executed by this HardwareManager. When it comes time to run these
+ steps, they'll be called using dispatch_to_managers() just like any
+ other IPA HardwareManager method. This means if they are unique to
+ your hardware, they should be uniquely named. For example,
+ upgrade_firmware would be a bad step name. Whereas
+ upgrade_foobar_device_firmware would be better.
+
+ :param node: The node object as provided by Ironic.
+ :param ports: Port objects as provided by Ironic.
+ :returns: A list of cleaning steps, as a list of dicts.
+ """
+ # While obviously you could actively run code here, generally this
+ # should just return a static value, as any initialization and
+ # detection should've been done in evaluate_hardware_support().
+ return [{
+ 'step': 'companyx_verify_device_lifecycle',
+ 'priority': 472,
+ # If you need Ironic to coordinate a reboot after this step
+ # runs, but before continuing cleaning, this should be true.
+ 'reboot_requested': False,
+ # If it's safe for Ironic to abort cleaning while this step
+ # runs, this should be true.
+ 'abortable': True
+ }]
+
+ # Other examples of interesting cleaning steps for this kind of hardware
+ # manager would include verifying node.properties matches current state of
+ # the node, checking smart stats to ensure the disk is not soon to fail,
+ # or enforcing security policies.
+ def companyx_verify_device_lifecycle(self, node, ports):
+ """Verify node is not beyond useful life of 3 years."""
+ create_date = node.get('created_at')
+ if create_date is not None:
+ server_age = time.time() - time.mktime(time.strptime(create_date))
+ if server_age > (60 * 60 * 24 * 365 * 3):
+ raise errors.CleaningError(
+ 'Server is too old to pass cleaning!')
+ else:
+ LOG.info('Node is %s seconds old, younger than 3 years, '
+ 'cleaning passes.', server_age)
diff --git a/examples/business-logic/setup.cfg b/examples/business-logic/setup.cfg
new file mode 100644
index 00000000..04cfc46d
--- /dev/null
+++ b/examples/business-logic/setup.cfg
@@ -0,0 +1,19 @@
+[metadata]
+name = example-business-logic
+author = Jay Faulkner
+author-email = jay@jvf.cc
+summary = IPA Example Hardware Managers: Business Logic
+license = Apache-2
+classifier =
+ Intended Audience :: Developers
+ Operating System :: OS Independent
+ License :: OSI Approved :: Apache Software License
+ Programming Language :: Python :: 3
+
+[files]
+modules =
+ example_business_logic
+
+[entry_points]
+ironic_python_agent.hardware_managers =
+ example_business_logic = example_business_logic:ExampleBusinessLogicHardwareManager
diff --git a/examples/business-logic/setup.py b/examples/business-logic/setup.py
new file mode 100644
index 00000000..ed58d0f2
--- /dev/null
+++ b/examples/business-logic/setup.py
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+import setuptools
+
+setuptools.setup(
+ setup_requires=['pbr'],
+ pbr=True)
diff --git a/examples/vendor-device/example_device.py b/examples/vendor-device/example_device.py
new file mode 100644
index 00000000..0f06a5f6
--- /dev/null
+++ b/examples/vendor-device/example_device.py
@@ -0,0 +1,152 @@
+# Copyright 2015 Rackspace, 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
+
+from ironic_python_agent import hardware
+
+LOG = log.getLogger()
+
+
+# All the helper methods should be kept outside of the HardwareManager
+# so they'll never get accidentally called by dispatch_to_managers()
+def _initialize_hardware():
+ """Example method for initalizing hardware."""
+ # Perform any operations here that are required to initialize your
+ # hardware.
+ LOG.debug('Loading drivers, settling udevs, and generally initalizing')
+ pass
+
+
+def _detect_hardware():
+ """Example method for hardware detection."""
+ # For this example, return true if hardware is detected, false if not
+ LOG.debug('Looking for example device')
+ return True
+
+
+def _is_latest_firmware():
+ """Detect if device is running latest firmware."""
+ # Actually detect the firmware version instead of returning here.
+ return True
+
+
+def _upgrade_firmware():
+ """Upgrade firmware on device."""
+ # Actually perform firmware upgrade instead of returning here.
+ return True
+
+
+class ExampleDeviceHardwareManager(hardware.HardwareManager):
+ """Example hardware manager to support a single device"""
+
+ # All hardware managers have a name and a version.
+ # Version should be bumped anytime a change is introduced. This will
+ # signal to Ironic that if automatic node cleaning is in progress to
+ # restart it from the beginning, to ensure consistency. The value can
+ # be anything; it's checked for equality against previously seen
+ # name:manager pairs.
+ HARDWARE_MANAGER_NAME = 'ExampleDeviceHardwareManager'
+ HARDWARE_MANAGER_VERSION = '1'
+
+ def evaluate_hardware_support(self):
+ """Declare level of hardware support provided.
+
+ Since this example covers a case of supporting a specific device,
+ this method is where you would do anything needed to initalize that
+ device, including loading drivers, and then detect if one exists.
+
+ In some cases, if you expect the hardware to be available on any node
+ running this hardware manager, or it's undetectable, you may want to
+ return a static value here.
+
+ Be aware all managers' loaded in IPA will run this method before IPA
+ performs a lookup or begins heartbeating, so the time needed to
+ execute this method will make cleaning and deploying slower.
+
+ :returns: HardwareSupport level for this manager.
+ """
+ _initialize_hardware()
+ if _detect_hardware():
+ # This actually resolves down to an int. Upstream IPA will never
+ # return a value higher than 2 (HardwareSupport.MAINLINE). This
+ # means your managers should always be SERVICE_PROVIDER or higher.
+ LOG.debug('Found example device, returning SERVICE_PROVIDER')
+ return hardware.HardwareSupport.SERVICE_PROVIDER
+ else:
+ # If the hardware isn't supported, return HardwareSupport.NONE (0)
+ # in order to prevent IPA from loading its clean steps or
+ # attempting to use any methods inside it.
+ LOG.debug('No example devices found, returning NONE')
+ return hardware.HardwareSupport.NONE
+
+ def get_clean_steps(self, node, ports):
+ """Get a list of clean steps with priority.
+
+ Define any clean steps added by this manager here. These will be mixed
+ with other loaded managers that support this hardware, and ordered by
+ priority. Higher priority steps run earlier.
+
+ Note that out-of-band clean steps may also be provided by Ironic.
+ These will follow the same priority ordering even though they are not
+ executed by IPA.
+
+ There is *no guarantee whatsoever* that steps defined here will be
+ executed by this HardwareManager. When it comes time to run these
+ steps, they'll be called using dispatch_to_managers() just like any
+ other IPA HardwareManager method. This means if they are unique to
+ your hardware, they should be uniquely named. For example,
+ upgrade_firmware would be a bad step name. Whereas
+ upgrade_foobar_device_firmware would be better.
+
+ :param node: The node object as provided by Ironic.
+ :param ports: Port objects as provided by Ironic.
+ :returns: A list of cleaning steps, as a list of dicts.
+ """
+ # While obviously you could actively run code here, generally this
+ # should just return a static value, as any initialization and
+ # detection should've been done in evaluate_hardware_support().
+ return [{
+ 'step': 'upgrade_example_device_model1234_firmware',
+ 'priority': 37,
+ # If you need Ironic to coordinate a reboot after this step
+ # runs, but before continuing cleaning, this should be true.
+ 'reboot_requested': True,
+ # If it's safe for Ironic to abort cleaning while this step
+ # runs, this should be true.
+ 'abortable': False
+ }]
+
+ def upgrade_example_device_model1234_firmware(self, node, ports):
+ """Upgrade firmware on Example Device Model #1234."""
+ # Any commands needed to perform the firmware upgrade should go here.
+ # If you plan on actually flashing firmware every cleaning cycle, you
+ # should ensure your device will not experience flash exhaustion. A
+ # good practice in some environments would be to check the firmware
+ # version against a constant in the code, and noop the method if an
+ # upgrade is not needed.
+ if _is_latest_firmware():
+ LOG.debug('Latest firmware already flashed, skipping')
+ # Return values are ignored here on success
+ return True
+ else:
+ LOG.debug('Firmware version X found, upgrading to Y')
+ # Perform firmware upgrade.
+ try:
+ _upgrade_firmware()
+ except Exception as e:
+ # Log and pass through the exception so cleaning will fail
+ LOG.exception(e)
+ raise
+ return True
diff --git a/examples/vendor-device/setup.cfg b/examples/vendor-device/setup.cfg
new file mode 100644
index 00000000..347b2ad6
--- /dev/null
+++ b/examples/vendor-device/setup.cfg
@@ -0,0 +1,20 @@
+[metadata]
+name = example-vendor-device
+author = Jay Faulkner
+author-email = jay@jvf.cc
+summary = IPA Example Hardware Managers: Vendor Device
+license = Apache-2
+classifier =
+ Intended Audience :: Developers
+ Operating System :: OS Independent
+ License :: OSI Approved :: Apache Software License
+ Programming Language :: Python :: 3
+ Development Status :: 4 - Beta
+
+[files]
+modules =
+ example_device
+
+[entry_points]
+ironic_python_agent.hardware_managers =
+ example_device = example_device:ExampleDeviceHardwareManager
diff --git a/examples/vendor-device/setup.py b/examples/vendor-device/setup.py
new file mode 100644
index 00000000..ed58d0f2
--- /dev/null
+++ b/examples/vendor-device/setup.py
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+import setuptools
+
+setuptools.setup(
+ setup_requires=['pbr'],
+ pbr=True)
diff --git a/tox.ini b/tox.ini
index f3fdec18..db526e3e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -35,11 +35,11 @@ commands = stestr run {posargs}
[testenv:pep8]
whitelist_externals = bash
commands =
- flake8 {posargs:ironic_python_agent imagebuild}
+ flake8 {posargs:ironic_python_agent examples}
# Run bashate during pep8 runs to ensure violations are caught by
# the check and gate queues.
{toxinidir}/tools/run_bashate.sh {toxinidir}
- doc8 doc/source README.rst
+ doc8 doc/source README.rst examples/README.rst
[testenv:cover]
setenv = VIRTUAL_ENV={envdir}
@@ -123,3 +123,8 @@ deps =
deps = -r{toxinidir}/test-requirements.txt
commands = bandit -r ironic_python_agent -x tests -n5 -ll -c tools/bandit.yml
+[testenv:examples]
+commands =
+ pip install -e {toxinidir}/examples/business-logic
+ pip install -e {toxinidir}/examples/vendor-device
+ python -c 'import example_business_logic; import example_device'
diff --git a/zuul.d/ironic-python-agent-jobs.yaml b/zuul.d/ironic-python-agent-jobs.yaml
index 6755320e..b0877c26 100644
--- a/zuul.d/ironic-python-agent-jobs.yaml
+++ b/zuul.d/ironic-python-agent-jobs.yaml
@@ -2,6 +2,7 @@
name: ironic-ipa-base
parent: ironic-base
irrelevant-files:
+ - ^examples/.*$
- ^test-requirements.txt$
- ^.*\.rst$
- ^doc/.*$
@@ -135,6 +136,23 @@
vars:
tox_envlist: bandit
irrelevant-files:
+ - ^examples/.*$
+ - ^test-requirements.txt$
+ - ^.*\.rst$
+ - ^doc/.*$
+ - ^ironic_python_agent/tests/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^tools/(?!bandit.yml).*$
+ - ^tox.ini$
+
+- job:
+ name: ipa-tox-examples
+ parent: openstack-tox
+ timeout: 600
+ vars:
+ tox_envlist: examples
+ irrelevant-files:
- ^test-requirements.txt$
- ^.*\.rst$
- ^doc/.*$
@@ -212,4 +230,4 @@
- job:
name: ipa-tempest-bios-ipmi-iscsi-tinyipa-src
- parent: ipa-tempest-partition-bios-ipmi-iscsi-tinyipa-src \ No newline at end of file
+ parent: ipa-tempest-partition-bios-ipmi-iscsi-tinyipa-src
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 076cc3cc..f0fc6512 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -8,6 +8,7 @@
- release-notes-jobs-python3
check:
jobs:
+ - ipa-tox-examples
# NOTE(iurygregory) Only run this two jobs since we are testing
# wholedisk + partition on tempest
- ipa-tempest-bios-ipmi-direct-src
@@ -41,4 +42,4 @@
post:
jobs:
- ironic-python-agent-build-image-tinyipa
- - ironic-python-agent-build-image-dib-centos8 \ No newline at end of file
+ - ironic-python-agent-build-image-dib-centos8