diff --git a/sdk/mounts/system.go b/sdk/mounts/system.go new file mode 100644 index 0000000..cd506ab --- /dev/null +++ b/sdk/mounts/system.go @@ -0,0 +1,34 @@ +package mounts + +import ( + "fmt" + + "github.com/kairos-io/kairos/pkg/machine" + "github.com/kairos-io/kairos/sdk/state" +) + +func PrepareWrite(partition state.PartitionState, mountpath string) error { + if partition.Mounted && partition.IsReadOnly { + if mountpath == partition.MountPoint { + return machine.Remount("rw", partition.MountPoint) + } + err := machine.Remount("rw", partition.MountPoint) + if err != nil { + return err + } + return machine.Mount(partition.Label, mountpath) + } + + return machine.Mount(partition.Label, mountpath) +} + +func Mount(partition state.PartitionState, mountpath string) error { + return machine.Mount(partition.Label, mountpath) +} + +func Umount(partition state.PartitionState) error { + if !partition.Mounted { + return fmt.Errorf("partition not mounted") + } + return machine.Umount(partition.MountPoint) +} diff --git a/sdk/state/machine.go b/sdk/state/machine.go new file mode 100644 index 0000000..cfbda51 --- /dev/null +++ b/sdk/state/machine.go @@ -0,0 +1,11 @@ +package state + +type Machine struct { + UUID string + BootArgs []string + CloudConfig string +} + +type Spec struct { + MachineSpec Machine +} diff --git a/sdk/state/state.go b/sdk/state/state.go new file mode 100644 index 0000000..829b7b5 --- /dev/null +++ b/sdk/state/state.go @@ -0,0 +1,142 @@ +package state + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "strings" + + "github.com/itchyny/gojq" + "github.com/jaypipes/ghw" + "github.com/jaypipes/ghw/pkg/block" + "github.com/kairos-io/kairos/pkg/machine" + "gopkg.in/yaml.v3" +) + +const ( + Active Boot = "active_boot" + Passive Boot = "passive_boot" + Recovery Boot = "recovery_boot" + LiveCD Boot = "livecd_boot" + Unknown Boot = "unknown" +) + +type Boot string + +type PartitionState struct { + Mounted bool `yaml:"mounted" json:"mounted"` + Name string `yaml:"name" json:"name"` + Label string `yaml:"label" json:"label"` + 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"` + UUID string `yaml:"uuid" json:"uuid"` // This would be volume UUID on macOS, PartUUID on linux, empty on Windows +} + +type Runtime struct { + UUID string `yaml:"uuid" json:"uuid"` + Persistent PartitionState `yaml:"persistent" json:"persistent"` + OEM PartitionState `yaml:"oem" json:"oem"` + State PartitionState `yaml:"state" json:"state"` + BootState Boot `yaml:"boot" json:"boot"` +} + +func detectPartition(b *block.Partition) PartitionState { + return PartitionState{ + Type: b.Type, + IsReadOnly: b.IsReadOnly, + UUID: b.UUID, + Name: fmt.Sprintf("/dev/%s", b.Name), + SizeBytes: b.SizeBytes, + Label: b.Label, + MountPoint: b.MountPoint, + Mounted: b.MountPoint != "", + } +} + +func detectBoot() Boot { + cmdline, err := ioutil.ReadFile("/proc/cmdline") + if err != nil { + return Unknown + } + cmdlineS := string(cmdline) + switch { + case strings.Contains(cmdlineS, "COS_ACTIVE"): + return Active + case strings.Contains(cmdlineS, "COS_PASSIVE"): + return Passive + case strings.Contains(cmdlineS, "COS_RECOVERY"), strings.Contains(cmdlineS, "COS_SYSTEM"): + return Recovery + case strings.Contains(cmdlineS, "live:LABEL"), strings.Contains(cmdlineS, "live:CDLABEL"): + return LiveCD + default: + return Unknown + } +} + +func detectRuntimeState(r *Runtime) error { + blockDevices, err := block.New(ghw.WithDisableTools(), ghw.WithDisableWarnings()) + if err != nil { + return err + } + for _, d := range blockDevices.Disks { + for _, part := range d.Partitions { + switch part.Label { + case "COS_PERSISTENT": + r.Persistent = detectPartition(part) + case "COS_OEM": + r.OEM = detectPartition(part) + case "COS_STATE": + r.State = detectPartition(part) + } + } + } + return nil +} + +func NewRuntime() (Runtime, error) { + runtime := &Runtime{ + BootState: detectBoot(), + UUID: machine.UUID(), + } + err := detectRuntimeState(runtime) + return *runtime, err +} + +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) { + 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 +} diff --git a/sdk/system/grub.go b/sdk/system/grub.go new file mode 100644 index 0000000..fabbcf2 --- /dev/null +++ b/sdk/system/grub.go @@ -0,0 +1,47 @@ +package system + +import ( + "fmt" + + "github.com/kairos-io/kairos/pkg/machine" + "github.com/kairos-io/kairos/pkg/utils" + "github.com/kairos-io/kairos/sdk/mounts" + "github.com/kairos-io/kairos/sdk/state" +) + +func SetGRUBOptions(opts map[string]string) Option { + return func(c *Changeset) error { + if len(opts) > 0 { + c.Add(func() error { return setGRUBOptions(opts) }) + } + return nil + } +} + +func setGRUBOptions(opts map[string]string) error { + runtime, err := state.NewRuntime() + if err != nil { + return err + } + + oem := runtime.OEM + if runtime.OEM.Name == "" { + oem = runtime.Persistent + } + + if err := mounts.PrepareWrite(oem, "/tmp/oem"); err != nil { + return err + } + defer func() { + machine.Umount("/tmp/oem") //nolint:errcheck + }() + + for k, v := range opts { + out, err := utils.SH(fmt.Sprintf(`grub2-editenv /tmp/oem/grubenv set "%s=%s"`, k, v)) + if err != nil { + fmt.Printf("could not set boot option: %s\n", out+err.Error()) + } + } + + return nil +} diff --git a/sdk/system/options.go b/sdk/system/options.go new file mode 100644 index 0000000..94b55c0 --- /dev/null +++ b/sdk/system/options.go @@ -0,0 +1,30 @@ +package system + +import ( + "github.com/hashicorp/go-multierror" +) + +type Changeset []func() error + +func (c *Changeset) Add(f func() error) { + *c = append(*c, f) +} + +type Option func(c *Changeset) error + +func Apply(opts ...Option) error { + + c := &Changeset{} + for _, o := range opts { + if err := o(c); err != nil { + return err + } + } + + var err error + for _, f := range *c { + err = multierror.Append(f()) + } + + return err +}