Files
warewulf/internal/app/wwclient/root.go
Jonathon Anderson 212b57f095 Merge pull request #1309 from griznog/sanitize-wwclient-url-data
sanitize wwclient url data
2024-08-02 10:50:26 -06:00

260 lines
7.2 KiB
Go

package wwclient
import (
"errors"
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"os/signal"
"path"
"strings"
"syscall"
"time"
"github.com/coreos/go-systemd/daemon"
"github.com/google/uuid"
"github.com/spf13/cobra"
"github.com/talos-systems/go-smbios/smbios"
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
"github.com/warewulf/warewulf/internal/pkg/pidfile"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
var (
rootCmd = &cobra.Command{
Use: "wwclient",
Short: "wwclient",
Long: "wwclient fetches the runtime overlay and puts it on the disk",
RunE: CobraRunE,
SilenceUsage: true,
}
DebugFlag bool
PIDFile string
Webclient *http.Client
WarewulfConfArg string
)
func init() {
rootCmd.PersistentFlags().BoolVarP(&DebugFlag, "debug", "d", false, "Run with debugging messages enabled.")
rootCmd.PersistentFlags().StringVarP(&PIDFile, "pidfile", "p", "/var/run/wwclient.pid", "PIDFile to use")
rootCmd.PersistentFlags().StringVar(&WarewulfConfArg, "warewulfconf", "", "Set the warewulf configuration file")
}
// GetRootCommand returns the root cobra.Command for the application.
func GetRootCommand() *cobra.Command {
// Run cobra
return rootCmd
}
func CobraRunE(cmd *cobra.Command, args []string) (err error) {
conf := warewulfconf.Get()
if WarewulfConfArg != "" {
err = conf.Read(WarewulfConfArg)
} else if os.Getenv("WAREWULFCONF") != "" {
err = conf.Read(os.Getenv("WAREWULFCONF"))
} else {
err = conf.Read(warewulfconf.ConfigFile)
}
if err != nil {
return
}
pid, err := pidfile.Write(PIDFile)
if err != nil && pid == -1 {
wwlog.Warn("%v. starting new wwclient", err)
} else if err != nil && pid > 0 {
return errors.New("found pidfile " + PIDFile + " not starting")
}
if os.Args[0] == path.Join(conf.Paths.WWClientdir, "wwclient") {
err := os.Chdir("/")
if err != nil {
wwlog.Error("failed to change dir: %s", err)
_ = os.Remove(PIDFile)
os.Exit(1)
}
log.Printf("Updating live file system LIVE, cancel now if this is in error")
time.Sleep(5000 * time.Millisecond)
} else {
fmt.Printf("Called via: %s\n", os.Args[0])
fmt.Printf("Runtime overlay is being put in '/warewulf/wwclient-test' rather than '/'\n")
fmt.Printf("For full functionality call with: %s\n", path.Join(conf.Paths.WWClientdir, "wwclient"))
err := os.MkdirAll("/warewulf/wwclient-test", 0755)
if err != nil {
wwlog.Error("failed to create dir: %s", err)
_ = os.Remove(PIDFile)
os.Exit(1)
}
err = os.Chdir("/warewulf/wwclient-test")
if err != nil {
wwlog.Error("failed to change dir: %s", err)
_ = os.Remove(PIDFile)
os.Exit(1)
}
}
localTCPAddr := net.TCPAddr{}
if conf.WWClient != nil && conf.WWClient.Port > 0 {
localTCPAddr.Port = int(conf.WWClient.Port)
wwlog.Info("Running from configured port %d", conf.WWClient.Port)
} else if conf.Warewulf.Secure {
// Setup local port to something privileged (<1024)
localTCPAddr.Port = 987
wwlog.Info("Running from trusted port")
}
Webclient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
LocalAddr: &localTCPAddr,
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
var localUUID uuid.UUID
var tag string
smbiosDump, smbiosErr := smbios.New()
if smbiosErr == nil {
sysinfoDump := smbiosDump.SystemInformation()
localUUID, _ = sysinfoDump.UUID()
x := smbiosDump.SystemEnclosure()
tag = strings.ReplaceAll(x.AssetTagNumber(), " ", "_")
if tag == "Unknown" {
dmiOut, err := exec.Command("dmidecode", "-s", "chassis-asset-tag").Output()
if err == nil {
chassisAssetTag := url.QueryEscape(strings.TrimSpace(string(dmiOut)))
if chassisAssetTag != "" {
tag = chassisAssetTag
}
}
}
} else {
// Raspberry Pi serial and DUID locations
// /sys/firmware/devicetree/base/serial-number
// /sys/firmware/devicetree/base/chosen/rpi-duid
piSerial, err := os.ReadFile("/sys/firmware/devicetree/base/serial-number")
if err != nil {
wwlog.Error("Could not get SMBIOS info: %s", smbiosErr)
os.Exit(1)
}
localUUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte("http://raspberrypi.com/serial-number/"+string(piSerial)))
tag = "Unknown"
}
cmdline, err := os.ReadFile("/proc/cmdline")
if err != nil {
wwlog.Error("Could not read from /proc/cmdline: %s", err)
os.Exit(1)
}
wwid_tmp := strings.Split(string(cmdline), "wwid=")
if len(wwid_tmp) < 2 {
wwlog.Error("'wwid' is not defined in /proc/cmdline")
os.Exit(1)
}
wwid := strings.Split(wwid_tmp[1], " ")[0]
wwid = strings.TrimSuffix(wwid, "\n")
// Dereference wwid from [interface] for cases that cannot have /proc/cmdline set by bootloader
if string(wwid[0]) == "[" {
iface := wwid[1 : len(wwid)-1]
wwid_tmp, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/address", iface))
if err != nil {
wwlog.Error("'wwid' cannot be dereferenced from /sys/class/net", iface)
os.Exit(1)
}
wwid = strings.TrimSuffix(string(wwid_tmp), "\n")
wwlog.Info("Dereferencing wwid from [%s] to %s", iface, wwid)
}
duration := 300
if conf.Warewulf.UpdateInterval > 0 {
duration = conf.Warewulf.UpdateInterval
}
stopTimer := time.NewTimer(time.Duration(duration) * time.Second)
// listen on SIGHUP
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
go func() {
for {
sig := <-sigs
switch sig {
case syscall.SIGHUP:
log.Printf("Received SIGNAL: %s\n", sig)
stopTimer.Stop()
stopTimer.Reset(0)
case syscall.SIGTERM, syscall.SIGINT:
wwlog.Info("termination wwclient!, %v", sig)
cleanUp()
os.Exit(0)
}
}
}()
var finishedInitialSync bool = false
for {
updateSystem(conf.Ipaddr, conf.Warewulf.Port, wwid, tag, localUUID)
if !finishedInitialSync {
// ignore error and status here, as this wouldn't change anything
_, _ = daemon.SdNotify(false, daemon.SdNotifyReady)
finishedInitialSync = true
}
<-stopTimer.C
stopTimer.Reset(time.Duration(duration) * time.Second)
}
}
func updateSystem(ipaddr string, port int, wwid string, tag string, localUUID uuid.UUID) {
var resp *http.Response
counter := 0
for {
var err error
getString := fmt.Sprintf("http://%s:%d/provision/%s?assetkey=%s&uuid=%s&stage=runtime&compress=gz", ipaddr, port, wwid, tag, localUUID)
wwlog.Debug("Making request: %s", getString)
resp, err = Webclient.Get(getString)
if err == nil {
break
} else {
if counter > 60 {
counter = 0
}
if counter == 0 {
log.Println(err)
}
counter++
}
time.Sleep(1000 * time.Millisecond)
}
if resp.StatusCode != 200 {
log.Printf("Not updating runtime overlay, got status code: %d\n", resp.StatusCode)
time.Sleep(60000 * time.Millisecond)
return
}
log.Printf("Updating system\n")
command := exec.Command("/bin/sh", "-c", "gzip -dc | cpio -iu")
command.Stdin = resp.Body
err := command.Run()
if err != nil {
log.Printf("ERROR: Failed running CPIO: %s\n", err)
}
}
func cleanUp() {
err := pidfile.Remove(PIDFile)
if err != nil {
wwlog.Error("could not remove pidfile: %s", err)
}
}