diff --git a/pkg/machine/bootcmdline.go b/pkg/machine/bootcmdline.go new file mode 100644 index 0000000..ede22c1 --- /dev/null +++ b/pkg/machine/bootcmdline.go @@ -0,0 +1,88 @@ +package machine + +import ( + "errors" + "fmt" + "io/ioutil" + "strings" + + "github.com/google/shlex" + "github.com/hashicorp/go-multierror" + "github.com/itchyny/gojq" + "gopkg.in/yaml.v2" +) + +func DotToYAML(file string) ([]byte, error) { + if file == "" { + file = "/proc/cmdline" + } + dat, err := ioutil.ReadFile(file) + if err != nil { + return []byte{}, err + } + + v := stringToMap(string(dat)) + + return dotToYAML(v) +} + +func stringToMap(s string) map[string]interface{} { + v := map[string]interface{}{} + + splitted, _ := shlex.Split(s) + for _, item := range splitted { + parts := strings.SplitN(item, "=", 2) + value := "true" + if len(parts) > 1 { + value = strings.Trim(parts[1], `"`) + } + key := strings.Trim(parts[0], `"`) + v[key] = value + } + + return v +} +func jq(command string, data map[string]interface{}) (map[string]interface{}, error) { + query, err := gojq.Parse(command) + if err != nil { + return nil, err + } + code, err := gojq.Compile(query) + if err != nil { + return nil, err + } + iter := code.Run(data) + + v, ok := iter.Next() + if !ok { + return nil, errors.New("failed getting rsult from gojq") + } + if err, ok := v.(error); ok { + return nil, err + } + if t, ok := v.(map[string]interface{}); ok { + return t, nil + } + + return make(map[string]interface{}), nil +} + +func dotToYAML(v map[string]interface{}) ([]byte, error) { + data := map[string]interface{}{} + var errs error + + for k, value := range v { + newData, err := jq(fmt.Sprintf(".%s=\"%s\"", k, value), data) + if err != nil { + errs = multierror.Append(errs, err) + continue + } + data = newData + } + + out, err := yaml.Marshal(&data) + if err != nil { + errs = multierror.Append(errs, err) + } + return out, errs +} diff --git a/pkg/machine/bootcmdline_test.go b/pkg/machine/bootcmdline_test.go new file mode 100644 index 0000000..9b28f0a --- /dev/null +++ b/pkg/machine/bootcmdline_test.go @@ -0,0 +1,29 @@ +package machine_test + +import ( + "io/ioutil" + "os" + + . "github.com/c3os-io/c3os/pkg/machine" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("BootCMDLine", func() { + Context("parses data", func() { + + It("returns cmdline if provided", func() { + f, err := ioutil.TempFile("", "test") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(f.Name()) + + err = ioutil.WriteFile(f.Name(), []byte(`config_url="foo bar" baz.bar=""`), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + b, err := DotToYAML(f.Name()) + Expect(err).ToNot(HaveOccurred()) + + Expect(string(b)).To(Equal("baz:\n bar: \"\"\nconfig_url: foo bar\n")) + }) + }) +}) diff --git a/pkg/machine/machine.go b/pkg/machine/machine.go new file mode 100644 index 0000000..e6aad1b --- /dev/null +++ b/pkg/machine/machine.go @@ -0,0 +1,157 @@ +package machine + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/c3os-io/c3os/pkg/machine/openrc" + "github.com/c3os-io/c3os/pkg/machine/systemd" + "github.com/denisbrodbeck/machineid" + + "github.com/c3os-io/c3os/pkg/utils" +) + +type Service interface { + WriteUnit() error + Start() error + OverrideCmd(string) error + Enable() error + Restart() error +} + +const ( + PassiveBoot = "passive" + ActiveBoot = "active" + RecoveryBoot = "recovery" + LiveCDBoot = "liveCD" + NetBoot = "netboot" + UnknownBoot = "unknown" +) + +// BootFrom returns the booting partition of the SUT. +func BootFrom() string { + out, err := utils.SH("cat /proc/cmdline") + if err != nil { + return UnknownBoot + } + switch { + case strings.Contains(out, "COS_ACTIVE"): + return ActiveBoot + case strings.Contains(out, "COS_PASSIVE"): + return PassiveBoot + case strings.Contains(out, "COS_RECOVERY"), strings.Contains(out, "COS_SYSTEM"): + return RecoveryBoot + case strings.Contains(out, "live:CDLABEL"): + return LiveCDBoot + case strings.Contains(out, "netboot"): + return NetBoot + default: + return UnknownBoot + } +} + +func EdgeVPN(instance, rootDir string) (Service, error) { + if utils.IsOpenRCBased() { + return openrc.NewService( + openrc.WithName("edgevpn"), + openrc.WithUnitContent(openrc.EdgevpnUnit), + openrc.WithRoot(rootDir), + ) + } + + return systemd.NewService( + systemd.WithName("edgevpn"), + systemd.WithInstance(instance), + systemd.WithUnitContent(systemd.EdgevpnUnit), + systemd.WithRoot(rootDir), + ) +} + +const EdgeVPNDefaultInstance string = "c3os" + +type fakegetty struct{} + +func (fakegetty) Restart() error { return nil } +func (fakegetty) Enable() error { return nil } +func (fakegetty) OverrideCmd(string) error { return nil } +func (fakegetty) SetEnvFile(string) error { return nil } +func (fakegetty) WriteUnit() error { return nil } +func (fakegetty) Start() error { + utils.SH("chvt 2") //nolint:errcheck + return nil +} + +func Getty(i int) (Service, error) { + if utils.IsOpenRCBased() { + return &fakegetty{}, nil + } + + return systemd.NewService( + systemd.WithName("getty"), + systemd.WithInstance(fmt.Sprintf("tty%d", i)), + ) +} + +func K3s() (Service, error) { + if utils.IsOpenRCBased() { + return openrc.NewService( + openrc.WithName("k3s"), + ) + } + + return systemd.NewService( + systemd.WithName("k3s"), + ) +} + +func K3sAgent() (Service, error) { + if utils.IsOpenRCBased() { + return openrc.NewService( + openrc.WithName("k3s-agent"), + ) + } + + return systemd.NewService( + systemd.WithName("k3s-agent"), + ) +} + +func K3sEnvUnit(unit string) string { + if utils.IsOpenRCBased() { + return fmt.Sprintf("/etc/rancher/k3s/%s.env", unit) + } + + return fmt.Sprintf("/etc/sysconfig/%s", unit) +} + +func UUID() string { + if os.Getenv("UUID") != "" { + return os.Getenv("UUID") + } + id, _ := machineid.ID() + hostname, _ := os.Hostname() + return fmt.Sprintf("%s-%s", id, hostname) +} + +func CreateSentinel(f string) error { + return ioutil.WriteFile(fmt.Sprintf("/usr/local/.c3os/sentinel_%s", f), []byte{}, os.ModePerm) +} + +func SentinelExist(f string) bool { + if _, err := os.Stat(fmt.Sprintf("/usr/local/.c3os/sentinel_%s", f)); err == nil { + return true + } + return false +} + +func ExecuteInlineCloudConfig(cloudConfig, stage string) error { + _, err := utils.ShellSTDIN(cloudConfig, fmt.Sprintf("elemental run-stage -s %s -", stage)) + return err +} + +func ExecuteCloudConfig(file, stage string) error { + _, err := utils.SH(fmt.Sprintf("elemental run-stage -s %s %s", stage, file)) + return err +} diff --git a/pkg/machine/machine_suite_test.go b/pkg/machine/machine_suite_test.go new file mode 100644 index 0000000..51bc130 --- /dev/null +++ b/pkg/machine/machine_suite_test.go @@ -0,0 +1,13 @@ +package machine_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestInstaller(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Machine Suite") +} diff --git a/pkg/machine/openrc/edgevpn.go b/pkg/machine/openrc/edgevpn.go new file mode 100644 index 0000000..246b525 --- /dev/null +++ b/pkg/machine/openrc/edgevpn.go @@ -0,0 +1,19 @@ +package openrc + +const EdgevpnUnit string = `#!/sbin/openrc-run + +depend() { + after net + provide edgevpn +} + +supervisor=supervise-daemon +name="edgevpn" +command="edgevpn" +supervise_daemon_args="--stdout /var/log/edgevpn.log --stderr /var/log/edgevpn.log" +pidfile="/run/edgevpn.pid" +respawn_delay=5 +set -o allexport +if [ -f /etc/environment ]; then source /etc/environment; fi +if [ -f /etc/systemd/system.conf.d/edgevpn-c3os.env ]; then source /etc/systemd/system.conf.d/edgevpn-c3os.env; fi +set +o allexport` diff --git a/pkg/machine/openrc/unit.go b/pkg/machine/openrc/unit.go new file mode 100644 index 0000000..8118608 --- /dev/null +++ b/pkg/machine/openrc/unit.go @@ -0,0 +1,98 @@ +package openrc + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + "github.com/c3os-io/c3os/pkg/utils" +) + +type ServiceUnit struct { + content string + name string + rootdir string +} + +type ServiceOpts func(*ServiceUnit) error + +func WithRoot(n string) ServiceOpts { + return func(su *ServiceUnit) error { + su.rootdir = n + return nil + } +} + +func WithName(n string) ServiceOpts { + return func(su *ServiceUnit) error { + su.name = n + return nil + } +} + +func WithUnitContent(n string) ServiceOpts { + return func(su *ServiceUnit) error { + su.content = n + return nil + } +} + +func NewService(opts ...ServiceOpts) (ServiceUnit, error) { + s := &ServiceUnit{} + for _, o := range opts { + if err := o(s); err != nil { + return *s, err + } + } + return *s, nil +} + +func (s ServiceUnit) WriteUnit() error { + uname := s.name + + if err := ioutil.WriteFile(filepath.Join(s.rootdir, fmt.Sprintf("/etc/init.d/%s", uname)), []byte(s.content), 0755); err != nil { + return err + } + + return nil +} + +// TODO: This is too much k3s specific. +func (s ServiceUnit) OverrideCmd(cmd string) error { + k3sbin := utils.K3sBin() + if k3sbin == "" { + return fmt.Errorf("no k3s binary found (?)") + } + cmd = strings.ReplaceAll(cmd, k3sbin+" ", "") + envFile := filepath.Join(s.rootdir, fmt.Sprintf("/etc/rancher/k3s/%s.env", s.name)) + env := make(map[string]string) + env["command_args"] = fmt.Sprintf("%s >>/var/log/%s.log 2>&1", cmd, s.name) + + return utils.WriteEnv(envFile, env) +} + +func (s ServiceUnit) Start() error { + out, err := utils.SH(fmt.Sprintf("/etc/init.d/%s start", s.name)) + if err != nil { + return fmt.Errorf("failed starting service: %s. %s (%w)", s.name, out, err) + } + return nil +} + +func (s ServiceUnit) Restart() error { + out, err := utils.SH(fmt.Sprintf("/etc/init.d/%s restart", s.name)) + if err != nil { + return fmt.Errorf("failed restarting service: %s. %s (%w)", s.name, out, err) + } + return nil +} + +func (s ServiceUnit) Enable() error { + _, err := utils.SH(fmt.Sprintf("ln -sf /etc/init.d/%s /etc/runlevels/default/%s", s.name, s.name)) + return err +} + +func (s ServiceUnit) StartBlocking() error { + return s.Start() +} diff --git a/pkg/machine/systemd/edgevpn.go b/pkg/machine/systemd/edgevpn.go new file mode 100644 index 0000000..068ffdf --- /dev/null +++ b/pkg/machine/systemd/edgevpn.go @@ -0,0 +1,12 @@ +package systemd + +const EdgevpnUnit string = `[Unit] +Description=EdgeVPN Daemon +After=network.target +[Service] +EnvironmentFile=/etc/systemd/system.conf.d/edgevpn-%i.env +LimitNOFILE=49152 +ExecStart=edgevpn +Restart=always +[Install] +WantedBy=multi-user.target` diff --git a/pkg/machine/systemd/unit.go b/pkg/machine/systemd/unit.go new file mode 100644 index 0000000..7ee1a4c --- /dev/null +++ b/pkg/machine/systemd/unit.go @@ -0,0 +1,115 @@ +package systemd + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/c3os-io/c3os/pkg/utils" +) + +type ServiceUnit struct { + content string + name, instance string + rootdir string +} + +const overrideCmdTemplate string = ` +[Service] +ExecStart= +ExecStart=%s +` + +type ServiceOpts func(*ServiceUnit) error + +func WithRoot(n string) ServiceOpts { + return func(su *ServiceUnit) error { + su.rootdir = n + return nil + } +} + +func WithName(n string) ServiceOpts { + return func(su *ServiceUnit) error { + su.name = n + return nil + } +} + +func WithInstance(n string) ServiceOpts { + return func(su *ServiceUnit) error { + su.instance = n + return nil + } +} + +func WithUnitContent(n string) ServiceOpts { + return func(su *ServiceUnit) error { + su.content = n + return nil + } +} + +func NewService(opts ...ServiceOpts) (ServiceUnit, error) { + s := &ServiceUnit{} + for _, o := range opts { + if err := o(s); err != nil { + return *s, err + } + } + return *s, nil +} + +func (s ServiceUnit) WriteUnit() error { + uname := s.name + if s.instance != "" { + uname = fmt.Sprintf("%s@", s.name) + } + + if err := ioutil.WriteFile(filepath.Join(s.rootdir, fmt.Sprintf("/etc/systemd/system/%s.service", uname)), []byte(s.content), 0600); err != nil { + return err + } + + _, err := utils.SH("systemctl daemon-reload") + return err +} + +func (s ServiceUnit) OverrideCmd(cmd string) error { + svcDir := filepath.Join(s.rootdir, fmt.Sprintf("/etc/systemd/system/%s.service.d/", s.name)) + os.MkdirAll(svcDir, 0600) //nolint:errcheck + + return ioutil.WriteFile(filepath.Join(svcDir, "override.conf"), []byte(fmt.Sprintf(overrideCmdTemplate, cmd)), 0600) +} + +func (s ServiceUnit) Start() error { + return s.systemctl("start", false) +} + +func (s ServiceUnit) Restart() error { + return s.systemctl("restart", false) +} + +func (s ServiceUnit) Enable() error { + return s.systemctl("enable", false) +} + +func (s ServiceUnit) StartBlocking() error { + return s.systemctl("start", true) +} + +func (s ServiceUnit) systemctl(action string, blocking bool) error { + uname := s.name + if s.instance != "" { + uname = fmt.Sprintf("%s@%s", s.name, s.instance) + } + args := []string{action} + if !blocking { + args = append(args, "--no-block") + } + args = append(args, uname) + + _, err := utils.SH(fmt.Sprintf("systemctl %s", strings.Join(args, " "))) + return err +}