diff --git a/internal/bus/bus.go b/internal/bus/bus.go new file mode 100644 index 0000000..8264146 --- /dev/null +++ b/internal/bus/bus.go @@ -0,0 +1,66 @@ +package bus + +import ( + "fmt" + "os" + "os/exec" + + "github.com/c3os-io/c3os/pkg/bus" + "github.com/mudler/go-pluggable" +) + +// Manager is the bus instance manager, which subscribes plugins to events emitted +var Manager *Bus = &Bus{ + Manager: pluggable.NewManager( + []pluggable.EventType{ + bus.EventBootstrap, + bus.EventChallenge, + bus.EventInstall, + }, + ), +} + +type Bus struct { + *pluggable.Manager +} + +func (b *Bus) Initialize() { + b.Manager.Autoload("agent-provider", "/system/providers").Register() + + for i := range b.Manager.Events { + e := b.Manager.Events[i] + b.Manager.Response(e, func(p *pluggable.Plugin, r *pluggable.EventResponse) { + if os.Getenv("BUS_DEBUG") == "true" { + fmt.Println( + fmt.Sprintf("[provider event: %s]", e), + "received from", + p.Name, + "at", + p.Executable, + r, + ) + } + if r.Errored() { + err := fmt.Sprintf("Provider %s at %s had an error: %s", p.Name, p.Executable, r.Error) + fmt.Println(err) + os.Exit(1) + } else { + if r.State != "" { + fmt.Println(fmt.Sprintf("[provider event: %s]", e), r.State) + } + } + }) + } +} + +func RunHookScript(s string) error { + _, err := os.Stat(s) + if err != nil { + return nil + } + cmd := exec.Command(s) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return cmd.Run() +} diff --git a/internal/c3os/branding.go b/internal/c3os/branding.go new file mode 100644 index 0000000..f34cd8a --- /dev/null +++ b/internal/c3os/branding.go @@ -0,0 +1,7 @@ +package c3os + +import "path" + +func BrandingFile(s string) string { + return path.Join("/etc", "c3os", "branding", s) +} diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go new file mode 100644 index 0000000..cd52b95 --- /dev/null +++ b/internal/cmd/commands.go @@ -0,0 +1,141 @@ +package cmd + +import ( + //"fmt" + + "encoding/base64" + "fmt" + "strconv" + "strings" + + config "github.com/c3os-io/c3os/pkg/config" + edgeVPNClient "github.com/mudler/edgevpn/api/client" + "github.com/mudler/edgevpn/api/client/service" + "github.com/mudler/edgevpn/pkg/node" + "github.com/urfave/cli" + "gopkg.in/yaml.v2" +) + +func CommonCommand(cmds ...cli.Command) []cli.Command { + return append(commonCommands, cmds...) +} + +var commonCommands = []cli.Command{ + { + Name: "get-kubeconfig", + Usage: "Return a deployment kubeconfig", + UsageText: "Retrieve a c3os network kubeconfig (only for automated deployments)", + Description: ` +Retrieve a network kubeconfig and prints out to screen. + +If a deployment was bootstrapped with a network token, you can use this command to retrieve the master node kubeconfig of a network id. + +For example: + +$ c3os get-kubeconfig --network-id c3os +`, + Flags: networkAPI, + Action: func(c *cli.Context) error { + cc := service.NewClient( + c.String("network-id"), + edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) + str, _ := cc.Get("kubeconfig", "master") + b, _ := base64.RawURLEncoding.DecodeString(str) + masterIP, _ := cc.Get("master", "ip") + fmt.Println(strings.ReplaceAll(string(b), "127.0.0.1", masterIP)) + return nil + }, + }, + { + Name: "role", + Usage: "Set or list node roles", + Subcommands: []cli.Command{ + { + Flags: networkAPI, + Name: "set", + Usage: "Set a node role", + UsageText: "c3os role set master", + Description: ` +Sets a node role propagating the setting to the network. + +A role must be set prior to the node joining a network. You can retrieve a node UUID by running "c3os uuid". + +Example: + +$ (node A) c3os uuid +$ (node B) c3os role set master +`, + Action: func(c *cli.Context) error { + cc := service.NewClient( + c.String("network-id"), + edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) + return cc.Set("role", c.Args()[0], c.Args()[1]) + }, + }, + { + Flags: networkAPI, + Name: "list", + Description: "List node roles", + Action: func(c *cli.Context) error { + cc := service.NewClient( + c.String("network-id"), + edgeVPNClient.NewClient(edgeVPNClient.WithHost(c.String("api")))) + advertizing, _ := cc.AdvertizingNodes() + fmt.Println("Node\tRole") + for _, a := range advertizing { + role, _ := cc.Get("role", a) + fmt.Printf("%s\t%s\n", a, role) + } + return nil + }, + }, + }, + }, + { + Name: "create-config", + Aliases: []string{"c"}, + UsageText: "Create a config with a generated network token", + + Usage: "Creates a pristine config file", + Description: ` +Prints a vanilla YAML configuration on screen which can be used to bootstrap a c3os network. +`, + ArgsUsage: "Optionally takes a token rotation interval (seconds)", + + Action: func(c *cli.Context) error { + l := int(^uint(0) >> 1) + args := c.Args() + if len(args) > 0 { + if i, err := strconv.Atoi(args[0]); err == nil { + l = i + } + } + cc := &config.Config{C3OS: &config.C3OS{NetworkToken: node.GenerateNewConnectionData(l).Base64()}} + y, _ := yaml.Marshal(cc) + fmt.Println(string(y)) + return nil + }, + }, + { + Name: "generate-token", + Aliases: []string{"g"}, + UsageText: "Generate a network token", + Usage: "Creates a new token", + Description: ` +Generates a new token which can be used to bootstrap a c3os network. +`, + ArgsUsage: "Optionally takes a token rotation interval (seconds)", + + Action: func(c *cli.Context) error { + l := int(^uint(0) >> 1) + args := c.Args() + if len(args) > 0 { + if i, err := strconv.Atoi(args[0]); err == nil { + l = i + } + } + fmt.Println(node.GenerateNewConnectionData(l).Base64()) + return nil + }, + }, +} diff --git a/internal/cmd/flags.go b/internal/cmd/flags.go new file mode 100644 index 0000000..308d631 --- /dev/null +++ b/internal/cmd/flags.go @@ -0,0 +1,18 @@ +package cmd + +import ( + "github.com/urfave/cli" +) + +var networkAPI = []cli.Flag{ + &cli.StringFlag{ + Name: "api", + Usage: "API Address", + Value: "http://localhost:8080", + }, + &cli.StringFlag{ + Name: "network-id", + Value: "c3os", + Usage: "Kubernetes Network Deployment ID", + }, +} diff --git a/internal/cmd/utils.go b/internal/cmd/utils.go new file mode 100644 index 0000000..557a0af --- /dev/null +++ b/internal/cmd/utils.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/c3os-io/c3os/internal/c3os" + "github.com/c3os-io/c3os/internal/utils" + "github.com/pterm/pterm" +) + +func PrintTextFromFile(f string, banner string) { + installText := "" + text, err := ioutil.ReadFile(f) + if err == nil { + installText = string(text) + } + pterm.DefaultBox.WithTitle(banner).WithTitleBottomRight().WithRightPadding(0).WithBottomPadding(0).Println( + installText) +} + +func PrintBranding(b []byte) { + brandingFile := c3os.BrandingFile("banner") + if _, err := os.Stat(brandingFile); err == nil { + f, err := ioutil.ReadFile(brandingFile) + if err == nil { + fmt.Println(string(f)) + } + + } + utils.PrintBanner(b) +} diff --git a/internal/github/releases.go b/internal/github/releases.go new file mode 100644 index 0000000..fef096b --- /dev/null +++ b/internal/github/releases.go @@ -0,0 +1,49 @@ +package github + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + + "github.com/google/go-github/v40/github" + "golang.org/x/oauth2" +) + +func newHTTPClient(ctx context.Context, token string) *http.Client { + if token == "" { + return http.DefaultClient + } + src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + return oauth2.NewClient(ctx, src) +} + +func FindReleases(ctx context.Context, token, slug string) ([]string, error) { + hc := newHTTPClient(ctx, token) + cli := github.NewClient(hc) + + repo := strings.Split(slug, "/") + if len(repo) != 2 || repo[0] == "" || repo[1] == "" { + return nil, fmt.Errorf("Invalid slug format. It should be 'owner/name': %s", slug) + } + + rels, res, err := cli.Repositories.ListReleases(ctx, repo[0], repo[1], nil) + if err != nil { + log.Println("API returned an error response:", err) + if res != nil && res.StatusCode == 404 { + // 404 means repository not found or release not found. It's not an error here. + err = nil + log.Println("API returned 404. Repository or release not found") + } + return nil, err + } + + versions := []string{} + for _, rel := range rels { + if strings.HasPrefix(*rel.Name, "v") { + versions = append(versions, *rel.Name) + } + } + return versions, nil +} diff --git a/internal/machine/machine.go b/internal/machine/machine.go new file mode 100644 index 0000000..e31ca94 --- /dev/null +++ b/internal/machine/machine.go @@ -0,0 +1,103 @@ +package machine + +import ( + "fmt" + "os" + + "github.com/c3os-io/c3os/internal/machine/openrc" + "github.com/c3os-io/c3os/internal/machine/systemd" + "github.com/denisbrodbeck/machineid" + + "github.com/c3os-io/c3os/internal/utils" +) + +type Service interface { + WriteUnit() error + Start() error + OverrideCmd(string) error + Enable() error + Restart() error +} + +func EdgeVPN(instance, rootDir string) (Service, error) { + if utils.IsOpenRCBased() { + return openrc.NewService( + openrc.WithName("edgevpn"), + openrc.WithUnitContent(openrc.EdgevpnUnit), + openrc.WithRoot(rootDir), + ) + } else { + 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") + return nil +} + +func Getty(i int) (Service, error) { + if utils.IsOpenRCBased() { + return &fakegetty{}, nil + } else { + 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"), + ) + } else { + return systemd.NewService( + systemd.WithName("k3s"), + ) + } +} + +func K3sAgent() (Service, error) { + if utils.IsOpenRCBased() { + return openrc.NewService( + openrc.WithName("k3s-agent"), + ) + } else { + return systemd.NewService( + systemd.WithName("k3s-agent"), + ) + } +} + +func K3sEnvUnit(unit string) string { + if utils.IsOpenRCBased() { + return fmt.Sprintf("/etc/rancher/k3s/%s.env", unit) + } else { + 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) +} diff --git a/internal/machine/openrc/edgevpn.go b/internal/machine/openrc/edgevpn.go new file mode 100644 index 0000000..246b525 --- /dev/null +++ b/internal/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/internal/machine/openrc/unit.go b/internal/machine/openrc/unit.go new file mode 100644 index 0000000..3a87e42 --- /dev/null +++ b/internal/machine/openrc/unit.go @@ -0,0 +1,86 @@ +package openrc + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + "github.com/c3os-io/c3os/internal/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 { + cmd = strings.ReplaceAll(cmd, "/usr/bin/k3s ", "") + svcDir := filepath.Join(s.rootdir, fmt.Sprintf("/etc/rancher/k3s/%s.env", s.name)) + + return ioutil.WriteFile(svcDir, []byte(fmt.Sprintf(`command_args="%s >>/var/log/%s.log 2>&1"`, cmd, s.name)), 0600) +} + +func (s ServiceUnit) Start() error { + _, err := utils.SH(fmt.Sprintf("/etc/init.d/%s start", s.name)) + return err +} + +func (s ServiceUnit) Restart() error { + _, err := utils.SH(fmt.Sprintf("/etc/init.d/%s restart", s.name)) + return err +} + +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/internal/machine/systemd/edgevpn.go b/internal/machine/systemd/edgevpn.go new file mode 100644 index 0000000..c2f8779 --- /dev/null +++ b/internal/machine/systemd/edgevpn.go @@ -0,0 +1,13 @@ +package systemd + +const EdgevpnUnit string = `[Unit] +Description=EdgeVPN Daemon +After=network.target +[Service] +EnvironmentFile=/etc/systemd/system.conf.d/edgevpn-%i.env +LimitNOFILE=49152 +ExecStartPre=-/bin/sh -c "sysctl -w net.core.rmem_max=2500000" +ExecStart=edgevpn +Restart=always +[Install] +WantedBy=multi-user.target` diff --git a/internal/machine/systemd/unit.go b/internal/machine/systemd/unit.go new file mode 100644 index 0000000..aacb4ff --- /dev/null +++ b/internal/machine/systemd/unit.go @@ -0,0 +1,115 @@ +package systemd + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/c3os-io/c3os/internal/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 + } + + utils.SH("systemctl daemon-reload") + return nil +} + +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) + + 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 +} diff --git a/internal/provider/bootstrap.go b/internal/provider/bootstrap.go new file mode 100644 index 0000000..ac0778b --- /dev/null +++ b/internal/provider/bootstrap.go @@ -0,0 +1,210 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + logging "github.com/ipfs/go-log" + edgeVPNClient "github.com/mudler/edgevpn/api/client" + "go.uber.org/zap" + + eventBus "github.com/c3os-io/c3os/internal/bus" + "github.com/c3os-io/c3os/internal/machine" + "github.com/c3os-io/c3os/internal/machine/openrc" + "github.com/c3os-io/c3os/internal/machine/systemd" + "github.com/c3os-io/c3os/internal/role" + "github.com/c3os-io/c3os/internal/utils" + "github.com/c3os-io/c3os/internal/vpn" + + "github.com/c3os-io/c3os/pkg/bus" + "github.com/c3os-io/c3os/pkg/config" + "github.com/mudler/edgevpn/api/client/service" + "github.com/mudler/go-pluggable" +) + +func Bootstrap(e *pluggable.Event) pluggable.EventResponse { + cfg := &bus.BootstrapPayload{} + err := json.Unmarshal([]byte(e.Data), cfg) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s input '%s'", err.Error(), e.Data)} + } + + c := &config.Config{} + err = config.FromString(cfg.Config, c) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s input '%s'", err.Error(), cfg.Config)} + } + + utils.SH("sysctl -w net.core.rmem_max=2500000") + + tokenNotDefined := (c.C3OS == nil || c.C3OS.NetworkToken == "") + + if c.C3OS == nil && !c.K3s.Enabled && !c.K3sAgent.Enabled { + return pluggable.EventResponse{State: "No config file supplied"} + } + + utils.SH("elemental run-stage c3os-agent.bootstrap") + eventBus.RunHookScript("/usr/bin/c3os-agent.bootstrap.hook") + + logLevel := "debug" + + if c.C3OS != nil && c.C3OS.LogLevel != "" { + logLevel = c.C3OS.LogLevel + } + + lvl, err := logging.LevelFromString(logLevel) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed setup VPN: %s", err.Error())} + } + + // TODO: Fixup Logging to file + loggerCfg := zap.NewProductionConfig() + loggerCfg.OutputPaths = []string{ + cfg.Logfile, + } + logger, err := loggerCfg.Build() + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed setup VPN: %s", err.Error())} + } + + logging.SetAllLoggers(lvl) + + log := &logging.ZapEventLogger{SugaredLogger: *logger.Sugar()} + + // Do onetimebootstrap if K3s or K3s-agent are enabled. + // Those blocks are not required to be enabled in case of a c3os + // full automated setup. Otherwise, they must be explicitly enabled. + if c.K3s.Enabled || c.K3sAgent.Enabled { + err := oneTimeBootstrap(log, c, func() error { return vpn.Setup(machine.EdgeVPNDefaultInstance, cfg.APIAddress, "/", true, c) }) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed setup: %s", err.Error())} + } + return pluggable.EventResponse{} + } else if tokenNotDefined { + return pluggable.EventResponse{Error: "No network token provided, exiting"} + } + + logger.Info("Configuring VPN") + + if err := vpn.Setup(machine.EdgeVPNDefaultInstance, cfg.APIAddress, "/", true, c); err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed setup VPN: %s", err.Error())} + } + + networkID := "c3os" + + if c.C3OS != nil && c.C3OS.NetworkID != "" { + networkID = c.C3OS.NetworkID + } + + cc := service.NewClient( + networkID, + edgeVPNClient.NewClient(edgeVPNClient.WithHost(cfg.APIAddress))) + + nodeOpts := []service.Option{ + service.WithLogger(log), + service.WithClient(cc), + service.WithUUID(machine.UUID()), + service.WithStateDir("/usr/local/.c3os/state"), + service.WithNetworkToken(c.C3OS.NetworkToken), + service.WithPersistentRoles("auto"), + service.WithRoles( + service.RoleKey{ + Role: "master", + RoleHandler: role.Master(c), + }, + service.RoleKey{ + Role: "worker", + RoleHandler: role.Worker(c), + }, + service.RoleKey{ + Role: "auto", + RoleHandler: role.Auto(c), + }, + ), + } + + // Optionally set up a specific node role if the user has defined so + if c.C3OS.Role != "" { + nodeOpts = append(nodeOpts, service.WithDefaultRoles(c.C3OS.Role)) + } + + k, err := service.NewNode(nodeOpts...) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed creating node: %s", err.Error())} + } + err = k.Start(context.Background()) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed start: %s", err.Error())} + } + + return pluggable.EventResponse{ + State: "", + Data: "", + Error: "shouldn't return here", + } +} + +func oneTimeBootstrap(l logging.StandardLogger, c *config.Config, vpnSetupFN func() error) error { + if role.SentinelExist() { + l.Info("Sentinel exists, nothing to do. exiting.") + return nil + } + l.Info("One time bootstrap starting") + + var svc machine.Service + k3sConfig := config.K3s{} + svcName := "k3s" + svcRole := "server" + + if c.K3s.Enabled { + k3sConfig = c.K3s + } else if c.K3sAgent.Enabled { + k3sConfig = c.K3sAgent + svcName = "k3s-agent" + svcRole = "agent" + } + + if utils.IsOpenRCBased() { + svc, _ = openrc.NewService( + openrc.WithName(svcName), + ) + } else { + svc, _ = systemd.NewService( + systemd.WithName(svcName), + ) + } + + envFile := machine.K3sEnvUnit(svcName) + if svc == nil { + return fmt.Errorf("could not detect OS") + } + + // Setup systemd unit and starts it + if err := utils.WriteEnv(envFile, + k3sConfig.Env, + ); err != nil { + return err + } + + if err := svc.OverrideCmd(fmt.Sprintf("/usr/bin/k3s %s %s", svcRole, strings.Join(k3sConfig.Args, " "))); err != nil { + return err + } + + if err := svc.Start(); err != nil { + return err + } + + if err := svc.Enable(); err != nil { + return err + } + + if len(c.VPN) > 0 { + if err := vpnSetupFN(); err != nil { + return err + } + } + + return role.CreateSentinel() +} diff --git a/internal/provider/bootstrap_test.go b/internal/provider/bootstrap_test.go new file mode 100644 index 0000000..5224130 --- /dev/null +++ b/internal/provider/bootstrap_test.go @@ -0,0 +1,53 @@ +package provider_test + +import ( + "encoding/json" + "io/ioutil" + "os" + + . "github.com/c3os-io/c3os/internal/provider" + "github.com/c3os-io/c3os/pkg/bus" + "github.com/c3os-io/c3os/pkg/config" + "github.com/mudler/go-pluggable" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "gopkg.in/yaml.v2" +) + +var _ = Describe("Bootstrap provider", func() { + Context("logging", func() { + e := &pluggable.Event{} + + BeforeEach(func() { + e = &pluggable.Event{} + }) + + It("logs to file", func() { + f, err := ioutil.TempFile(os.TempDir(), "tests") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(f.Name()) + + cfg := &config.Config{ + C3OS: &config.C3OS{ + NetworkToken: "foo", + }, + } + dat, err := yaml.Marshal(cfg) + Expect(err).ToNot(HaveOccurred()) + payload := &bus.BootstrapPayload{Logfile: f.Name(), Config: string(dat)} + + dat, err = json.Marshal(payload) + Expect(err).ToNot(HaveOccurred()) + + e.Data = string(dat) + resp := Bootstrap(e) + dat, _ = json.Marshal(resp) + Expect(resp.Errored()).To(BeTrue(), string(dat)) + + data, err := ioutil.ReadFile(f.Name()) + Expect(err).ToNot(HaveOccurred()) + + Expect(string(data)).Should(ContainSubstring("Configuring VPN")) + }) + }) +}) diff --git a/internal/provider/challenge.go b/internal/provider/challenge.go new file mode 100644 index 0000000..77b47c2 --- /dev/null +++ b/internal/provider/challenge.go @@ -0,0 +1,36 @@ +package provider + +import ( + "encoding/json" + "fmt" + + "github.com/c3os-io/c3os/pkg/bus" + "github.com/c3os-io/c3os/pkg/config" + "github.com/mudler/go-nodepair" + "github.com/mudler/go-pluggable" +) + +func Challenge(e *pluggable.Event) pluggable.EventResponse { + p := &bus.EventPayload{} + err := json.Unmarshal([]byte(e.Data), p) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s input '%s'", err.Error(), e.Data)} + } + + cfg := &config.Config{} + err = config.FromString(p.Config, cfg) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s input '%s'", err.Error(), p.Config)} + } + + tk := "" + if cfg.C3OS != nil && cfg.C3OS.NetworkToken != "" { + tk = cfg.C3OS.NetworkToken + } + if tk == "" { + tk = nodepair.GenerateToken() + } + return pluggable.EventResponse{ + Data: tk, + } +} diff --git a/internal/provider/challenge_test.go b/internal/provider/challenge_test.go new file mode 100644 index 0000000..232e495 --- /dev/null +++ b/internal/provider/challenge_test.go @@ -0,0 +1,64 @@ +package provider_test + +import ( + "encoding/json" + "io/ioutil" + "os" + + . "github.com/c3os-io/c3os/internal/provider" + "github.com/c3os-io/c3os/pkg/bus" + "github.com/c3os-io/c3os/pkg/config" + "github.com/mudler/go-pluggable" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Challenge provider", func() { + Context("network token", func() { + e := &pluggable.Event{} + + BeforeEach(func() { + e = &pluggable.Event{} + }) + + It("returns it if provided", func() { + f, err := ioutil.TempFile(os.TempDir(), "tests") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(f.Name()) + + cfg := &config.Config{ + C3OS: &config.C3OS{ + NetworkToken: "foo", + }, + } + c := &bus.EventPayload{Config: cfg.String()} + dat, err := json.Marshal(c) + Expect(err).ToNot(HaveOccurred()) + + e.Data = string(dat) + resp := Challenge(e) + + Expect(string(resp.Data)).Should(ContainSubstring("foo")) + }) + + It("generates it if not provided", func() { + f, err := ioutil.TempFile(os.TempDir(), "tests") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(f.Name()) + + cfg := &config.Config{ + C3OS: &config.C3OS{ + NetworkToken: "", + }, + } + c := &bus.EventPayload{Config: cfg.String()} + dat, err := json.Marshal(c) + Expect(err).ToNot(HaveOccurred()) + + e.Data = string(dat) + resp := Challenge(e) + + Expect(len(string(resp.Data))).Should(BeNumerically(">", 12)) + }) + }) +}) diff --git a/internal/provider/install.go b/internal/provider/install.go new file mode 100644 index 0000000..bcf327e --- /dev/null +++ b/internal/provider/install.go @@ -0,0 +1,36 @@ +package provider + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/c3os-io/c3os/pkg/bus" + "github.com/mudler/go-nodepair" + "github.com/mudler/go-pluggable" +) + +func Install(e *pluggable.Event) pluggable.EventResponse { + cfg := &bus.InstallPayload{} + err := json.Unmarshal([]byte(e.Data), cfg) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s", err.Error())} + } + + r := map[string]string{} + ctx := context.Background() + if err := nodepair.Receive(ctx, &r, nodepair.WithToken(cfg.Token)); err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed reading JSON input: %s", err.Error())} + } + + payload, err := json.Marshal(r) + if err != nil { + return pluggable.EventResponse{Error: fmt.Sprintf("Failed marshalling JSON input: %s", err.Error())} + } + + return pluggable.EventResponse{ + State: "", + Data: string(payload), + Error: "", + } +} diff --git a/internal/provider/provider_suite_test.go b/internal/provider/provider_suite_test.go new file mode 100644 index 0000000..c9a2885 --- /dev/null +++ b/internal/provider/provider_suite_test.go @@ -0,0 +1,13 @@ +package provider_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestInstaller(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Provider Suite") +} diff --git a/internal/role/auto.go b/internal/role/auto.go new file mode 100644 index 0000000..48dc41b --- /dev/null +++ b/internal/role/auto.go @@ -0,0 +1,57 @@ +package role + +import ( + "github.com/c3os-io/c3os/pkg/config" + utils "github.com/mudler/edgevpn/pkg/utils" + + service "github.com/mudler/edgevpn/api/client/service" +) + +func contains(slice []string, elem string) bool { + for _, s := range slice { + if elem == s { + return true + } + } + return false +} +func Auto(cc *config.Config) Role { + return func(c *service.RoleConfig) error { + advertizing, _ := c.Client.AdvertizingNodes() + actives, _ := c.Client.ActiveNodes() + + c.Logger.Info("Active nodes:", actives) + c.Logger.Info("Advertizing nodes:", advertizing) + + if len(advertizing) < 2 { + c.Logger.Info("Not enough nodes") + return nil + } + + // first get available nodes + nodes := advertizing + shouldBeLeader := utils.Leader(advertizing) + + lead, _ := c.Client.Get("auto", "leader") + + // From now on, only the leader keeps processing + // TODO: Make this more reliable with consensus + if shouldBeLeader != c.UUID && lead != c.UUID { + c.Logger.Infof("<%s> not a leader, leader is '%s', sleeping", c.UUID, shouldBeLeader) + return nil + } + + if shouldBeLeader == c.UUID && (lead == "" || !contains(nodes, lead)) { + c.Client.Set("auto", "leader", c.UUID) + c.Logger.Info("Announcing ourselves as leader, backing off") + return nil + } + + if lead != c.UUID { + c.Logger.Info("Backing off, as we are not currently flagged as leader") + return nil + } + + return scheduleRoles(nodes, c, cc) + } +} diff --git a/internal/role/common.go b/internal/role/common.go new file mode 100644 index 0000000..1efa51f --- /dev/null +++ b/internal/role/common.go @@ -0,0 +1,21 @@ +package role + +import ( + "io/ioutil" + "os" + + service "github.com/mudler/edgevpn/api/client/service" +) + +type Role func(*service.RoleConfig) error + +func SentinelExist() bool { + if _, err := os.Stat("/usr/local/.c3os/deployed"); err == nil { + return true + } + return false +} + +func CreateSentinel() error { + return ioutil.WriteFile("/usr/local/.c3os/deployed", []byte{}, os.ModePerm) +} diff --git a/internal/role/master.go b/internal/role/master.go new file mode 100644 index 0000000..dc4e459 --- /dev/null +++ b/internal/role/master.go @@ -0,0 +1,134 @@ +package role + +import ( + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/c3os-io/c3os/internal/machine" + "github.com/c3os-io/c3os/internal/utils" + "github.com/c3os-io/c3os/pkg/config" + + service "github.com/mudler/edgevpn/api/client/service" +) + +func propagateMasterData(ip string, c *service.RoleConfig) error { + defer func() { + // Avoid polluting the API. + // The ledger already retries in the background to update the blockchain, but it has + // a default timeout where it would stop trying afterwards. + // Each request here would have it's own background announce, so that can become expensive + // when network is having lot of changes on its way. + time.Sleep(30 * time.Second) + }() + + // If we are configured as master, always signal our role + c.Client.Set("role", c.UUID, "master") + + tokenB, err := ioutil.ReadFile("/var/lib/rancher/k3s/server/node-token") + if err != nil { + c.Logger.Error(err) + return err + } + + nodeToken := string(tokenB) + nodeToken = strings.TrimRight(nodeToken, "\n") + if nodeToken != "" { + c.Client.Set("nodetoken", "token", nodeToken) + } + + kubeB, err := ioutil.ReadFile("/etc/rancher/k3s/k3s.yaml") + if err != nil { + c.Logger.Error(err) + return err + } + kubeconfig := string(kubeB) + if kubeconfig != "" { + c.Client.Set("kubeconfig", "master", base64.RawURLEncoding.EncodeToString(kubeB)) + } + c.Client.Set("master", "ip", ip) + return nil +} + +func Master(cc *config.Config) Role { + return func(c *service.RoleConfig) error { + + ip := utils.GetInterfaceIP("edgevpn0") + if ip == "" { + return errors.New("node doesn't have an ip yet") + } + + if cc.C3OS.Role != "" { + // propagate role if we were forced by configuration + // This unblocks eventual auto instances to try to assign roles + c.Client.Set("role", c.UUID, cc.C3OS.Role) + } + + if SentinelExist() { + c.Logger.Info("Node already configured, backing off") + return propagateMasterData(ip, c) + } + + // Configure k3s service to start on edgevpn0 + c.Logger.Info("Configuring k3s") + + svc, err := machine.K3s() + if err != nil { + return err + } + + k3sConfig := config.K3s{} + if cc.K3s.Enabled { + k3sConfig = cc.K3s + } + + env := map[string]string{} + if !k3sConfig.ReplaceEnv { + // Override opts with user-supplied + for k, v := range k3sConfig.Env { + env[k] = v + } + } else { + env = k3sConfig.Env + } + + if err := utils.WriteEnv(machine.K3sEnvUnit("k3s"), + env, + ); err != nil { + return err + } + + args := []string{"--flannel-iface=edgevpn0"} + if k3sConfig.ReplaceArgs { + args = k3sConfig.Args + } else { + args = append(args, k3sConfig.Args...) + } + + if err := svc.OverrideCmd(fmt.Sprintf("/usr/bin/k3s server %s", strings.Join(args, " "))); err != nil { + return err + } + + if err := svc.Start(); err != nil { + return err + } + + if err := svc.Enable(); err != nil { + return err + } + + propagateMasterData(ip, c) + + CreateSentinel() + + return nil + } +} + +// TODO: https://rancher.com/docs/k3s/latest/en/installation/ha-embedded/ +func HAMaster(c *service.RoleConfig) { + c.Logger.Info("HA Role not implemented yet") +} diff --git a/internal/role/schedule.go b/internal/role/schedule.go new file mode 100644 index 0000000..80e6377 --- /dev/null +++ b/internal/role/schedule.go @@ -0,0 +1,73 @@ +package role + +import ( + "math/rand" + "time" + + "github.com/c3os-io/c3os/pkg/config" + service "github.com/mudler/edgevpn/api/client/service" +) + +// scheduleRoles assigns roles to nodes. Meant to be called only by leaders +// TODO: HA-Auto +func scheduleRoles(nodes []string, c *service.RoleConfig, cc *config.Config) error { + rand.Seed(time.Now().Unix()) + + // Assign roles to nodes + currentRoles := map[string]string{} + + existsMaster := false + unassignedNodes := []string{} + for _, a := range nodes { + role, _ := c.Client.Get("role", a) + currentRoles[a] = role + if role == "master" { + existsMaster = true + } else if role == "" { + unassignedNodes = append(unassignedNodes, a) + } + } + + c.Logger.Infof("I'm the leader. My UUID is: %s.\n Current assigned roles: %+v", c.UUID, currentRoles) + c.Logger.Infof("Master already present: %t", existsMaster) + c.Logger.Infof("Unassigned nodes: %+v", unassignedNodes) + + if !existsMaster && len(unassignedNodes) > 0 { + var selected string + toSelect := unassignedNodes + + // Avoid to schedule to ourselves if we have a static role + if cc.C3OS.Role != "" { + toSelect = []string{} + for _, u := range unassignedNodes { + if u != c.UUID { + toSelect = append(toSelect, u) + } + } + } + + // select one node without roles to become master + if len(toSelect) == 1 { + selected = toSelect[0] + } else { + selected = toSelect[rand.Intn(len(toSelect)-1)] + } + + c.Client.Set("role", selected, "master") + c.Logger.Info("-> Set master to", selected) + currentRoles[selected] = "master" + // Return here, so next time we get called + // makes sure master is set. + return nil + } + + // cycle all empty roles and assign worker roles + for _, uuid := range unassignedNodes { + c.Client.Set("role", uuid, "worker") + c.Logger.Info("-> Set worker to", uuid) + } + + c.Logger.Info("Done scheduling") + + return nil +} diff --git a/internal/role/worker.go b/internal/role/worker.go new file mode 100644 index 0000000..f1af388 --- /dev/null +++ b/internal/role/worker.go @@ -0,0 +1,108 @@ +package role + +import ( + "errors" + "fmt" + "strings" + + "github.com/c3os-io/c3os/internal/machine" + "github.com/c3os-io/c3os/internal/utils" + "github.com/c3os-io/c3os/pkg/config" + + service "github.com/mudler/edgevpn/api/client/service" +) + +func Worker(cc *config.Config) Role { + return func(c *service.RoleConfig) error { + + if cc.C3OS.Role != "" { + // propagate role if we were forced by configuration + // This unblocks eventual auto instances to try to assign roles + c.Client.Set("role", c.UUID, cc.C3OS.Role) + } + + if SentinelExist() { + c.Logger.Info("Node already configured, backing off") + return nil + } + + masterIP, _ := c.Client.Get("master", "ip") + if masterIP == "" { + c.Logger.Info("MasterIP not there still..") + return nil + } + + nodeToken, _ := c.Client.Get("nodetoken", "token") + if masterIP == "" { + c.Logger.Info("nodetoken not there still..") + return nil + } + + nodeToken = strings.TrimRight(nodeToken, "\n") + + ip := utils.GetInterfaceIP("edgevpn0") + if ip == "" { + return errors.New("node doesn't have an ip yet") + } + + c.Logger.Info("Configuring k3s-agent", ip, masterIP, nodeToken) + + svc, err := machine.K3sAgent() + if err != nil { + return err + } + + k3sConfig := config.K3s{} + if cc.K3sAgent.Enabled { + k3sConfig = cc.K3sAgent + } + + env := map[string]string{ + "K3S_URL": fmt.Sprintf("https://%s:6443", masterIP), + "K3S_TOKEN": nodeToken, + } + + if !k3sConfig.ReplaceEnv { + // Override opts with user-supplied + for k, v := range k3sConfig.Env { + env[k] = v + } + } else { + env = k3sConfig.Env + } + + // Setup systemd unit and starts it + if err := utils.WriteEnv(machine.K3sEnvUnit("k3s-agent"), + env, + ); err != nil { + return err + } + + args := []string{ + "--with-node-id", + fmt.Sprintf("--node-ip %s", ip), + "--flannel-iface=edgevpn0", + } + if k3sConfig.ReplaceArgs { + args = k3sConfig.Args + } else { + args = append(args, k3sConfig.Args...) + } + + if err := svc.OverrideCmd(fmt.Sprintf("/usr/bin/k3s agent %s", strings.Join(args, " "))); err != nil { + return err + } + + if err := svc.Start(); err != nil { + return err + } + + if err := svc.Enable(); err != nil { + return err + } + + CreateSentinel() + + return nil + } +} diff --git a/internal/utils/console.go b/internal/utils/console.go new file mode 100644 index 0000000..4b9c12d --- /dev/null +++ b/internal/utils/console.go @@ -0,0 +1,37 @@ +package utils + +import ( + "bufio" + "bytes" + "fmt" + "image" + "os" + "strings" + + "github.com/pterm/pterm" + "github.com/qeesung/image2ascii/convert" +) + +func Prompt(t string) (string, error) { + if t != "" { + pterm.Info.Println(t) + } + answer, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + return "", err + } + + return strings.TrimSpace(answer), nil +} + +func PrintBanner(d []byte) { + img, _, _ := image.Decode(bytes.NewReader(d)) + + convertOptions := convert.DefaultOptions + convertOptions.FixedWidth = 100 + convertOptions.FixedHeight = 40 + + // Create the image converter + converter := convert.NewImageConverter() + fmt.Print(converter.Image2ASCIIString(img, &convertOptions)) +} diff --git a/internal/utils/sh.go b/internal/utils/sh.go new file mode 100644 index 0000000..4f0e619 --- /dev/null +++ b/internal/utils/sh.go @@ -0,0 +1,33 @@ +package utils + +import ( + "io/ioutil" + "os" + "os/exec" + + "github.com/joho/godotenv" +) + +func SH(c string) (string, error) { + o, err := exec.Command("/bin/sh", "-c", c).CombinedOutput() + return string(o), err +} + +func WriteEnv(envFile string, config map[string]string) error { + content, _ := ioutil.ReadFile(envFile) + env, _ := godotenv.Unmarshal(string(content)) + + for key, val := range config { + env[key] = val + } + + return godotenv.Write(env, envFile) +} + +func Shell() *exec.Cmd { + cmd := exec.Command("/bin/sh") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + return cmd +} diff --git a/internal/utils/signal.go b/internal/utils/signal.go new file mode 100644 index 0000000..62af370 --- /dev/null +++ b/internal/utils/signal.go @@ -0,0 +1,15 @@ +package utils + +import ( + "os" + "os/signal" +) + +func OnSignal(fn func(), sig ...os.Signal) { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, sig...) + go func() { + <-sigs + fn() + }() +} diff --git a/internal/utils/strings.go b/internal/utils/strings.go new file mode 100644 index 0000000..39c5109 --- /dev/null +++ b/internal/utils/strings.go @@ -0,0 +1,20 @@ +package utils + +import ( + "math/rand" + "time" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func RandStringRunes(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} diff --git a/internal/utils/system.go b/internal/utils/system.go new file mode 100644 index 0000000..985a57e --- /dev/null +++ b/internal/utils/system.go @@ -0,0 +1,82 @@ +package utils + +import ( + "fmt" + "net" + "strings" + + "github.com/joho/godotenv" + "github.com/pterm/pterm" +) + +func Reboot() { + pterm.Info.Println("Rebooting node") + SH("reboot") +} + +func PowerOFF() { + pterm.Info.Println("Shutdown node") + if IsOpenRCBased() { + SH("poweroff") + } else { + SH("shutdown") + } +} + +func Version() string { + release, _ := godotenv.Read("/etc/os-release") + v := release["VERSION"] + v = strings.ReplaceAll(v, "+k3s1-c3OS", "-") + return strings.ReplaceAll(v, "+k3s-c3OS", "-") +} + +func OSRelease(key string) (string, error) { + release, err := godotenv.Read("/etc/os-release") + if err != nil { + return "", err + } + v, exists := release[key] + if !exists { + return "", fmt.Errorf("key not found") + } + return v, nil +} + +func Flavor() string { + release, _ := godotenv.Read("/etc/os-release") + v := release["NAME"] + return strings.ReplaceAll(v, "c3os-", "") +} + +func IsOpenRCBased() bool { + f := Flavor() + return f == "alpine" || f == "alpine-arm-rpi" +} + +func GetInterfaceIP(in string) string { + ifaces, err := net.Interfaces() + if err != nil { + fmt.Println("failed getting system interfaces") + return "" + } + for _, i := range ifaces { + if i.Name == in { + addrs, _ := i.Addrs() + // handle err + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip != nil { + return ip.String() + + } + } + } + } + return "" +} diff --git a/internal/utils/token.go b/internal/utils/token.go new file mode 100644 index 0000000..884ed53 --- /dev/null +++ b/internal/utils/token.go @@ -0,0 +1,13 @@ +package utils + +import "strings" + +const sep = "_CREDENTIALS_" + +func EncodeRecoveryToken(data ...string) string { + return strings.Join(data, sep) +} + +func DecodeRecoveryToken(recoverytoken string) []string { + return strings.Split(recoverytoken, sep) +} diff --git a/internal/vpn/setup.go b/internal/vpn/setup.go new file mode 100644 index 0000000..16bb1ea --- /dev/null +++ b/internal/vpn/setup.go @@ -0,0 +1,90 @@ +package vpn + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/c3os-io/c3os/internal/machine" + "github.com/c3os-io/c3os/internal/machine/systemd" + "github.com/c3os-io/c3os/internal/utils" + "github.com/c3os-io/c3os/pkg/config" + yip "github.com/mudler/yip/pkg/schema" +) + +func Setup(instance, apiAddress, rootDir string, start bool, c *config.Config) error { + + if c.C3OS == nil || c.C3OS.NetworkToken == "" { + return fmt.Errorf("no network token defined") + } + + svc, err := machine.EdgeVPN(instance, rootDir) + if err != nil { + return err + } + + apiAddress = strings.ReplaceAll(apiAddress, "https://", "") + apiAddress = strings.ReplaceAll(apiAddress, "http://", "") + + vpnOpts := map[string]string{ + "EDGEVPNTOKEN": c.C3OS.NetworkToken, + "API": "true", + "APILISTEN": apiAddress, + "EDGEVPNLOWPROFILEVPN": "true", + "DHCP": "true", + "DHCPLEASEDIR": "/usr/local/.c3os/lease", + } + // Override opts with user-supplied + for k, v := range c.VPN { + vpnOpts[k] = v + } + + if c.C3OS.DNS { + vpnOpts["DNSADDRESS"] = "127.0.0.1:53" + vpnOpts["DNSFORWARD"] = "true" + if !utils.IsOpenRCBased() { + if _, err := os.Stat("/etc/sysconfig/network/config"); err == nil { + utils.WriteEnv("/etc/sysconfig/network/config", map[string]string{ + "NETCONFIG_DNS_STATIC_SERVERS": "127.0.0.1", + }) + if utils.Flavor() == "opensuse" { + // TODO: This is dependant on wickedd, move this out in its own network detection block + svc, err := systemd.NewService(systemd.WithName("wickedd")) + if err == nil { + svc.Restart() + } + } + } + } + if err := config.SaveCloudConfig("dns", yip.YipConfig{ + Name: "DNS Configuration", + Stages: map[string][]yip.Stage{ + config.NetworkStage.String(): {{Dns: yip.DNS{Nameservers: []string{"127.0.0.1"}}}}}, + }); err != nil { + fmt.Println("Failed installing DNS") + } + } + + os.MkdirAll("/etc/systemd/system.conf.d/", 0600) + // Setup edgevpn instance + err = utils.WriteEnv(filepath.Join(rootDir, "/etc/systemd/system.conf.d/edgevpn-c3os.env"), vpnOpts) + if err != nil { + return err + } + + err = svc.WriteUnit() + if err != nil { + return err + } + + if start { + err = svc.Start() + if err != nil { + return err + } + + return svc.Enable() + } + return nil +}