commit 3a16aa0a2a2e9e9a19e7c1115a2cabe7ff5353d0
Author: Benjamin Wiegand <126627496+Benjamin-Wiegand@users.noreply.github.com>
Date:   Sun May 19 04:08:43 2024 -0700

    initial commit

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f5eff27
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# Fast ILO Exporter
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..dd15303
--- /dev/null
+++ b/main.py
@@ -0,0 +1,206 @@
+from prometheus_client import start_http_server, Gauge, Counter
+from prometheus_client.core import REGISTRY, GaugeMetricFamily
+from prometheus_client.registry import Collector
+
+from pysnmp.entity.engine import SnmpEngine
+from pysnmp.hlapi import CommunityData, UdpTransportTarget, ContextData
+
+from snmp import SnmpConfiguration, snmp_get
+import scrape
+
+from snmp_groups import BulkValues, BulkDummyValue
+from targets.temp import *
+from targets.fan import *
+from targets.cpu import *
+from targets.drive import *
+from targets.memory import *
+import targets.power
+
+import argparse
+import traceback
+
+NAMESPACE = 'ilo'
+
+arg_parser = argparse.ArgumentParser(
+    'ilo_exporter',
+    description='A fast(er) prometheus exporter for applicable HP servers using SNMP via the ILO controller.',
+)
+
+arg_parser.add_argument('-i', '--ilo-address', help='ILO IP address to scan.', required=True)
+arg_parser.add_argument('-a', '--server-address', default='0.0.0.0', help='Address to bind for hosting the metrics endpoint.')
+arg_parser.add_argument('-p', '--server-port', default=6969, help='Port to bind for the metrics endpoint.')
+arg_parser.add_argument('-c', '--snmp-community', default='public', help='SNMP community to read.')
+arg_parser.add_argument('--snmp-port', default=161, help='SNMP port to use.')
+arg_parser.add_argument('-o', '--scan-once', action='store_true', help='Only scan for SNMP variables on init, instead of on each collection (except hard drives, see --scan-drives-once). This is a small optimizaion that can be used if your sever configuration never changes.')
+arg_parser.add_argument('--scan-drives-once', action='store_true', help='When combined with --scan-once, this also prevents hard drives from being rescanned on collection. This is not recommeded.')
+arg_parser.add_argument('-v', '--verbose', action='store_true', help='Increases verbosity.')
+arg_parser.add_argument('-q', '--quiet', action='store_true', help='Tells the exporter to stfu under normal operation unless there is an error/warning.')
+
+args = arg_parser.parse_args()
+if args.quiet and args.verbose:
+    print('stop it. (--quiet and --verbose do not mix)')
+    exit(1)
+
+SCAN_FAIL_COUNTER = Counter('exporter', 'Number of times scanning the iLO for SNMP variables has failed.', namespace=NAMESPACE, subsystem='snmp_scan_failures')
+
+
+def noisy(*a, **kwa):
+    if not args.quiet:
+        print(*a, **kwa)
+
+
+def verbose(*a, **kwa):
+    if args.verbose:
+        print(*a, **kwa)
+
+
+class BulkCollector(Collector):
+    def __init__(self, snmp_config: SnmpConfiguration, index_oid_template: str, target_name: str, scan_on_collect: bool, *metrics_groups: tuple[str, BulkValues, list[BulkEnums]], scan_method: any = scrape.detect_things):
+        self._snmp_config = snmp_config
+        self._metrics_groups = metrics_groups
+        self._target_name = target_name
+        self._name_template = '%s_%s_' % (NAMESPACE, target_name) + '%s'
+        self._ids = []
+        self._index_oid_template = index_oid_template
+        self._scan_on_collect = scan_on_collect
+        self._scan_method = scan_method
+
+        if not scan_on_collect:
+            self.scan()
+
+    def scan(self):
+        verbose('scanning target', self._target_name)
+        self._ids = self._scan_method(self._snmp_config, self._index_oid_template)
+        noisy('found', len(self._ids), 'items for target', self._target_name)
+
+    def collect(self):
+        cache = {}
+
+        if self._scan_on_collect:
+            try:
+                self.scan()
+            except Exception as e:
+                traceback.print_exception(e)
+                print('Failed to scan SNMP, aborting collection')
+                SCAN_FAIL_COUNTER.inc()
+                return
+
+        for documentation, bulk_values, bulk_labels in self._metrics_groups:
+            metric_name = self._name_template % bulk_values.name
+            verbose('collecting', metric_name)
+
+            label_names = ['id']
+            label_maps = []
+
+            for label in bulk_labels:
+                # the labels are cached since they may be reused
+                if label.name not in cache:
+                    cache[label.name] = label.get_values(self._snmp_config, self._ids)
+                label_names.append(label.name)
+                label_maps.append(cache[label.name])
+
+            metric = GaugeMetricFamily(
+                metric_name,
+                documentation,
+                labels=label_names
+            )
+
+            # values are not reused
+            value_map = bulk_values.get_values(self._snmp_config, self._ids)
+
+            # do some fuckery (bad design, I know.)
+            for i in self._ids:
+                labels = [str(i)]  # id is first
+                for label_map in label_maps:
+                    label_value = label_map[i]
+                    labels.append(str(label_value))
+
+                value = value_map[i]
+                metric.add_metric(labels, value)
+
+            yield metric
+
+
+def get_power_draw() -> float:
+    verbose('collecting ilo_server_power_draw')
+    val = snmp_get(config, targets.power.POWER_METER_READING)
+    return val
+
+
+if __name__ == '__main__':
+
+    config = SnmpConfiguration(
+        SnmpEngine(),
+        CommunityData(args.snmp_community),
+        UdpTransportTarget((args.ilo_address[0], args.snmp_port)),
+        ContextData(),
+    )
+
+    power = Gauge("ilo_server_power_draw", "Power draw of the server in watts")
+    power.set_function(get_power_draw)
+
+    no_value = BulkDummyValue('info')
+
+    REGISTRY.register(BulkCollector(
+        config,
+        TEMP_INDEX,
+        'temperature',
+        not args.scan_once,
+        ('Temperatures readings of each temperature sensor in celsius', TEMP_CELSIUS, [TEMP_SENSOR_LOCALE, TEMP_CONDITION]),
+        ('Temperature thresholds for each temperature sensor in celsius', TEMP_THRESHOLD, [TEMP_SENSOR_LOCALE, TEMP_THRESHOLD_TYPE]),
+    ))
+
+    REGISTRY.register(BulkCollector(
+        config,
+        FAN_INDEX,
+        'fan',
+        not args.scan_once,
+        ('Information about system fans', no_value, [FAN_LOCALE, FAN_CONDITION, FAN_SPEED, FAN_PRESENT, FAN_PRESENCE_TEST]),
+    ))
+
+    REGISTRY.register(BulkCollector(
+        config,
+        CPU_INDEX,
+        'cpu',
+        not args.scan_once,
+        ('Information about CPUs', no_value, [CPU_NAME, CPU_STATUS, CPU_POWER_STATUS]),
+        ('Speed of CPUs in megahertz', CPU_SPEED, [CPU_NAME]),
+        ('CPU step', CPU_STEP, [CPU_NAME]),     # I dunno
+        ('Number of enabled cores', CORES_ENABLED, [CPU_NAME]),
+        ('Number of available threads', THREADS_AVAILABLE, [CPU_NAME]),
+    ))
+
+    # logical drives are for v2 if it ever exists (I don't use logical drives, sorry)
+
+    REGISTRY.register(BulkCollector(
+        config,
+        DRIVE_INDEX,
+        'drive',
+        not args.scan_drives_once,
+        ('Information about installed drives', no_value, [DRIVE_BOX, DRIVE_BAY, DRIVE_VENDOR, DRIVE_LOCATION, DRIVE_SERIAL, DRIVE_LINK_RATE, DRIVE_STATUS, DRIVE_CONDITION]),
+        ('Sizes of installed drives in megabytes', DRIVE_SIZE, [DRIVE_BOX, DRIVE_BAY, DRIVE_VENDOR, DRIVE_LOCATION, DRIVE_SERIAL]),
+        ('Temperatures of installed drives in celsius', DRIVE_TEMP, [DRIVE_BOX, DRIVE_BAY, DRIVE_VENDOR, DRIVE_LOCATION, DRIVE_SERIAL]),
+        ('Temperature thresholds of installed drives in celsius', DRIVE_TEMP_THRESHOLD, [DRIVE_BOX, DRIVE_BAY, DRIVE_VENDOR, DRIVE_LOCATION, DRIVE_SERIAL]),
+        ('Maximum temperatures of installed drives in celsius', DRIVE_TEMP_MAX, [DRIVE_BOX, DRIVE_BAY, DRIVE_VENDOR, DRIVE_LOCATION, DRIVE_SERIAL]),
+        ('Reference time of installed drives in hours', DRIVE_REFERENCE_TIME, [DRIVE_BOX, DRIVE_BAY, DRIVE_VENDOR, DRIVE_LOCATION, DRIVE_SERIAL]),
+        scan_method=scrape.detect_complex,
+    ))
+
+    REGISTRY.register(BulkCollector(
+        config,
+        MEMORY_INDEX,
+        'memory',
+        not args.scan_once,
+        ('Information about system memory', no_value, [MEMORY_LOCATION, MEMORY_MANUFACTURER, MEMORY_PART_NUMBER, MEMORY_STATUS, MEMORY_CONDITION]),
+        ('Sizes of system memory modules in kilobytes', MEMORY_SIZE, [MEMORY_LOCATION]),
+    ))
+
+    # start metrics endpoint
+    addr = args.server_address
+    port = args.server_port
+    print('starting metrics server on http://%s:%s' % (addr, port))
+    server, thread = start_http_server(port, addr)
+    print('ready!')
+
+    thread.join()
+    print('thread died!')
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..b3a7d35
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+prometheus_client~=0.20.0
+pysnmp~=4.4.12
\ No newline at end of file
diff --git a/scrape.py b/scrape.py
new file mode 100644
index 0000000..58fe029
--- /dev/null
+++ b/scrape.py
@@ -0,0 +1,80 @@
+from snmp import snmp_get_all, snmp_walk, SnmpConfiguration, SnmpEngine, CommunityData, UdpTransportTarget, ContextData
+
+
+def detect_things(c: SnmpConfiguration, base_oid: str) -> list[int]:
+    """ Scans for things and returns a list of their ids. """
+    things = []
+    for _, index in snmp_walk(c, base_oid):
+        assert isinstance(index, int)
+        assert index not in things
+        things.append(index)
+    return things
+
+
+# because of the way drive indexing works, this is the simplest way I can think to do it without over-complicating
+# everything else
+def detect_complex(c: SnmpConfiguration, base_oid: str) -> list[tuple[int]]:
+    """ Scans for things and returns a list of their oid indexes. """
+    drives = []
+    for oid, _ in snmp_walk(c, base_oid):
+        index = oid[len(base_oid) + 1:]
+        index = (*[int(i) for i in index.split('.')],)
+        assert index not in drives
+        drives.append(index)
+    return drives
+
+
+if __name__ == '__main__':
+    from targets.fan import FAN_VALUES, FAN_INDEX
+    from targets.temp import TEMP_VALUES, TEMP_INDEX
+    from targets.cpu import CPU_VALUES, CPU_INDEX
+    from targets.memory import MEMORY_VALUES, MEMORY_INDEX
+    from targets.drive import DRIVE_INDEX
+    from targets.logical_drive import LOGICAL_DRIVES_INDEX
+
+    config = SnmpConfiguration(
+        SnmpEngine(),
+        CommunityData('deeznuts'),
+        UdpTransportTarget(('192.168.100.88', 161)),
+        ContextData(),
+    )
+
+    print('scanning hardware...')
+    fans = detect_things(config, FAN_INDEX)
+    temp_sensors = detect_things(config, TEMP_INDEX)
+    cpus = detect_things(config, CPU_INDEX)
+    logical_drives = detect_things(config, LOGICAL_DRIVES_INDEX)
+    drives = detect_complex(config, DRIVE_INDEX)
+    memory_slots = detect_things(config, MEMORY_INDEX)
+
+    print('\'puter has', len(fans), 'fans')
+    print('\'puter has', len(temp_sensors), 'temp sensors')
+    print('\'puter has', len(cpus), 'processors')
+    print('\'puter has', len(logical_drives), 'logical drives')
+    print('\'puter has', len(drives), 'physical drives')
+    print('\'puter has', len(memory_slots), 'memory slots')
+
+    for ilo_enum in FAN_VALUES:
+        states = ilo_enum.get_values(config, fans)
+        for fan in fans:
+            print('fan', fan, ilo_enum.name, 'is', states[fan])
+        print()
+
+    for value in TEMP_VALUES:
+        states = value.get_values(config, temp_sensors)
+        for sensor in temp_sensors:
+            print('temperature', sensor, value.name, 'is', states[sensor])
+        print()
+
+    for value in CPU_VALUES:
+        states = value.get_values(config, cpus)
+        for cpu in cpus:
+            print('cpu', cpu, value.name, 'is', states[cpu])
+        print()
+
+    for value in MEMORY_VALUES:
+        states = value.get_values(config, memory_slots)
+        for slot in memory_slots:
+            print('memory slot', slot, value.name, 'is', states[slot])
+        print()
+
diff --git a/snmp.py b/snmp.py
new file mode 100644
index 0000000..41bb6e0
--- /dev/null
+++ b/snmp.py
@@ -0,0 +1,97 @@
+# just a highly simplified wrapper over pysnmp
+
+from pysnmp.hlapi import NoSuchInstance, Integer, Integer32, Counter32, OctetString, ObjectType, ObjectIdentity, getCmd, nextCmd, SnmpEngine, CommunityData, UdpTransportTarget, ContextData
+
+# for bulk requests. I find large requests crash the ilo (lol)
+MAX_CHUNK = 64
+
+
+class SnmpConfiguration(object):
+    def __init__(self, engine: SnmpEngine, auth: CommunityData, transport: UdpTransportTarget, context: ContextData):
+        self.engine = engine
+        self.auth = auth
+        self.transport = transport
+        self.context = context
+
+
+class AgentError(Exception):
+    pass
+
+
+class EngineError(Exception):
+    pass
+
+
+def process_value(var_bind) -> str | int | float | None:
+    val = var_bind[1]
+    if isinstance(val, NoSuchInstance):
+        return None
+    elif isinstance(val, Integer) or isinstance(val, Integer32) or isinstance(val, Counter32):
+        return int(val)
+    elif isinstance(val, OctetString):
+        return str(val)
+    else:
+        print('i dunno:', val)
+        print('unhandled type:', type(val))
+        return val.prettyPrint()
+
+
+def snmp_get(c: SnmpConfiguration, oid: str | tuple[int]) -> str | int | float | None:
+    """ gets a single oid """
+    return snmp_get_all(c, oid)[0]
+
+
+def snmp_get_all(c: SnmpConfiguration, *oid: str | tuple[int]) -> list[str | int | float | None]:
+    """ does a bulk request """
+    if len(oid) > MAX_CHUNK:
+        # split it up to not break the target
+        results = []
+        results.extend(snmp_get_all(c, *oid[:MAX_CHUNK]))
+        results.extend(snmp_get_all(c, *oid[MAX_CHUNK:]))
+        return results
+
+    # do snmp get
+    it = getCmd(c.engine, c.auth, c.transport, c.context, *[ObjectType(ObjectIdentity(x)) for x in oid])
+    engine_err, agent_err, agent_err_index, var_binds = next(it)
+
+    # handle errors
+    if engine_err:
+        raise EngineError(engine_err)
+    elif agent_err:
+        raise AgentError('%s at %s' % (agent_err.prettyPrint(), var_binds[int(agent_err_index) - 1] if agent_err_index else '?'))
+
+    # debugging
+    # for var_bind in var_binds:
+    #     print('got snmp:', ' = '.join([x.prettyPrint() for x in var_bind]))
+
+    return [process_value(vb) for vb in var_binds]
+
+
+def snmp_walk(c: SnmpConfiguration, base_oid: str) -> list[tuple[str, str | int | float | None]]:
+    """ does a walk within the range of a specified base oid """
+    results = []
+
+    # do snmp get
+    it = nextCmd(c.engine, c.auth, c.transport, c.context, ObjectType(ObjectIdentity(base_oid)))
+    within = True
+    while within:
+        engine_err, agent_err, agent_err_index, var_binds = next(it)
+
+        # handle errors
+        if engine_err:
+            raise EngineError(engine_err)
+        elif agent_err:
+            raise AgentError('%s at %s' % (agent_err.prettyPrint(), var_binds[int(agent_err_index) - 1] if agent_err_index else '?'))
+
+        for var_bind in var_binds:
+            # print(var_bind)
+            oid = str(var_bind[0].getOid())
+            if oid.startswith(base_oid):
+                results.append((oid, process_value(var_bind)))
+            else:
+                within = False
+
+        if len(var_binds) == 0:
+            within = False
+
+    return results
diff --git a/snmp_groups.py b/snmp_groups.py
new file mode 100644
index 0000000..161249a
--- /dev/null
+++ b/snmp_groups.py
@@ -0,0 +1,104 @@
+from snmp import SnmpConfiguration, snmp_get_all
+
+
+class EnumMapping(object):
+    def __init__(self, value: int, value_map: dict[int, str]):
+        self._value = value
+        self._value_map = value_map
+
+    def get_value(self) -> int:
+        return self._value
+
+    def get_name(self) -> str | None:
+        if self._value not in self._value_map.keys():
+            return None
+        return self._value_map[self._value]
+
+    def __str__(self) -> str:
+        name = self.get_name()
+        if name is None:
+            return 'unknown state %i' % self._value
+        return name
+
+
+class BulkValues(object):
+    def __init__(self, oid_template, name: str):
+        self._oid_template = oid_template
+        self._name = name
+
+    @property
+    def name(self):
+        return self._name
+
+    def get_values(self, c: SnmpConfiguration, indexes: list) -> dict:
+        oids = [self._oid_template(index) for index in indexes]
+        results = snmp_get_all(c, *oids)
+        result_dict = {}
+        for index in indexes:
+            result_dict[index] = results.pop(0)
+
+        return result_dict
+
+
+class BulkDummyValue(BulkValues):
+    def __init__(self, name: str):
+        super().__init__(None, name)
+        self._name = name
+
+    @property
+    def name(self):
+        return self._name
+
+    def get_values(self, _: SnmpConfiguration, indexes: list) -> dict:
+        result_dict = {}
+        for index in indexes:
+            result_dict[index] = 1
+
+        return result_dict
+
+
+class BulkNumbers(BulkValues):
+    def __init__(self, oid_template, name: str):
+        super().__init__(oid_template, name)
+
+    def get_values(self, c: SnmpConfiguration, indexes: list) -> dict:
+        result_dict = super().get_values(c, indexes)
+        for key in result_dict.keys():
+            if not isinstance(result_dict[key], int):
+                result_dict[key] = -1
+                print('unknown value (not an int):', result_dict[key])
+        return result_dict
+
+
+class BulkEnums(BulkNumbers):
+    def __init__(self, oid_template, name: str, value_map: dict):
+        super().__init__(oid_template, name)
+        self._value_map = value_map
+
+    @property
+    def state_map(self):
+        return self._value_map
+
+    def get_values(self, c: SnmpConfiguration, indexes: list) -> dict:
+        result_dict = super().get_values(c, indexes)
+        for key in result_dict.keys():
+            value = result_dict[key]
+            result_dict[key] = EnumMapping(value, self._value_map)
+            if __debug__ and value not in self._value_map:
+                print('unexpected enum value from ilo for %s: %i' % (self.name, value))
+        return result_dict
+
+
+class BulkStrings(BulkValues):
+    def __init__(self, oid_template, name: str):
+        super().__init__(oid_template, name)
+
+    def get_values(self, c: SnmpConfiguration, indexes: list) -> dict:
+        result_dict = super().get_values(c, indexes)
+        for key in result_dict.keys():
+            if not isinstance(result_dict[key], str):
+                result_dict[key] = 'unknown value: %s' % str(result_dict[key])
+                print('unknown value (not a string):', result_dict[key])
+            else:
+                result_dict[key] = result_dict[key].strip()
+        return result_dict
diff --git a/targets/__init__.py b/targets/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/targets/cpu.py b/targets/cpu.py
new file mode 100644
index 0000000..a0fb6c0
--- /dev/null
+++ b/targets/cpu.py
@@ -0,0 +1,62 @@
+from snmp_groups import BulkEnums, BulkNumbers, BulkStrings
+
+CPU_INDEX = '1.3.6.1.4.1.232.1.2.2.1.1.1'
+
+CPU_NAME = BulkStrings(
+    (lambda i: '1.3.6.1.4.1.232.1.2.2.1.1.3.%i' % i),
+    'name',
+)
+
+CPU_SPEED = BulkNumbers(
+    (lambda i: '1.3.6.1.4.1.232.1.2.2.1.1.4.%i' % i),
+    'speed',
+)
+
+CPU_STEP = BulkNumbers(
+    (lambda i: '1.3.6.1.4.1.232.1.2.2.1.1.5.%i' % i),
+    'step',
+)
+
+CPU_STATUS = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.1.2.2.1.1.6.%i' % i),
+    'status',
+    {
+        1: 'unknown',
+        2: 'ok',
+        3: 'degraded',
+        4: 'failed',
+        5: 'disabled',
+    }
+)
+
+CORES_ENABLED = BulkNumbers(
+    (lambda i: '1.3.6.1.4.1.232.1.2.2.1.1.15.%i' % i),
+    'cores_enabled',
+)
+
+THREADS_AVAILABLE = BulkNumbers(
+    (lambda i: '1.3.6.1.4.1.232.1.2.2.1.1.25.%i' % i),
+    'threads_available',
+)
+
+CPU_POWER_STATUS = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.1.2.2.1.1.26.%i' % i),
+    'power_status',
+    {
+        1: 'unknown',
+        2: 'Low Powered',
+        3: 'Normal Powered',
+        4: 'High Powered',
+    }
+)
+
+# for debugging
+CPU_VALUES = [
+    CPU_NAME,
+    CPU_SPEED,
+    CPU_STEP,
+    CPU_STATUS,
+    CORES_ENABLED,
+    THREADS_AVAILABLE,
+    CPU_POWER_STATUS,
+]
diff --git a/targets/drive.py b/targets/drive.py
new file mode 100644
index 0000000..8f7c21a
--- /dev/null
+++ b/targets/drive.py
@@ -0,0 +1,100 @@
+from snmp_groups import BulkEnums, BulkNumbers, BulkStrings
+
+DRIVE_INDEX = '1.3.6.1.4.1.232.3.2.5.1.1.2'
+
+# controller? idk
+# if anyone can show me a situation where this and drive bay are not related I'll uncomment this
+# DRIVE_CONTROLLER = BulkNumbers(
+#     (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 1) + i),
+#     'controller'
+# )
+
+DRIVE_BOX = BulkNumbers(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 63) + i),
+    'box'
+)
+
+DRIVE_BAY = BulkNumbers(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 5) + i),
+    'bay'
+)
+
+DRIVE_VENDOR = BulkStrings(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 3) + i),
+    'vendor',
+)
+
+# this may be slightly redundant
+DRIVE_LOCATION = BulkStrings(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 64) + i),
+    'location',
+)
+
+DRIVE_SERIAL = BulkStrings(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 51) + i),
+    'serial',
+)
+
+DRIVE_SIZE = BulkNumbers(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 45) + i),
+    'size',
+)
+
+DRIVE_LINK_RATE = BulkEnums(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 65) + i),
+    'link_rate',
+    {
+        1: 'other',
+        2: '1.5Gbps',
+        3: '3.0Gbps',
+        4: '6.0Gbps',
+        5: '12.0Gbps',
+    }
+)
+
+DRIVE_TEMP = BulkNumbers(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 70) + i),
+    'temperature'
+)
+
+DRIVE_TEMP_THRESHOLD = BulkNumbers(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 71) + i),
+    'temperature_threshold'
+)
+
+DRIVE_TEMP_MAX = BulkNumbers(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 72) + i),
+    'temperature_maximum'
+)
+
+DRIVE_STATUS = BulkEnums(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 6) + i),
+    'status',
+    {
+        1: 'Other',
+        2: 'Ok',
+        3: 'Failed',
+        4: 'Predictive Failure',
+        5: 'Erasing',
+        6: 'Erase Done',
+        7: 'Erase Queued',
+        8: 'SSD Wear Out',
+        9: 'Not Authenticated',
+    }
+)
+
+DRIVE_CONDITION = BulkEnums(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 37) + i),
+    'condition',
+    {
+        1: 'other',
+        2: 'ok',
+        3: 'degraded',
+        4: 'failed',
+    }
+)
+
+DRIVE_REFERENCE_TIME = BulkNumbers(
+    (lambda i: (1, 3, 6, 1, 4, 1, 232, 3, 2, 5, 1, 1, 9) + i),
+    'reference_time'
+)
diff --git a/targets/fan.py b/targets/fan.py
new file mode 100644
index 0000000..fd5aaaa
--- /dev/null
+++ b/targets/fan.py
@@ -0,0 +1,78 @@
+from snmp_groups import BulkEnums
+
+FAN_INDEX = '1.3.6.1.4.1.232.6.2.6.7.1.2.0'
+
+FAN_LOCALE = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.7.1.3.0.%i' % i),
+    'locale',
+    {
+        1: 'other',
+        2: 'unknown',
+        3: 'system',
+        4: 'systemBoard',
+        5: 'ioBoard',
+        6: 'cpu',
+        7: 'memory',
+        8: 'storage',
+        9: 'removable media',
+        10: 'power supply',
+        11: 'ambent',
+        12: 'chassis',
+        13: 'bridge card',
+        14: 'management board',
+        15: 'backplane',
+        16: 'network slot',
+        17: 'blade slot',
+        18: 'virtual',
+    }
+)
+
+FAN_PRESENT = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.7.1.4.0.%i' % i),
+    'presence',
+    {
+        1: 'other',
+        2: 'absent',
+        3: 'present',
+    }
+)
+
+FAN_PRESENCE_TEST = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.7.1.5.0.%i' % i),
+    'presence_test',
+    {
+        1: 'other',
+        2: 'tachOutput',
+        3: 'spinDetect',
+    }
+)
+
+FAN_SPEED = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.7.1.6.0.%i' % i),
+    'speed',
+    {
+        1: 'other',
+        2: 'normal',
+        3: 'high',
+    }
+)
+
+FAN_CONDITION = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.7.1.6.0.%i' % i),
+    'condition',
+    {
+        1: 'other',
+        2: 'normal',
+        3: 'degraded',
+        4: 'failed',
+    }
+)
+
+# for debugging
+FAN_VALUES = [
+    FAN_LOCALE,
+    FAN_PRESENT,
+    FAN_PRESENCE_TEST,
+    FAN_SPEED,
+    FAN_CONDITION,
+]
diff --git a/targets/logical_drive.py b/targets/logical_drive.py
new file mode 100644
index 0000000..34bb520
--- /dev/null
+++ b/targets/logical_drive.py
@@ -0,0 +1,3 @@
+# I do not use HP's raid utility, so I cannot test this
+
+LOGICAL_DRIVES_INDEX = '1.3.6.1.4.1.232.3.2.3.1.1.2.0'
diff --git a/targets/memory.py b/targets/memory.py
new file mode 100644
index 0000000..e2688a2
--- /dev/null
+++ b/targets/memory.py
@@ -0,0 +1,77 @@
+from snmp_groups import BulkEnums, BulkNumbers, BulkStrings
+
+MEMORY_INDEX = '1.3.6.1.4.1.232.6.2.14.13.1.1'
+
+MEMORY_LOCATION = BulkStrings(
+    (lambda i: '1.3.6.1.4.1.232.6.2.14.13.1.13.%i' % i),
+    'location',
+)
+
+MEMORY_MANUFACTURER = BulkStrings(
+    (lambda i: '1.3.6.1.4.1.232.6.2.14.13.1.9.%i' % i),
+    'manufacturer',
+)
+
+MEMORY_PART_NUMBER = BulkStrings(
+    (lambda i: '1.3.6.1.4.1.232.6.2.14.13.1.10.%i' % i),
+    'part_number',
+)
+
+MEMORY_SIZE = BulkNumbers(
+    (lambda i: '1.3.6.1.4.1.232.6.2.14.13.1.6.%i' % i),
+    'size',
+)
+
+# this is an enum, but I don't know the mappings
+# I also don't have HP smart ram for testing
+# MEMORY_TECHNOLOGY = BulkNumbers(
+#     (lambda i: '1.3.6.1.4.1.232.6.2.14.13.1.8.%i' % i),
+#     'technology',
+# )
+
+# this is another enum, but I don't know the mappings
+# MEMORY_TYPE = BulkNumbers(
+#     (lambda i: '1.3.6.1.4.1.232.6.2.14.13.1.7.%i' % i),
+#     'type',
+# )
+
+MEMORY_STATUS = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.14.13.1.19.%i' % i),
+    'status',
+    {
+        1: 'other',
+        2: 'notPresent',
+        3: 'present',
+        4: 'good',
+        5: 'add',
+        6: 'upgrade',
+        7: 'missing',
+        8: 'doesNotMatch',
+        9: 'notSupported',
+        10: 'badConfig',
+        11: 'degraded',
+        12: 'spare',
+        13: 'partial',
+    }
+)
+
+MEMORY_CONDITION = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.14.13.1.20.%i' % i),
+    'condition',
+    {
+        1: 'other',
+        2: 'ok',
+        3: 'degraded',
+        4: 'degradedModuleIndexUnknown',
+    }
+)
+
+# for debugging
+MEMORY_VALUES = [
+    MEMORY_LOCATION,
+    MEMORY_MANUFACTURER,
+    MEMORY_PART_NUMBER,
+    MEMORY_SIZE,
+    MEMORY_STATUS,
+    MEMORY_CONDITION
+]
diff --git a/targets/power.py b/targets/power.py
new file mode 100644
index 0000000..c3ee80e
--- /dev/null
+++ b/targets/power.py
@@ -0,0 +1,7 @@
+
+POWER_METER_READING = '1.3.6.1.4.1.232.6.2.15.3.0'
+
+# I have no idea what these values mean (or map to). any help would be appreciated
+# POWER_METER_SUPPORT = '1.3.6.1.4.1.232.6.2.15.1'
+# POWER_METER_STATUS = '1.3.6.1.4.1.232.6.2.15.2'
+# POWER_METER_PREVIOUS_READING = '1.3.6.1.4.1.232.6.2.15.4'
diff --git a/targets/temp.py b/targets/temp.py
new file mode 100644
index 0000000..1358e16
--- /dev/null
+++ b/targets/temp.py
@@ -0,0 +1,64 @@
+from snmp_groups import BulkEnums, BulkNumbers
+
+TEMP_INDEX = '1.3.6.1.4.1.232.6.2.6.8.1.2.0'
+
+TEMP_CELSIUS = BulkNumbers(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.8.1.4.0.%i' % i),
+    'celsius',
+)
+
+TEMP_THRESHOLD = BulkNumbers(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.8.1.5.0.%i' % i),
+    'threshold',
+)
+
+TEMP_SENSOR_LOCALE = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.8.1.3.0.%i' % i),
+    'sensor_locale',
+    {
+        1: 'other',
+        2: 'unknown',
+        3: 'system',
+        4: 'systemBoard',
+        5: 'ioBoard',
+        6: 'cpu',
+        7: 'memory',
+        8: 'storage',
+        9: 'removable media',
+        10: 'power supply',
+        11: 'ambent',
+        12: 'chassis',
+        13: 'bridge card',
+    }
+)
+
+TEMP_THRESHOLD_TYPE = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.8.1.7.0.%i' % i),
+    'threshold_type',
+    {
+        1: 'other',
+        5: 'blowout',
+        9: 'caution',
+        15: 'critical',
+        16: 'noreaction',
+    }
+)
+
+TEMP_CONDITION = BulkEnums(
+    (lambda i: '1.3.6.1.4.1.232.6.2.6.8.1.6.0.%i' % i),
+    'condition',
+    {
+        1: 'other',
+        2: 'normal',
+        3: 'high',
+    }
+)
+
+# for debugging
+TEMP_VALUES = [
+    TEMP_SENSOR_LOCALE,
+    TEMP_THRESHOLD_TYPE,
+    TEMP_CONDITION,
+    TEMP_CELSIUS,
+    TEMP_THRESHOLD,
+]