mirror of
https://github.com/kairos-io/kairos-sdk.git
synced 2025-04-27 19:15:23 +00:00
* Bump go-uefi to include panic fix
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
* Revert "Disable certs list in state (#113)"
This reverts commit c643cb5e73
.
---------
Signed-off-by: Mauro Morales <mauro.morales@spectrocloud.com>
433 lines
14 KiB
Go
433 lines
14 KiB
Go
package state
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/foxboron/go-uefi/efi"
|
|
"github.com/itchyny/gojq"
|
|
"github.com/jaypipes/ghw"
|
|
"github.com/jaypipes/ghw/pkg/block"
|
|
"github.com/kairos-io/kairos-sdk/signatures"
|
|
"github.com/kairos-io/kairos-sdk/types"
|
|
"github.com/kairos-io/kairos-sdk/utils"
|
|
"github.com/rs/zerolog"
|
|
"github.com/zcalusic/sysinfo"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
Active Boot = "active_boot"
|
|
Passive Boot = "passive_boot"
|
|
Recovery Boot = "recovery_boot"
|
|
LiveCD Boot = "livecd_boot"
|
|
AutoReset Boot = "autoreset_boot"
|
|
Unknown Boot = "unknown"
|
|
|
|
UEFICurrentEntryFile = "/sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
|
|
)
|
|
|
|
var Log zerolog.Logger
|
|
|
|
type Boot string
|
|
|
|
type PartitionState struct {
|
|
Mounted bool `yaml:"mounted" json:"mounted"`
|
|
Name string `yaml:"name" json:"name"`
|
|
Label string `yaml:"label" json:"label"`
|
|
FilesystemLabel string `yaml:"filesystemlabel" json:"filesystemlabel"`
|
|
MountPoint string `yaml:"mount_point" json:"mount_point"`
|
|
SizeBytes uint64 `yaml:"size_bytes" json:"size_bytes"`
|
|
Type string `yaml:"type" json:"type"`
|
|
IsReadOnly bool `yaml:"read_only" json:"read_only"`
|
|
Found bool `yaml:"found" json:"found"`
|
|
UUID string `yaml:"uuid" json:"uuid"` // This would be volume UUID on macOS, PartUUID on linux, empty on Windows
|
|
}
|
|
|
|
type Kairos struct {
|
|
Flavor string `yaml:"flavor" json:"flavor"`
|
|
Version string `yaml:"version" json:"version"`
|
|
Init string `yaml:"init" json:"init"`
|
|
SecureBoot bool `yaml:"secureboot" json:"secureboot"`
|
|
EfiCerts types.EfiCerts `yaml:"eficerts,omitempty" json:"eficerts,omitempty"`
|
|
}
|
|
|
|
type EncryptedParts struct {
|
|
ByLabel map[string]PartitionState `yaml:"by_label,omitempty" json:"by_label,omitempty"`
|
|
ByDevice map[string]PartitionState `yaml:"by_device,omitempty" json:"by_device,omitempty"`
|
|
}
|
|
|
|
type Runtime struct {
|
|
UUID string `yaml:"uuid" json:"uuid"`
|
|
Persistent PartitionState `yaml:"persistent" json:"persistent"`
|
|
Recovery PartitionState `yaml:"recovery" json:"recovery"`
|
|
OEM PartitionState `yaml:"oem" json:"oem"`
|
|
State PartitionState `yaml:"state" json:"state"`
|
|
EncryptedPartitions EncryptedParts `yaml:"encrypted_partitions,omitempty" json:"encrypted_partitions,omitempty"`
|
|
BootState Boot `yaml:"boot" json:"boot"`
|
|
System sysinfo.SysInfo `yaml:"system" json:"system"`
|
|
Kairos Kairos `yaml:"kairos" json:"kairos"`
|
|
}
|
|
|
|
type FndMnt struct {
|
|
Filesystems []struct {
|
|
Target string `json:"target,omitempty"`
|
|
FsOptions string `json:"fs-options,omitempty"`
|
|
} `json:"filesystems,omitempty"`
|
|
}
|
|
|
|
// Lsblk is the struct to marshal the output of lsblk
|
|
type Lsblk struct {
|
|
BlockDevices []struct {
|
|
Path string `json:"path,omitempty"`
|
|
Mountpoint string `json:"mountpoint,omitempty"`
|
|
FsType string `json:"fstype,omitempty"`
|
|
Size string `json:"size,omitempty"`
|
|
Label string `json:"label,omitempty"`
|
|
RO bool `json:"ro,omitempty"`
|
|
} `json:"blockdevices,omitempty"`
|
|
}
|
|
|
|
func detectPartitionByFindmnt(b *block.Partition) PartitionState {
|
|
// If mountpoint seems empty, try to get the mountpoint of the partition label also the RO status
|
|
// This is a current shortcoming of ghw which only identifies mountpoints via device, not by label/uuid/anything else
|
|
mountpoint := b.MountPoint
|
|
readOnly := b.IsReadOnly
|
|
if b.MountPoint == "" && b.FilesystemLabel != "" {
|
|
out, err := utils.SH(fmt.Sprintf("findmnt /dev/disk/by-label/%s -f -J -o TARGET,FS-OPTIONS", b.FilesystemLabel))
|
|
mnt := &FndMnt{}
|
|
if err == nil {
|
|
err = json.Unmarshal([]byte(out), mnt)
|
|
// This should not happen, if there were no targets, the command would have returned an error, but you never know...
|
|
if err == nil && len(mnt.Filesystems) == 1 {
|
|
mountpoint = mnt.Filesystems[0].Target
|
|
// Don't assume its ro or rw by default, check both. One should match
|
|
regexRW := regexp.MustCompile("^rw,|^rw$|,rw,|,rw$")
|
|
regexRO := regexp.MustCompile("^ro,|^ro$|,ro,|,ro$")
|
|
if regexRW.Match([]byte(mnt.Filesystems[0].FsOptions)) {
|
|
readOnly = false
|
|
}
|
|
if regexRO.Match([]byte(mnt.Filesystems[0].FsOptions)) {
|
|
readOnly = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return PartitionState{
|
|
Type: b.Type,
|
|
IsReadOnly: readOnly,
|
|
UUID: b.UUID,
|
|
Name: fmt.Sprintf("/dev/%s", b.Name),
|
|
SizeBytes: b.SizeBytes,
|
|
Label: b.Label,
|
|
FilesystemLabel: b.FilesystemLabel,
|
|
MountPoint: mountpoint,
|
|
Mounted: mountpoint != "",
|
|
Found: true,
|
|
}
|
|
}
|
|
|
|
func detectBoot(logger zerolog.Logger) Boot {
|
|
logger.Info().Msg("detecting boot state")
|
|
cmdline, err := os.ReadFile("/proc/cmdline")
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg("Error reading /proc/cmdline file " + err.Error())
|
|
return Unknown
|
|
}
|
|
|
|
cmdlineS := string(cmdline)
|
|
|
|
if DetectUKIboot(cmdlineS) {
|
|
logger.Debug().Msg("Detected uki boot")
|
|
return getUKIBootState(logger)
|
|
}
|
|
|
|
return getNonUKIBootState(cmdlineS)
|
|
}
|
|
|
|
func getUKIBootState(logger zerolog.Logger) Boot {
|
|
if !EfiBootFromInstall(logger) {
|
|
return LiveCD
|
|
}
|
|
|
|
currentEntryBytes, err := os.ReadFile(UEFICurrentEntryFile)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg(fmt.Sprintf("Error reading %s file %s", UEFICurrentEntryFile, err.Error()))
|
|
return Unknown
|
|
}
|
|
|
|
// Create a regular expression to remove non-printable characters
|
|
regex := regexp.MustCompile("[[:cntrl:]]")
|
|
currentEntry := regex.ReplaceAllString(string(currentEntryBytes), "")
|
|
|
|
logger.Debug().Msg("Current entry: " + currentEntry)
|
|
|
|
if !strings.HasSuffix(currentEntry, ".conf") {
|
|
return Unknown
|
|
}
|
|
|
|
switch {
|
|
case strings.HasPrefix(currentEntry, "active"):
|
|
return Active
|
|
case strings.HasPrefix(currentEntry, "passive"):
|
|
return Passive
|
|
case strings.HasPrefix(currentEntry, "recovery"):
|
|
return Recovery
|
|
case strings.HasPrefix(currentEntry, "statereset"):
|
|
return AutoReset
|
|
default:
|
|
return Unknown
|
|
}
|
|
}
|
|
|
|
func getNonUKIBootState(cmdline string) Boot {
|
|
switch {
|
|
case strings.Contains(cmdline, "COS_ACTIVE"):
|
|
return Active
|
|
case strings.Contains(cmdline, "COS_PASSIVE"):
|
|
return Passive
|
|
case strings.Contains(cmdline, "COS_RECOVERY"), strings.Contains(cmdline, "COS_SYSTEM"), strings.Contains(cmdline, "recovery-mode"):
|
|
return Recovery
|
|
case strings.Contains(cmdline, "live:LABEL"), strings.Contains(cmdline, "live:CDLABEL"), strings.Contains(cmdline, "netboot"):
|
|
return LiveCD
|
|
default:
|
|
return Unknown
|
|
}
|
|
}
|
|
|
|
// Detects if we are on uki mode
|
|
func DetectUKIboot(cmdline string) bool {
|
|
Log.Info().Msg("checking cmdline for uki:" + cmdline)
|
|
return strings.Contains(cmdline, "rd.immucore.uki")
|
|
}
|
|
|
|
// EfiBootFromInstall will try to check the /sys/firmware/efi/LoaderDevicePartUUID-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f
|
|
// systemd vendor Id is 4a67b082-0a4c-41cf-b6c7-440b29bb8c4f and will never change
|
|
// LoaderDevicePartUUID contains the partition UUID of the EFI System Partition the boot loader was run from. Set by the boot loader.
|
|
// This will return true if we are running from a DISK device, which sets the efivar
|
|
// This wil return false when running from a volatile media, like CD or netboot as it cannot infer where it was booted from
|
|
// Useful to check if we are on install phase or not
|
|
// This efi var is VOLATILE so once we reboot is GONE. No way of keeping it across reboots, its set by the bootloader.
|
|
func EfiBootFromInstall(logger zerolog.Logger) bool {
|
|
file := "/sys/firmware/efi/efivars/LoaderDevicePartUUID-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"
|
|
readFile, err := os.ReadFile(file)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg("Error reading LoaderDevicePartUUID file")
|
|
return false
|
|
}
|
|
if len(readFile) == 0 || string(readFile) == "" {
|
|
logger.Debug().Str("file", string(readFile)).Msg("Error reading LoaderDevicePartUUID file")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// DetectBootWithVFS will detect the boot state using a vfs so it can be used for tests as well
|
|
func DetectBootWithVFS(fs types.KairosFS) (Boot, error) {
|
|
cmdline, err := fs.ReadFile("/proc/cmdline")
|
|
if err != nil {
|
|
return Unknown, err
|
|
}
|
|
cmdlineS := string(cmdline)
|
|
switch {
|
|
case strings.Contains(cmdlineS, "COS_ACTIVE"):
|
|
return Active, nil
|
|
case strings.Contains(cmdlineS, "COS_PASSIVE"):
|
|
return Passive, nil
|
|
case strings.Contains(cmdlineS, "COS_RECOVERY"), strings.Contains(cmdlineS, "COS_SYSTEM"), strings.Contains(cmdlineS, "recovery-mode"):
|
|
return Recovery, nil
|
|
case strings.Contains(cmdlineS, "live:LABEL"), strings.Contains(cmdlineS, "live:CDLABEL"), strings.Contains(cmdlineS, "netboot"):
|
|
return LiveCD, nil
|
|
default:
|
|
return Unknown, nil
|
|
}
|
|
}
|
|
|
|
func detectRuntimeState(r *Runtime) error {
|
|
blockDevices, err := block.New(ghw.WithDisableTools(), ghw.WithDisableWarnings())
|
|
// ghw currently only detects if partitions are mounted via the device
|
|
// If we mount them via label, then its set as not mounted.
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, d := range blockDevices.Disks {
|
|
for _, part := range d.Partitions {
|
|
switch part.FilesystemLabel {
|
|
case "COS_PERSISTENT":
|
|
r.Persistent = detectPartitionByFindmnt(part)
|
|
case "COS_RECOVERY":
|
|
r.Recovery = detectPartitionByFindmnt(part)
|
|
case "COS_OEM":
|
|
r.OEM = detectPartitionByFindmnt(part)
|
|
case "COS_STATE":
|
|
r.State = detectPartitionByFindmnt(part)
|
|
}
|
|
}
|
|
}
|
|
if !r.Persistent.Found {
|
|
r.Persistent = detectPartitionByLabelLsblk("COS_PERSISTENT")
|
|
}
|
|
if !r.OEM.Found {
|
|
r.OEM = detectPartitionByLabelLsblk("COS_OEM")
|
|
}
|
|
if !r.Recovery.Found {
|
|
r.Recovery = detectPartitionByLabelLsblk("COS_RECOVERY")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// detectPartitionByLsblk will try to detect info about a partition by using lsblk and the given LABEL
|
|
// Useful for LVM partitions which ghw is unable to find
|
|
func detectPartitionByLabelLsblk(label string) PartitionState {
|
|
return detectPartitionByLsblk(fmt.Sprintf("/dev/disk/by-label/%s", label))
|
|
}
|
|
|
|
// detectPartitionByLsblk generic function to get info about a partition via any given path
|
|
// Could be /dev/disk/by-{label,path,uuid} for example or even a device directly like /dev/sda1
|
|
func detectPartitionByLsblk(path string) PartitionState {
|
|
out, err := utils.SH(fmt.Sprintf("lsblk %s -o PATH,FSTYPE,MOUNTPOINT,SIZE,RO,LABEL -J", path))
|
|
mnt := &Lsblk{}
|
|
part := PartitionState{}
|
|
if err == nil {
|
|
err = json.Unmarshal([]byte(out), mnt)
|
|
// This should not happen, if there were no targets, the command would have returned an error, but you never know...
|
|
if err == nil && len(mnt.BlockDevices) == 1 {
|
|
blk := mnt.BlockDevices[0]
|
|
part.Found = true
|
|
part.Name = blk.Path
|
|
part.Mounted = blk.Mountpoint != ""
|
|
part.MountPoint = blk.Mountpoint
|
|
part.Type = blk.FsType
|
|
part.FilesystemLabel = blk.Label
|
|
// this seems to report always false. We can try to use findmnt here to know if its ro/rw
|
|
part.IsReadOnly = blk.RO
|
|
}
|
|
}
|
|
|
|
return part
|
|
}
|
|
|
|
func detectSystem(r *Runtime) {
|
|
var si sysinfo.SysInfo
|
|
|
|
si.GetSysInfo()
|
|
r.System = si
|
|
}
|
|
|
|
func detectKairos(r *Runtime) {
|
|
k := &Kairos{}
|
|
k.Flavor = utils.Flavor()
|
|
|
|
v, err := utils.OSRelease("VERSION")
|
|
if err == nil {
|
|
k.Version = v
|
|
}
|
|
k.Init = utils.GetInit()
|
|
k.EfiCerts = getEfiCertsCommonNames()
|
|
k.SecureBoot = efi.GetSecureBoot()
|
|
r.Kairos = *k
|
|
|
|
}
|
|
|
|
func detectEncryptedPartitions(runtime *Runtime) {
|
|
results := EncryptedParts{
|
|
ByDevice: make(map[string]PartitionState),
|
|
ByLabel: make(map[string]PartitionState),
|
|
}
|
|
blockDevices, err := block.New(ghw.WithDisableTools(), ghw.WithDisableWarnings())
|
|
// ghw currently only detects if partitions are mounted via the device
|
|
// If we mount them via label, then its set as not mounted.
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, d := range blockDevices.Disks {
|
|
for _, part := range d.Partitions {
|
|
if part.Type == "crypto_LUKS" {
|
|
// detect partition by the mapper + part.name (i.e. vda2)
|
|
p := detectPartitionByLsblk(fmt.Sprintf("/dev/mapper/%s", part.Name))
|
|
if p.Found {
|
|
results.ByLabel[part.Label] = p
|
|
results.ByDevice[fmt.Sprintf("/dev/%s", part.Name)] = p
|
|
}
|
|
}
|
|
}
|
|
}
|
|
runtime.EncryptedPartitions = results
|
|
}
|
|
|
|
// getEfiCertsCommonNames returns a simple list of the Common names of the certs
|
|
func getEfiCertsCommonNames() types.EfiCerts {
|
|
var data types.EfiCerts
|
|
certs, _ := signatures.GetAllCerts() // Ignore errors here, we dont care about them, we only want the presentation of the names
|
|
for _, c := range certs.PK {
|
|
data.PK = append(data.PK, c.Issuer.CommonName)
|
|
}
|
|
for _, c := range certs.KEK {
|
|
data.KEK = append(data.KEK, c.Issuer.CommonName)
|
|
}
|
|
for _, c := range certs.DB {
|
|
data.DB = append(data.DB, c.Issuer.CommonName)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func NewRuntimeWithLogger(logger zerolog.Logger) (Runtime, error) {
|
|
logger.Info().Msg("creating a runtime")
|
|
runtime := &Runtime{
|
|
BootState: detectBoot(logger),
|
|
UUID: utils.UUID(),
|
|
}
|
|
|
|
detectSystem(runtime)
|
|
detectKairos(runtime)
|
|
detectEncryptedPartitions(runtime)
|
|
err := detectRuntimeState(runtime)
|
|
|
|
return *runtime, err
|
|
}
|
|
|
|
func NewRuntime() (Runtime, error) {
|
|
return NewRuntimeWithLogger(Log)
|
|
}
|
|
|
|
func (r Runtime) String() string {
|
|
dat, err := yaml.Marshal(r)
|
|
if err == nil {
|
|
return string(dat)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (r Runtime) Query(s string) (res string, err error) {
|
|
s = fmt.Sprintf(".%s", s)
|
|
jsondata := map[string]interface{}{}
|
|
var dat []byte
|
|
dat, err = json.Marshal(r)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = json.Unmarshal(dat, &jsondata)
|
|
if err != nil {
|
|
return
|
|
}
|
|
query, err := gojq.Parse(s)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
iter := query.Run(jsondata) // or query.RunWithContext
|
|
for {
|
|
v, ok := iter.Next()
|
|
if !ok {
|
|
break
|
|
}
|
|
if err, ok := v.(error); ok {
|
|
return res, err
|
|
}
|
|
res += fmt.Sprint(v)
|
|
}
|
|
return
|
|
}
|