Skip to content

Commit 7fb098a

Browse files
dtantsurjayofdoom
andcommitted
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>
1 parent 622ca73 commit 7fb098a

File tree

11 files changed

+377
-4
lines changed

11 files changed

+377
-4
lines changed

doc/source/contributor/hardware_managers.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ may want to implement are list_hardware_info(), to add additional hardware
4646
the GenericHardwareManager is unable to identify and erase_devices(), to
4747
erase devices in ways other than ATA secure erase or shredding.
4848

49+
The examples_ directory has two example hardware managers that can be copied
50+
and adapter for your use case.
51+
52+
.. _examples: https://opendev.org/openstack/ironic-python-agent/src/branch/master/examples
53+
4954
Custom HardwareManagers and Cleaning
5055
------------------------------------
5156
One of the reasons to build a custom hardware manager is to expose extra steps

examples/README.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Example Hardware Managers
2+
=========================
3+
4+
``vendor-device``
5+
-----------------
6+
7+
This example manager is meant to demonstrate good patterns for developing a
8+
device-specific hardware manager, such as for a specific version of NIC or
9+
disk.
10+
11+
Use Cases include:
12+
13+
* Adding device-specific clean-steps, such as to flash firmware or
14+
verify it's still properly working after being provisioned.
15+
* Implementing erase_device() using a vendor-provided utility for a given
16+
disk model.
17+
18+
``business-logic``
19+
------------------
20+
21+
This example manager is meant to demonstrate how cleaning and the agent can
22+
use the node object and the node itself to enforce business logic and node
23+
consistency.
24+
25+
Use Cases include:
26+
27+
* Quality control on hardware by ensuring no component is beyond its useful
28+
life.
29+
* Asserting truths about the node; such as number of disks or total RAM.
30+
* Reporting metrics about the node's hardware state.
31+
* Overriding logic of get_os_install_device().
32+
* Inserting additional deploy steps.
33+
34+
Make your own Manager based on these
35+
------------------------------------
36+
37+
To make your own hardware manager based on these examples, copy a relevant
38+
example out of this directory. Modify class names and entrypoints in setup.cfg
39+
to be not-examples.
40+
41+
Since the entrypoints are defined in setup.cfg, simply installing your new
42+
python package alongside IPA in a custom ramdisk should be enough to enable
43+
the new hardware manager.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright 2015 Rackspace, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import time
16+
17+
from oslo_log import log
18+
19+
from ironic_python_agent import errors
20+
from ironic_python_agent import hardware
21+
22+
LOG = log.getLogger()
23+
24+
25+
class ExampleBusinessLogicHardwareManager(hardware.HardwareManager):
26+
"""Example hardware manager to enforce business logic"""
27+
28+
# All hardware managers have a name and a version.
29+
# Version should be bumped anytime a change is introduced. This will
30+
# signal to Ironic that if automatic node cleaning is in progress to
31+
# restart it from the beginning, to ensure consistency. The value can
32+
# be anything; it's checked for equality against previously seen
33+
# name:manager pairs.
34+
HARDWARE_MANAGER_NAME = 'ExampleBusinessLogicHardwareManager'
35+
HARDWARE_MANAGER_VERSION = '1'
36+
37+
def evaluate_hardware_support(self):
38+
"""Declare level of hardware support provided.
39+
40+
Since this example is explicitly about enforcing business logic during
41+
cleaning, we want to return a static value.
42+
43+
:returns: HardwareSupport level for this manager.
44+
"""
45+
return hardware.HardwareSupport.SERVICE_PROVIDER
46+
47+
def get_clean_steps(self, node, ports):
48+
"""Get a list of clean steps with priority.
49+
50+
Define any clean steps added by this manager here. These will be mixed
51+
with other loaded managers that support this hardware, and ordered by
52+
priority. Higher priority steps run earlier.
53+
54+
Note that out-of-band clean steps may also be provided by Ironic.
55+
These will follow the same priority ordering even though they are not
56+
executed by IPA.
57+
58+
There is *no guarantee whatsoever* that steps defined here will be
59+
executed by this HardwareManager. When it comes time to run these
60+
steps, they'll be called using dispatch_to_managers() just like any
61+
other IPA HardwareManager method. This means if they are unique to
62+
your hardware, they should be uniquely named. For example,
63+
upgrade_firmware would be a bad step name. Whereas
64+
upgrade_foobar_device_firmware would be better.
65+
66+
:param node: The node object as provided by Ironic.
67+
:param ports: Port objects as provided by Ironic.
68+
:returns: A list of cleaning steps, as a list of dicts.
69+
"""
70+
# While obviously you could actively run code here, generally this
71+
# should just return a static value, as any initialization and
72+
# detection should've been done in evaluate_hardware_support().
73+
return [{
74+
'step': 'companyx_verify_device_lifecycle',
75+
'priority': 472,
76+
# If you need Ironic to coordinate a reboot after this step
77+
# runs, but before continuing cleaning, this should be true.
78+
'reboot_requested': False,
79+
# If it's safe for Ironic to abort cleaning while this step
80+
# runs, this should be true.
81+
'abortable': True
82+
}]
83+
84+
# Other examples of interesting cleaning steps for this kind of hardware
85+
# manager would include verifying node.properties matches current state of
86+
# the node, checking smart stats to ensure the disk is not soon to fail,
87+
# or enforcing security policies.
88+
def companyx_verify_device_lifecycle(self, node, ports):
89+
"""Verify node is not beyond useful life of 3 years."""
90+
create_date = node.get('created_at')
91+
if create_date is not None:
92+
server_age = time.time() - time.mktime(time.strptime(create_date))
93+
if server_age > (60 * 60 * 24 * 365 * 3):
94+
raise errors.CleaningError(
95+
'Server is too old to pass cleaning!')
96+
else:
97+
LOG.info('Node is %s seconds old, younger than 3 years, '
98+
'cleaning passes.', server_age)

examples/business-logic/setup.cfg

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[metadata]
2+
name = example-business-logic
3+
author = Jay Faulkner
4+
author-email = jay@jvf.cc
5+
summary = IPA Example Hardware Managers: Business Logic
6+
license = Apache-2
7+
classifier =
8+
Intended Audience :: Developers
9+
Operating System :: OS Independent
10+
License :: OSI Approved :: Apache Software License
11+
Programming Language :: Python :: 3
12+
13+
[files]
14+
modules =
15+
example_business_logic
16+
17+
[entry_points]
18+
ironic_python_agent.hardware_managers =
19+
example_business_logic = example_business_logic:ExampleBusinessLogicHardwareManager

examples/business-logic/setup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env python
2+
import setuptools
3+
4+
setuptools.setup(
5+
setup_requires=['pbr'],
6+
pbr=True)
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Copyright 2015 Rackspace, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from oslo_log import log
16+
17+
from ironic_python_agent import hardware
18+
19+
LOG = log.getLogger()
20+
21+
22+
# All the helper methods should be kept outside of the HardwareManager
23+
# so they'll never get accidentally called by dispatch_to_managers()
24+
def _initialize_hardware():
25+
"""Example method for initalizing hardware."""
26+
# Perform any operations here that are required to initialize your
27+
# hardware.
28+
LOG.debug('Loading drivers, settling udevs, and generally initalizing')
29+
pass
30+
31+
32+
def _detect_hardware():
33+
"""Example method for hardware detection."""
34+
# For this example, return true if hardware is detected, false if not
35+
LOG.debug('Looking for example device')
36+
return True
37+
38+
39+
def _is_latest_firmware():
40+
"""Detect if device is running latest firmware."""
41+
# Actually detect the firmware version instead of returning here.
42+
return True
43+
44+
45+
def _upgrade_firmware():
46+
"""Upgrade firmware on device."""
47+
# Actually perform firmware upgrade instead of returning here.
48+
return True
49+
50+
51+
class ExampleDeviceHardwareManager(hardware.HardwareManager):
52+
"""Example hardware manager to support a single device"""
53+
54+
# All hardware managers have a name and a version.
55+
# Version should be bumped anytime a change is introduced. This will
56+
# signal to Ironic that if automatic node cleaning is in progress to
57+
# restart it from the beginning, to ensure consistency. The value can
58+
# be anything; it's checked for equality against previously seen
59+
# name:manager pairs.
60+
HARDWARE_MANAGER_NAME = 'ExampleDeviceHardwareManager'
61+
HARDWARE_MANAGER_VERSION = '1'
62+
63+
def evaluate_hardware_support(self):
64+
"""Declare level of hardware support provided.
65+
66+
Since this example covers a case of supporting a specific device,
67+
this method is where you would do anything needed to initalize that
68+
device, including loading drivers, and then detect if one exists.
69+
70+
In some cases, if you expect the hardware to be available on any node
71+
running this hardware manager, or it's undetectable, you may want to
72+
return a static value here.
73+
74+
Be aware all managers' loaded in IPA will run this method before IPA
75+
performs a lookup or begins heartbeating, so the time needed to
76+
execute this method will make cleaning and deploying slower.
77+
78+
:returns: HardwareSupport level for this manager.
79+
"""
80+
_initialize_hardware()
81+
if _detect_hardware():
82+
# This actually resolves down to an int. Upstream IPA will never
83+
# return a value higher than 2 (HardwareSupport.MAINLINE). This
84+
# means your managers should always be SERVICE_PROVIDER or higher.
85+
LOG.debug('Found example device, returning SERVICE_PROVIDER')
86+
return hardware.HardwareSupport.SERVICE_PROVIDER
87+
else:
88+
# If the hardware isn't supported, return HardwareSupport.NONE (0)
89+
# in order to prevent IPA from loading its clean steps or
90+
# attempting to use any methods inside it.
91+
LOG.debug('No example devices found, returning NONE')
92+
return hardware.HardwareSupport.NONE
93+
94+
def get_clean_steps(self, node, ports):
95+
"""Get a list of clean steps with priority.
96+
97+
Define any clean steps added by this manager here. These will be mixed
98+
with other loaded managers that support this hardware, and ordered by
99+
priority. Higher priority steps run earlier.
100+
101+
Note that out-of-band clean steps may also be provided by Ironic.
102+
These will follow the same priority ordering even though they are not
103+
executed by IPA.
104+
105+
There is *no guarantee whatsoever* that steps defined here will be
106+
executed by this HardwareManager. When it comes time to run these
107+
steps, they'll be called using dispatch_to_managers() just like any
108+
other IPA HardwareManager method. This means if they are unique to
109+
your hardware, they should be uniquely named. For example,
110+
upgrade_firmware would be a bad step name. Whereas
111+
upgrade_foobar_device_firmware would be better.
112+
113+
:param node: The node object as provided by Ironic.
114+
:param ports: Port objects as provided by Ironic.
115+
:returns: A list of cleaning steps, as a list of dicts.
116+
"""
117+
# While obviously you could actively run code here, generally this
118+
# should just return a static value, as any initialization and
119+
# detection should've been done in evaluate_hardware_support().
120+
return [{
121+
'step': 'upgrade_example_device_model1234_firmware',
122+
'priority': 37,
123+
# If you need Ironic to coordinate a reboot after this step
124+
# runs, but before continuing cleaning, this should be true.
125+
'reboot_requested': True,
126+
# If it's safe for Ironic to abort cleaning while this step
127+
# runs, this should be true.
128+
'abortable': False
129+
}]
130+
131+
def upgrade_example_device_model1234_firmware(self, node, ports):
132+
"""Upgrade firmware on Example Device Model #1234."""
133+
# Any commands needed to perform the firmware upgrade should go here.
134+
# If you plan on actually flashing firmware every cleaning cycle, you
135+
# should ensure your device will not experience flash exhaustion. A
136+
# good practice in some environments would be to check the firmware
137+
# version against a constant in the code, and noop the method if an
138+
# upgrade is not needed.
139+
if _is_latest_firmware():
140+
LOG.debug('Latest firmware already flashed, skipping')
141+
# Return values are ignored here on success
142+
return True
143+
else:
144+
LOG.debug('Firmware version X found, upgrading to Y')
145+
# Perform firmware upgrade.
146+
try:
147+
_upgrade_firmware()
148+
except Exception as e:
149+
# Log and pass through the exception so cleaning will fail
150+
LOG.exception(e)
151+
raise
152+
return True

examples/vendor-device/setup.cfg

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[metadata]
2+
name = example-vendor-device
3+
author = Jay Faulkner
4+
author-email = jay@jvf.cc
5+
summary = IPA Example Hardware Managers: Vendor Device
6+
license = Apache-2
7+
classifier =
8+
Intended Audience :: Developers
9+
Operating System :: OS Independent
10+
License :: OSI Approved :: Apache Software License
11+
Programming Language :: Python :: 3
12+
Development Status :: 4 - Beta
13+
14+
[files]
15+
modules =
16+
example_device
17+
18+
[entry_points]
19+
ironic_python_agent.hardware_managers =
20+
example_device = example_device:ExampleDeviceHardwareManager

examples/vendor-device/setup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env python
2+
import setuptools
3+
4+
setuptools.setup(
5+
setup_requires=['pbr'],
6+
pbr=True)

tox.ini

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ commands = stestr run {posargs}
3535
[testenv:pep8]
3636
whitelist_externals = bash
3737
commands =
38-
flake8 {posargs:ironic_python_agent imagebuild}
38+
flake8 {posargs:ironic_python_agent examples}
3939
# Run bashate during pep8 runs to ensure violations are caught by
4040
# the check and gate queues.
4141
{toxinidir}/tools/run_bashate.sh {toxinidir}
42-
doc8 doc/source README.rst
42+
doc8 doc/source README.rst examples/README.rst
4343

4444
[testenv:cover]
4545
setenv = VIRTUAL_ENV={envdir}
@@ -123,3 +123,8 @@ deps =
123123
deps = -r{toxinidir}/test-requirements.txt
124124
commands = bandit -r ironic_python_agent -x tests -n5 -ll -c tools/bandit.yml
125125

126+
[testenv:examples]
127+
commands =
128+
pip install -e {toxinidir}/examples/business-logic
129+
pip install -e {toxinidir}/examples/vendor-device
130+
python -c 'import example_business_logic; import example_device'

0 commit comments

Comments
 (0)