hw: read static serial numbers from EEPROMs

On the most recent HW revisions of Clearfog (both the SoM and the
carrier board), there should be some EEPROMs that are factory-populated
with ONIE-compatible TLV data. The EEPROMs also happen to be these
semi-magic models which embed some unique data at the end of the memory
space (and within a HW-protected, read-only range).

On some of these boards, the EEPROMs are not populated at all (which is
expected on pre-1.3 "base" carrier and on some older SoM). The majority
of our HW actually has this old revision, so we cannot really hard-fail
with an error message. This is easily detectable by a missing "eeprom"
file in sysfs (but the dir entry for the I2C slave is always present, as
long as the Device Tree is not borked).

The other source of breakage is that these EEPROMs are documented to
contain some ONIE-compatible TLV data, but I have yet to see an EEPROM
with *any* data burned in; the ones which we have are usually empty.
However, in future we *will* probably get our hands on a ClearFog Base
or a SoM with these TLV data built in, and on these models, we *should*
show serial numbers from these EEPROMs which will, hopefully, match the
printed barcode that's surely found somewhere on the PCBs.

In the meanwhile, though, we can leverage that unique UID/EUI-48 code
that's available at the end of the EEPROM space, and in fact, there's no
harm in showing both on HW which has both the magic EEPROM and some
actual TLV data. So I felt that the cleanest solution for this is to put
a new node just for the EEPROM into ietf-hardware, and put the S/N from
the unique part of each EEPROM in there. This required some extra
handling of the `oper-state` leaf so as not to confuse the downstream
consumers on legacy boards with no populated EEPROM.

Compared to all that mess, the EEPROMs which we're putting on our PCBs
are also magic, but in a different way: these EEPROM chips occupy a pair
of I2C addresses, one for the usual EEPROM, and one for the magic one
which only holds the S/N. We *know* that this EEPROM is always present,
so we don't have to mess with nested ietf-hardware hardware instances,
and it's OK to simply read this S/N and propagate that as a piece of
static data to the YANG datastore. Except when working with the
hotpluggable fans, but man, this starts getting a wee bit involved,
isn't it?

Change-Id: I36f810e4ca57b28405926da20a3c50bd520c480f
diff --git a/tests/hardware_eeprom.cpp b/tests/hardware_eeprom.cpp
new file mode 100644
index 0000000..1732f2d
--- /dev/null
+++ b/tests/hardware_eeprom.cpp
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Jan Kundrát <jan.kundrat@cesnet.cz>
+ *
+*/
+
+#include "trompeloeil_doctest.h"
+#include <filesystem>
+#include "ietf-hardware/IETFHardware.h"
+#include "pretty_printers.h"
+#include "test_log_setup.h"
+#include "tests/configure.cmake.h"
+
+using namespace std::literals;
+
+TEST_CASE("EEPROM with UID/EID")
+{
+    using namespace velia::ietf_hardware;
+    using namespace data_reader;
+    TEST_INIT_LOGS;
+    const auto sysfs = CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/"s;
+
+    REQUIRE(*hexEEPROM(sysfs, 1, 0x5c, 16, 0, 16) == "1E70C61C941000628C2EA000A000000C");
+
+    auto working = EepromWithUid("x:eeprom", "x", sysfs, 0, 0x52, 256, 256 - 6, 6);
+    REQUIRE(working().data == DataTree{
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/class", "iana-hardware:module"},
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/parent", "x"},
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/serial-num", "294100B13DA3"},
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/state/oper-state", "enabled"},
+            });
+
+    auto missing = EepromWithUid("x:eeprom", "x", sysfs, 0, 0x53, 256, 256 - 6, 6);
+    REQUIRE(missing().data == DataTree{
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/class", "iana-hardware:module"},
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/parent", "x"},
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/state/oper-state", "disabled"},
+            });
+
+    auto corrupted = EepromWithUid("x:eeprom", "x", sysfs, 0, 0x53, 16, 2, 6);
+    REQUIRE(corrupted().data == DataTree{
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/class", "iana-hardware:module"},
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/parent", "x"},
+                {"/ietf-hardware:hardware/component[name='x:eeprom']/state/oper-state", "disabled"},
+            });
+
+    REQUIRE_THROWS_WITH(EepromWithUid("x:eeprom", "x", sysfs, 0, 0x20, 256, 256 - 6, 6),
+                        "EEPROM: no I2C device defined at bus 0 address 0x20");
+
+    REQUIRE_THROWS_WITH(hexEEPROM(sysfs, 0, 0, 10, 5, 6),
+                        "EEPROM: region out of range");
+}
diff --git a/tests/sysfs/bus/i2c/devices/0-0052/eeprom b/tests/sysfs/bus/i2c/devices/0-0052/eeprom
new file mode 100644
index 0000000..6b2e886
--- /dev/null
+++ b/tests/sysfs/bus/i2c/devices/0-0052/eeprom
Binary files differ
diff --git a/tests/sysfs/bus/i2c/devices/0-0053/waiting_for_supplier b/tests/sysfs/bus/i2c/devices/0-0053/waiting_for_supplier
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/sysfs/bus/i2c/devices/0-0053/waiting_for_supplier
diff --git a/tests/sysfs/bus/i2c/devices/1-005c/eeprom b/tests/sysfs/bus/i2c/devices/1-005c/eeprom
new file mode 100644
index 0000000..aecf2f5
--- /dev/null
+++ b/tests/sysfs/bus/i2c/devices/1-005c/eeprom
Binary files differ