Files
warewulf/internal/app/wwclient/root.go
Tobias Poschwatta 971d9e2118 Add option for wwclient port number
With this change, you can specify a port number for wwclient.
If

  wwclient:
    port: NUMBER

is specified in warewulf.conf, wwclient will bind to the specified local
port NUMBER. If no port is specified, wwclient will use any available
port, or port 987 if secure is true.

Port 987 is in the default port range used by the Linux NFS client
(665-1023, see linux/include/linux/sunrpc/xprtsock.h). Changing
the port can avoid failures when port 987 is already in use.

Signed-off-by: Tobias Poschwatta <poschwatta@zib.de>
2024-08-02 11:09:16 +02:00

259 lines
7.2 KiB
Go

package wwclient
import (
"errors"
"fmt"
"log"
"net"
"net/http"
"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 := 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)
}
}