summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Tantsur <dtantsur@protonmail.com>2020-07-29 18:07:36 +0200
committerDmitry Tantsur <dtantsur@protonmail.com>2020-07-31 13:35:34 +0200
commit7fb098aa0b282a42e64f832bfe540110416cc71b (patch)
tree1fed4a74ab44c3e0357e2d48b3b53b0f4258a6e0
parent622ca733e2641b9c59e213a26749e9e76d3ea43d (diff)
downloadironic-python-agent-7fb098aa0b282a42e64f832bfe540110416cc71b.tar.gz
Import example hardware managers from ipa-example-hardware-managers
They're not easily discoverable there, let's keep them in tree. The examples have been restructured to have two different projects ready to be copied and adjusted. PEP8 failures have been fixed. Change-Id: I2af04f4b7f9a2109fe83ec517e716159331a48bb Co-Authored-By: Jay Faulkner <jay@jvf.cc>
-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