mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-09-03 18:16:50 +00:00
Split off cli into separate binaries (#37)
* 🎨 Split off cli into separate binaries This commit splits off the cli into 3 binaries: - agent - cli - provider The provider now is a separate component that can be tested by itself and have its own lifecycle. This paves the way to a ligher c3os variant, HA support and other features that can be provided on runtime. This is working, but still there are low hanging fruit to care about. Fixes #14 * 🤖 Add provider bin to releases * ⚙️ Handle signals * ⚙️ Reduce buildsize footprint * 🎨 Scan for providers also in /system/providers * 🤖 Run goreleaser * 🎨 Refactoring
This commit is contained in:
committed by
Itxaka
parent
74bfd373db
commit
63cd28d1cb
66
internal/bus/bus.go
Normal file
66
internal/bus/bus.go
Normal file
@@ -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()
|
||||||
|
}
|
7
internal/c3os/branding.go
Normal file
7
internal/c3os/branding.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package c3os
|
||||||
|
|
||||||
|
import "path"
|
||||||
|
|
||||||
|
func BrandingFile(s string) string {
|
||||||
|
return path.Join("/etc", "c3os", "branding", s)
|
||||||
|
}
|
141
internal/cmd/commands.go
Normal file
141
internal/cmd/commands.go
Normal file
@@ -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 <UUID> 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 <UUID of node A> 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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
18
internal/cmd/flags.go
Normal file
18
internal/cmd/flags.go
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
}
|
33
internal/cmd/utils.go
Normal file
33
internal/cmd/utils.go
Normal file
@@ -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)
|
||||||
|
}
|
49
internal/github/releases.go
Normal file
49
internal/github/releases.go
Normal file
@@ -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
|
||||||
|
}
|
103
internal/machine/machine.go
Normal file
103
internal/machine/machine.go
Normal file
@@ -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)
|
||||||
|
}
|
19
internal/machine/openrc/edgevpn.go
Normal file
19
internal/machine/openrc/edgevpn.go
Normal file
@@ -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`
|
86
internal/machine/openrc/unit.go
Normal file
86
internal/machine/openrc/unit.go
Normal file
@@ -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()
|
||||||
|
}
|
13
internal/machine/systemd/edgevpn.go
Normal file
13
internal/machine/systemd/edgevpn.go
Normal file
@@ -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`
|
115
internal/machine/systemd/unit.go
Normal file
115
internal/machine/systemd/unit.go
Normal file
@@ -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
|
||||||
|
}
|
210
internal/provider/bootstrap.go
Normal file
210
internal/provider/bootstrap.go
Normal file
@@ -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()
|
||||||
|
}
|
53
internal/provider/bootstrap_test.go
Normal file
53
internal/provider/bootstrap_test.go
Normal file
@@ -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"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
36
internal/provider/challenge.go
Normal file
36
internal/provider/challenge.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
64
internal/provider/challenge_test.go
Normal file
64
internal/provider/challenge_test.go
Normal file
@@ -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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
36
internal/provider/install.go
Normal file
36
internal/provider/install.go
Normal file
@@ -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: "",
|
||||||
|
}
|
||||||
|
}
|
13
internal/provider/provider_suite_test.go
Normal file
13
internal/provider/provider_suite_test.go
Normal file
@@ -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")
|
||||||
|
}
|
57
internal/role/auto.go
Normal file
57
internal/role/auto.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
21
internal/role/common.go
Normal file
21
internal/role/common.go
Normal file
@@ -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)
|
||||||
|
}
|
134
internal/role/master.go
Normal file
134
internal/role/master.go
Normal file
@@ -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")
|
||||||
|
}
|
73
internal/role/schedule.go
Normal file
73
internal/role/schedule.go
Normal file
@@ -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
|
||||||
|
}
|
108
internal/role/worker.go
Normal file
108
internal/role/worker.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
37
internal/utils/console.go
Normal file
37
internal/utils/console.go
Normal file
@@ -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))
|
||||||
|
}
|
33
internal/utils/sh.go
Normal file
33
internal/utils/sh.go
Normal file
@@ -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
|
||||||
|
}
|
15
internal/utils/signal.go
Normal file
15
internal/utils/signal.go
Normal file
@@ -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()
|
||||||
|
}()
|
||||||
|
}
|
20
internal/utils/strings.go
Normal file
20
internal/utils/strings.go
Normal file
@@ -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)
|
||||||
|
}
|
82
internal/utils/system.go
Normal file
82
internal/utils/system.go
Normal file
@@ -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 ""
|
||||||
|
}
|
13
internal/utils/token.go
Normal file
13
internal/utils/token.go
Normal file
@@ -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)
|
||||||
|
}
|
90
internal/vpn/setup.go
Normal file
90
internal/vpn/setup.go
Normal file
@@ -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
|
||||||
|
}
|
Reference in New Issue
Block a user