c/agent: include EFI support in hardware report

Adds firmware EFI support status to the hardware report.
It is represented as an enum which is populated based on two different
data sources. The ENABLED state is set if Linux has EFI runtime
services available. The SUPPORTED/UNSUPPORTED states depend on the
SMBIOS tables provided by the firmware
and are used if EFI runtime services are not available.
If neither are available, UNKNOWN is used.

Change-Id: I7642ccda14d5494294a7463755de18e73a8a9c53
Reviewed-on: https://review.monogon.dev/c/monogon/+/1571
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/agent/api/hwreport.proto b/cloud/agent/api/hwreport.proto
index 129e984..7c5d4fd 100644
--- a/cloud/agent/api/hwreport.proto
+++ b/cloud/agent/api/hwreport.proto
@@ -95,6 +95,27 @@
   string model = 11;
 }
 
+enum EFISupport {
+  // EFI support was not evaluated by the report generator.
+  EFI_INVALID = 0;
+  // It is not known if EFI is supported by the node. EFI runtime services are
+  // not available. This occurs if the report generator generally supports
+  // reporting EFI support, but none of its mechanisms to determine EFI support
+  // returned any data.
+  EFI_UNKNOWN = 1;
+  // The firmware indicates that EFI is not supported by the node. EFI runtime
+  // services are not available.
+  // Note that the firmware indication can be wrong.
+  EFI_UNSUPPORTED = 2;
+  // The firmware indicates that EFI is supported, but EFI runtime services
+  // are not available. This usually means that the hardware report was
+  // generated from a kernel booted in Compatibility Support Mode (CSM).
+  // Note that the firmware indication can be wrong.
+  EFI_SUPPORTED = 3;
+  // EFI and its runtime services are available and working.
+  EFI_ENABLED = 4;
+}
+
 message Node {
   // Manufacturer of the system, taken from DMI.
   string manufacturer = 1;
@@ -103,6 +124,9 @@
   // Serial number of the system, taken from DMI.
   string serial_number = 3;
 
+  // Information about EFI support in the node firmware.
+  EFISupport efi_support = 13;
+
   // Amount of physical memory installed, in bytes. Determined using DMI (if
   // available and not marked unusable) or memory blocks in sysfs
   // (/sys/devices/system/memory/...). This is not taken from meminfo as that
diff --git a/cloud/agent/hwreport.go b/cloud/agent/hwreport.go
index 3b82d27..c92244d 100644
--- a/cloud/agent/hwreport.go
+++ b/cloud/agent/hwreport.go
@@ -45,6 +45,14 @@
 		c.node.Product = smbTbl.SystemInformationRaw.ProductName
 		c.node.SerialNumber = smbTbl.SystemInformationRaw.SerialNumber
 	}
+	if smbTbl.BIOSInformationRaw != nil && smbTbl.BIOSInformationRaw.StructureVersion.AtLeast(2, 2) {
+		uefiSupport := smbTbl.BIOSInformationRaw.BIOSCharacteristicsExtensionByte2&smbios.UEFISpecificationSupported != 0
+		if uefiSupport {
+			c.node.EfiSupport = api.EFISupport_EFI_SUPPORTED
+		} else {
+			c.node.EfiSupport = api.EFISupport_EFI_UNSUPPORTED
+		}
+	}
 	for _, d := range smbTbl.MemoryDevicesRaw {
 		if d.StructureVersion.AtLeast(3, 2) && d.MemoryTechnology != 0x03 {
 			// If MemoryTechnology is available, only count DRAM
@@ -400,6 +408,7 @@
 	hwReportCtx := hwReportContext{
 		node: &api.Node{},
 	}
+	hwReportCtx.node.EfiSupport = api.EFISupport_EFI_UNKNOWN
 
 	hwReportCtx.gatherCPU()
 	hwReportCtx.gatherSMBIOS()
@@ -415,5 +424,9 @@
 	hwReportCtx.gatherNICs()
 	hwReportCtx.gatherBlockDevices()
 
+	if _, err := os.Stat("/sys/firmware/efi/runtime"); err == nil {
+		hwReportCtx.node.EfiSupport = api.EFISupport_EFI_ENABLED
+	}
+
 	return hwReportCtx.node, hwReportCtx.errors
 }
diff --git a/metropolis/pkg/smbios/smbios.go b/metropolis/pkg/smbios/smbios.go
index 1c8ae69..0080278 100644
--- a/metropolis/pkg/smbios/smbios.go
+++ b/metropolis/pkg/smbios/smbios.go
@@ -160,7 +160,10 @@
 
 // AtLeast returns true if the version in v is at least the given version.
 func (v *Version) AtLeast(major, minor uint8) bool {
-	return v.Major >= major && v.Minor >= minor
+	if v.Major > major {
+		return true
+	}
+	return v.Major == major && v.Minor >= minor
 }
 
 // UnmarshalStructureRaw unmarshals a SMBIOS structure into a Go struct which
diff --git a/metropolis/pkg/smbios/structures.go b/metropolis/pkg/smbios/structures.go
index e4bc4d2..a6bdb86 100644
--- a/metropolis/pkg/smbios/structures.go
+++ b/metropolis/pkg/smbios/structures.go
@@ -12,6 +12,9 @@
 	structTypeMemoryDevice         = 17
 )
 
+// Table 7.1.2.2 Bit 3
+const UEFISpecificationSupported = 1 << 3
+
 // BIOSInformationRaw contains decoded data from the BIOS Information structure
 // (SMBIOS Type 0). See Table 6 in the specification for detailed documentation
 // about the individual fields. Note that structure versions 2.1 and 2.2 are