diff --git a/cmd/wwclient/main.go b/cmd/wwclient/main.go new file mode 100644 index 00000000..335a6b1c --- /dev/null +++ b/cmd/wwclient/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "os" + + "github.com/hpcng/warewulf/internal/app/wwclient" +) + +func main() { + + root := wwclient.GetRootCommand() + + err := root.Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) + if wwclient.DebugFlag { + fmt.Printf("\nSTACK TRACE: %+v\n", err) + } + os.Exit(255) + } +} diff --git a/cmd/wwclient/wwclient.go b/cmd/wwclient/wwclient.go deleted file mode 100644 index b51c9aca..00000000 --- a/cmd/wwclient/wwclient.go +++ /dev/null @@ -1,140 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "log" - "net" - "net/http" - "os" - "os/exec" - "strings" - "time" - - "github.com/hpcng/warewulf/internal/pkg/warewulfconf" - "github.com/hpcng/warewulf/internal/pkg/wwlog" - "github.com/talos-systems/go-smbios/smbios" -) - -func main() { - if os.Args[0] == "/warewulf/bin/wwclient" { - err := os.Chdir("/") - if err != nil { - wwlog.Printf(wwlog.ERROR, "failed to change dir: %s", err) - 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") - err := os.MkdirAll("/warewulf/wwclient-test", 0755) - if err != nil { - wwlog.Printf(wwlog.ERROR, "failed to create dir: %s", err) - os.Exit(1) - } - - err = os.Chdir("/warewulf/wwclient-test") - if err != nil { - wwlog.Printf(wwlog.ERROR, "failed to change dir: %s", err) - os.Exit(1) - } - } - - conf, err := warewulfconf.New() - if err != nil { - wwlog.Printf(wwlog.ERROR, "Could not get Warewulf configuration: %s\n", err) - os.Exit(1) - } - - localTCPAddr := net.TCPAddr{} - if conf.Warewulf.Secure { - // Setup local port to something privileged (<1024) - localTCPAddr.Port = 987 - wwlog.Printf(wwlog.INFO, "Running from trusted port\n") - } - - 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, - }, - } - - smbiosDump, err := smbios.New() - if err != nil { - wwlog.Printf(wwlog.ERROR, "Could not get SMBIOS info: %s\n", err) - os.Exit(1) - } - sysinfoDump := smbiosDump.SystemInformation() - localUUID, _ := sysinfoDump.UUID() - x := smbiosDump.SystemEnclosure() - tag := x.AssetTagNumber() - - cmdline, err := ioutil.ReadFile("/proc/cmdline") - if err != nil { - wwlog.Printf(wwlog.ERROR, "Could not read from /proc/cmdline: %s\n", err) - os.Exit(1) - } - - wwid_tmp := strings.Split(string(cmdline), "wwid=") - if len(wwid_tmp) < 2 { - wwlog.Printf(wwlog.ERROR, "'wwid' is not defined in /proc/cmdline\n") - os.Exit(1) - } - - wwid := strings.Split(wwid_tmp[1], " ")[0] - - for { - var resp *http.Response - counter := 0 - - for { - var err error - - getString := fmt.Sprintf("http://%s:%d/overlay-runtime/%s?assetkey=%s&uuid=%s", conf.Ipaddr, conf.Warewulf.Port, wwid, tag, localUUID) - 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) - continue - } - - 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) - } - - if conf.Warewulf.UpdateInterval > 0 { - time.Sleep(time.Duration(conf.Warewulf.UpdateInterval*1000) * time.Millisecond) - } else { - time.Sleep(30000 * time.Millisecond * 1000) - } - } -} diff --git a/etc/ipxe/default.ipxe b/etc/ipxe/default.ipxe index 32868718..a23fcd4b 100644 --- a/etc/ipxe/default.ipxe +++ b/etc/ipxe/default.ipxe @@ -15,9 +15,8 @@ kernel --name kernel ${base}/kernel/{{.Hwaddr}}?assetkey=${asset}&uuid=${uuid} initrd --name container ${base}/container/{{.Hwaddr}}?assetkey=${asset}&uuid=${uuid} || goto reboot initrd --name kmods ${base}/kmods/{{.Hwaddr}}?assetkey=${asset}&uuid=${uuid} || goto reboot initrd --name system ${base}/overlay-system/{{.Hwaddr}}?assetkey=${asset}&uuid=${uuid} || goto reboot -initrd --name runtime ${base}/overlay-runtime/{{.Hwaddr}}?assetkey=${asset}&uuid=${uuid} || goto reboot -boot kernel initrd=container initrd=kmods initrd=system initrd=runtime wwid={{.Hwaddr}} {{.KernelArgs}} || goto reboot +boot kernel initrd=container initrd=kmods initrd=system wwid={{.Hwaddr}} {{.KernelArgs}} || goto reboot :reboot echo diff --git a/go.mod b/go.mod index bdda8a46..08cc3e1c 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,10 @@ require ( github.com/brotherpowers/ipsubnet v0.0.0-20170914094241-30bc98f0a5b1 github.com/containers/image/v5 v5.7.0 github.com/containers/storage v1.30.0 + github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e github.com/creasty/defaults v1.5.2 github.com/fatih/color v1.13.0 + github.com/google/uuid v1.1.2 github.com/manifoldco/promptui v0.8.0 github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6 github.com/opencontainers/umoci v0.4.6 diff --git a/go.sum b/go.sum index 36920bdd..7d0741a2 100644 --- a/go.sum +++ b/go.sum @@ -202,6 +202,7 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= diff --git a/include/systemd/warewulfd.service.in b/include/systemd/warewulfd.service.in index 20bd6367..a343a0df 100644 --- a/include/systemd/warewulfd.service.in +++ b/include/systemd/warewulfd.service.in @@ -5,7 +5,7 @@ After=network-online.target AssertFileIsExecutable=@BINDIR@/wwctl [Service] -Type=forking +Type=simple User=root Group=root diff --git a/internal/app/wwclient/root.go b/internal/app/wwclient/root.go new file mode 100644 index 00000000..fdca4cef --- /dev/null +++ b/internal/app/wwclient/root.go @@ -0,0 +1,213 @@ +package wwclient + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "os" + "os/exec" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/google/uuid" + "github.com/talos-systems/go-smbios/smbios" + "github.com/coreos/go-systemd/daemon" + "github.com/hpcng/warewulf/internal/pkg/pidfile" + "github.com/hpcng/warewulf/internal/pkg/warewulfconf" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "github.com/spf13/cobra" +) + +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 +) + +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") + +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetRootCommand() *cobra.Command { + // Run cobra + return rootCmd +} + +func CobraRunE(cmd *cobra.Command, args []string) error { + conf, err := warewulfconf.New() + if err != nil { + return err + } + + pid, err := pidfile.Write(PIDFile) + if err != nil && pid == -1 { + wwlog.Printf(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] == "/warewulf/bin/wwclient" { + err := os.Chdir("/") + if err != nil { + wwlog.Printf(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") + err := os.MkdirAll("/warewulf/wwclient-test", 0755) + if err != nil { + wwlog.Printf(wwlog.ERROR, "failed to create dir: %s", err) + _ = os.Remove(PIDFile) + os.Exit(1) + } + + err = os.Chdir("/warewulf/wwclient-test") + if err != nil { + wwlog.Printf(wwlog.ERROR, "failed to change dir: %s", err) + _ = os.Remove(PIDFile) + os.Exit(1) + } + } + + localTCPAddr := net.TCPAddr{} + if conf.Warewulf.Secure { + // Setup local port to something privileged (<1024) + localTCPAddr.Port = 987 + wwlog.Printf(wwlog.INFO, "Running from trusted port\n") + } + + 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, + }, + } + smbiosDump, err := smbios.New() + if err != nil { + wwlog.Printf(wwlog.ERROR, "Could not get SMBIOS info: %s\n", err) + os.Exit(1) + } + sysinfoDump := smbiosDump.SystemInformation() + localUUID, _ := sysinfoDump.UUID() + x := smbiosDump.SystemEnclosure() + tag := x.AssetTagNumber() + + cmdline, err := ioutil.ReadFile("/proc/cmdline") + if err != nil { + wwlog.Printf(wwlog.ERROR, "Could not read from /proc/cmdline: %s\n", err) + os.Exit(1) + } + + wwid_tmp := strings.Split(string(cmdline), "wwid=") + if len(wwid_tmp) < 2 { + wwlog.Printf(wwlog.ERROR, "'wwid' is not defined in /proc/cmdline\n") + os.Exit(1) + } + + wwid := strings.Split(wwid_tmp[1], " ")[0] + + + 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.Printf(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/overlay-runtime/%s?assetkey=%s&uuid=%s", ipaddr, port, wwid, tag, localUUID) + 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.Printf(wwlog.ERROR, "could not remove pidfile: %s\n", err) + } +} diff --git a/internal/pkg/overlay/overlay.go b/internal/pkg/overlay/overlay.go index 06104b40..49342927 100644 --- a/internal/pkg/overlay/overlay.go +++ b/internal/pkg/overlay/overlay.go @@ -262,6 +262,16 @@ func BuildOverlay(nodeInfo node.NodeInfo, overlayName string) error { // } else if b, _ := regexp.MatchString(`\.ww[a-zA-Z0-9\-\._]*$`, location); b { // wwlog.Printf(wwlog.DEBUG, "Ignoring WW template file: %s\n", location) + } else if info.Mode()&os.ModeSymlink == os.ModeSymlink { + wwlog.Printf(wwlog.DEBUG, "Found symlink %s\n", location) + destination, err := os.Readlink(location) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + } + err = os.Symlink(destination, path.Join(tmpDir, location)) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + } } else { err := util.CopyFile(location, path.Join(tmpDir, location)) diff --git a/internal/pkg/pidfile/pidfile.go b/internal/pkg/pidfile/pidfile.go new file mode 100644 index 00000000..9541ea99 --- /dev/null +++ b/internal/pkg/pidfile/pidfile.go @@ -0,0 +1,84 @@ +package pidfile + +// based on original work found here, github.com/soellman/pidfile + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "strconv" + "strings" + "syscall" +) + +var ( + ErrProcessRunning = errors.New("process is running") + ErrFileStale = errors.New("pidfile exists but process is not running") + ErrFileInvalid = errors.New("pidfile has invalid contents") +) + +// Remove a pidfile +func Remove(filename string) error { + return os.RemoveAll(filename) +} + +// Write writes a pidfile, returning an error +// if the process is already running or pidfile is orphaned +func Write(filename string) (int, error) { + return WriteControl(filename, os.Getpid(), false) +} + +func WriteControl(filename string, pid int, overwrite bool) (int, error) { + // Check for existing pid + oldpid, err := pidfileContents(filename) + if err != nil && !os.IsNotExist(err) { + return oldpid, err + } + + // We have a pid + if err == nil { + if pidIsRunning(oldpid) { + return oldpid, ErrProcessRunning + } + if !overwrite { + return -1, ErrFileStale + } + } + + // We're clear to (over)write the file + return pid, ioutil.WriteFile(filename, []byte(fmt.Sprintf("%d\n", pid)), 0644) +} + +func pidfileContents(filename string) (int, error) { + contents, err := ioutil.ReadFile(filename) + if err != nil { + return 0, err + } + + pid, err := strconv.Atoi(strings.TrimSpace(string(contents))) + if err != nil { + return 0, ErrFileInvalid + } + + return pid, nil +} + +func pidIsRunning(pid int) bool { + process, err := os.FindProcess(pid) + if err != nil { + return false + } + + err = process.Signal(syscall.Signal(0)) + + if err != nil && err.Error() == "no such process" { + return false + } + + if err != nil && err.Error() == "os: process already finished" { + return false + } + + return true +} diff --git a/overlays/wwinit/etc/systemd/system/multi-user.target.wants/wwclient.service b/overlays/wwinit/etc/systemd/system/multi-user.target.wants/wwclient.service new file mode 120000 index 00000000..84f22df4 --- /dev/null +++ b/overlays/wwinit/etc/systemd/system/multi-user.target.wants/wwclient.service @@ -0,0 +1 @@ +/etc/systemd/system/wwclient.service \ No newline at end of file diff --git a/overlays/wwinit/etc/systemd/system/wwclient.service b/overlays/wwinit/etc/systemd/system/wwclient.service new file mode 100644 index 00000000..4cc81927 --- /dev/null +++ b/overlays/wwinit/etc/systemd/system/wwclient.service @@ -0,0 +1,14 @@ +[Unit] +Description=Warewulf node runtime overlay update +After=network.target +After=local-fs.target + +[Service] +Type=notify +ExecStart=/warewulf/bin/wwclient +ExecReload=/bin/kill -s SIGHUP "$MAINPID" +PIDFile=/var/run/wwclient.pid +TimeoutSec=60 + +[Install] +WantedBy=multi-user.target diff --git a/overlays/wwinit/warewulf/init.d/80-wwclient b/overlays/wwinit/warewulf/init.d/80-wwclient index 184dac4e..d0945766 100644 --- a/overlays/wwinit/warewulf/init.d/80-wwclient +++ b/overlays/wwinit/warewulf/init.d/80-wwclient @@ -1,6 +1,7 @@ #!/bin/sh . /warewulf/config - +# Only start if the systemd is no available +test -e /usr/lib/systemd/systemd && exit 0 echo "Starting wwclient" nohup /warewulf/bin/wwclient >/var/log/wwclient.log 2>&1