Skip to content

Commit

Permalink
✨ state api sdk (kairos-io#262)
Browse files Browse the repository at this point in the history
* ✨ Add state api

This is related to kairos-io#34.

Starts to unify the API to retrieve the state in the sdk to have a common place to query system status information.

* 🤖 Add test

* Update go.mod

* ⚙️ Fine-tune detection of partitions

* 🤖 Add more fine-grained tests

* 🎨 Add /dev/ to partition name

* 🤖 Fixup tests

* ⚙️ Remount accessors

* ✨ Add state partition to cloud-init paths

* 📝 Upper case Kairos in motd

* 🎨 Add mounts sdk

* 🎨 Set grub options via SDK

* 🎨 Make it more idiomatic
  • Loading branch information
mudler authored Oct 23, 2022
1 parent 35fe06a commit 4666c17
Show file tree
Hide file tree
Showing 15 changed files with 369 additions and 22 deletions.
46 changes: 46 additions & 0 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
machine "github.com/kairos-io/kairos/pkg/machine"
"github.com/kairos-io/kairos/pkg/utils"
bundles "github.com/kairos-io/kairos/sdk/bundles"
"github.com/kairos-io/kairos/sdk/state"

"github.com/urfave/cli"
)
Expand Down Expand Up @@ -179,6 +180,51 @@ E.g. kairos-agent install-bundle container:quay.io/kairos/kairos...
return nil
},
},

{
Name: "state",
Usage: "get machine state",
Description: "Print machine state information, e.g. `state get .uuid` returns the machine uuid",
Aliases: []string{"s"},
Action: func(c *cli.Context) error {
runtime, err := state.NewRuntime()
if err != nil {
return err
}

fmt.Print(runtime)
return err
},
Subcommands: []cli.Command{
{
Name: "apply",
Usage: "Applies a machine state",
Description: "Set runtime machine configuration",
Aliases: []string{"a"},
Action: func(c *cli.Context) error {
// TODO
return nil
},
},
{
Name: "get",
Usage: "get specific ",
Description: "query state data",
Aliases: []string{"g"},
Action: func(c *cli.Context) error {
runtime, err := state.NewRuntime()
if err != nil {
return err
}

res, err := runtime.Query(c.Args().First())
fmt.Print(res)
return err
},
},
},
},

{
Name: "interactive-install",
Description: `
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/kairos-io/kairos

go 1.17
go 1.18

require (
github.com/avast/retry-go v3.0.0+incompatible
Expand Down
4 changes: 2 additions & 2 deletions internal/agent/hooks/bundles.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ func (b BundleOption) Run(c config.Config) error {

machine.Mount("COS_PERSISTENT", "/usr/local") //nolint:errcheck
defer func() {
machine.Umount("/usr/local")
machine.Umount("/usr/local") //nolint:errcheck
}()

machine.Mount("COS_OEM", "/oem") //nolint:errcheck
defer func() {
machine.Umount("/oem")
machine.Umount("/oem") //nolint:errcheck
}()

opts := c.Install.Bundles.Options()
Expand Down
18 changes: 4 additions & 14 deletions internal/agent/hooks/gruboptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,15 @@ import (
"fmt"

config "github.com/kairos-io/kairos/pkg/config"
"github.com/kairos-io/kairos/pkg/machine"
"github.com/kairos-io/kairos/pkg/utils"
"github.com/kairos-io/kairos/sdk/system"
)

type GrubOptions struct{}

func (b GrubOptions) Run(c config.Config) error {

machine.Mount("COS_OEM", "/tmp/oem") //nolint:errcheck
defer func() {
machine.Umount("/tmp/oem")
}()
for k, v := range c.Install.GrubOptions {
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 // do not error out
}
err := system.Apply(system.SetGRUBOptions(c.Install.GrubOptions))
if err != nil {
fmt.Println(err)
}

return nil
}
2 changes: 1 addition & 1 deletion internal/agent/hooks/kcrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (k Kcrypt) Run(c config.Config) error {

machine.Mount("COS_OEM", "/oem") //nolint:errcheck
defer func() {
machine.Umount("/oem")
machine.Umount("/oem") //nolint:errcheck
}()

_ = os.MkdirAll("/oem/system/discovery", 0650)
Expand Down
4 changes: 4 additions & 0 deletions overlay/files-opensuse-arm-rpi/etc/elemental/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ reset:
grub-entry-name: "kairos"
system:
size: 2000

cloud-init-paths:
- /run/initramfs/cos-state
# - /run/initramfs/live
6 changes: 5 additions & 1 deletion overlay/files/etc/elemental/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ upgrade:
reset:
grub-entry-name: "Kairos"
system:
size: 3000
size: 3000

cloud-init-paths:
- /run/initramfs/cos-state
# - /run/initramfs/live
2 changes: 1 addition & 1 deletion overlay/files/etc/motd
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
Welcome to kairos!
Welcome to Kairos!

Refer to https://kairos.io for documentation.
16 changes: 14 additions & 2 deletions pkg/machine/partitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,20 @@ import (
"github.com/kairos-io/kairos/pkg/utils"
)

func Umount(path string) {
utils.SH(fmt.Sprintf("umount %s", path)) //nolint:errcheck
func Umount(path string) error {
out, err := utils.SH(fmt.Sprintf("umount %s", path))
if err != nil {
return fmt.Errorf("failed umounting: %s: %w", out, err)
}
return nil
}

func Remount(opt, path string) error {
out, err := utils.SH(fmt.Sprintf("mount -o %s,remount %s", opt, path))
if err != nil {
return fmt.Errorf("failed umounting: %s: %w", out, err)
}
return nil
}

func Mount(label, mountpoint string) error {
Expand Down
34 changes: 34 additions & 0 deletions sdk/mounts/system.go
Original file line number Diff line number Diff line change
@@ -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)
}
11 changes: 11 additions & 0 deletions sdk/state/machine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package state

type Machine struct {
UUID string
BootArgs []string
CloudConfig string
}

type Spec struct {
MachineSpec Machine
}
142 changes: 142 additions & 0 deletions sdk/state/state.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 4666c17

Please sign in to comment.