From 189618a111d641483cc353fac136ebaa6e90c8d4 Mon Sep 17 00:00:00 2001 From: Gregory Kurtzer Date: Tue, 10 Nov 2020 21:28:39 -0800 Subject: [PATCH] Major refactor for Cobra and new pkg/app structure for wwctl --- Makefile | 9 +- cmd/warewulfd/runtime.go | 2 +- cmd/warewulfd/system.go | 2 +- cmd/warewulfd/warewulfd.go | 4 +- cmd/wwbuild/overlay-runtime.go | 6 +- cmd/wwbuild/overlay-system.go | 2 +- cmd/wwbuild/vnfs-local.go | 44 ------ cmd/wwbuild/wwbuild.go | 9 +- cmd/wwclient/wwclient.go | 6 +- cmd/wwctl/main.go | 1 - internal/app/wwctl/build/build.go | 79 +++++++++++ internal/app/wwctl/build/kernel/kernel.go | 74 ++++++++++ internal/app/wwctl/build/root.go | 30 ++-- .../build/runtime-overlay/runtime-overlay.go | 133 ++++++++++++++++++ .../build/system-overlay/system-overlay.go | 133 ++++++++++++++++++ internal/app/wwctl/build/vnfs/containerdir.go | 50 +++++++ .../app/wwctl/build/vnfs/docker.go | 32 ++--- .../app/wwctl/build/vnfs/util.go | 7 +- internal/app/wwctl/build/vnfs/vnfs.go | 51 +++++++ internal/app/wwctl/root.go | 6 +- internal/pkg/assets/assets.go | 8 +- internal/pkg/util/util.go | 18 +++ internal/pkg/vnfs/vnfs.go | 52 +++++-- internal/pkg/wwlog/wwlog.go | 30 ++-- 24 files changed, 658 insertions(+), 130 deletions(-) delete mode 100644 cmd/wwbuild/vnfs-local.go create mode 100644 internal/app/wwctl/build/build.go create mode 100644 internal/app/wwctl/build/kernel/kernel.go create mode 100644 internal/app/wwctl/build/runtime-overlay/runtime-overlay.go create mode 100644 internal/app/wwctl/build/system-overlay/system-overlay.go create mode 100644 internal/app/wwctl/build/vnfs/containerdir.go rename cmd/wwbuild/vnfs-oci.go => internal/app/wwctl/build/vnfs/docker.go (66%) rename cmd/wwbuild/vnfs.go => internal/app/wwctl/build/vnfs/util.go (86%) create mode 100644 internal/app/wwctl/build/vnfs/vnfs.go diff --git a/Makefile b/Makefile index 9247e807..f792b785 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ lint: @echo Running golangci-lint... @$(GOLANGCI_LINT) run --build-tags "$(WW_BUILD_GO_BUILD_TAGS)" ./... -all: warewulfd wwbuild wwclient +all: warewulfd wwctl wwclient files: all install -d -m 0755 /var/warewulf/ @@ -67,8 +67,8 @@ services: files warewulfd: cd cmd/warewulfd; go build -mod vendor -o ../../warewulfd -wwbuild: - cd cmd/wwbuild; go build -mod vendor -tags "$(WW_BUILD_GO_BUILD_TAGS)" -o ../../wwbuild +wwctl: + cd cmd/wwctl; go build -mod vendor -tags "$(WW_BUILD_GO_BUILD_TAGS)" -o ../../wwctl wwclient: cd cmd/wwclient; CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -ldflags '-extldflags -static' -o ../../wwclient @@ -76,7 +76,6 @@ wwclient: clean: rm -f warewulfd rm -f wwbuild - rm -f wwclient + rm -f wwctl install: files services - diff --git a/cmd/warewulfd/runtime.go b/cmd/warewulfd/runtime.go index 7ee88283..4378e3d7 100644 --- a/cmd/warewulfd/runtime.go +++ b/cmd/warewulfd/runtime.go @@ -59,7 +59,7 @@ func runtimeOverlaySend(w http.ResponseWriter, req *http.Request) { } } else { w.WriteHeader(503) - log.Printf("ERROR: No 'runtime overlay' set for node %s\n", node.Fqdn) + log.Printf("ERROR: No 'runtime system-overlay' set for node %s\n", node.Fqdn) } return diff --git a/cmd/warewulfd/system.go b/cmd/warewulfd/system.go index 7efe428b..3007fdf8 100644 --- a/cmd/warewulfd/system.go +++ b/cmd/warewulfd/system.go @@ -27,7 +27,7 @@ func systemOverlaySend(w http.ResponseWriter, req *http.Request) { } } else { w.WriteHeader(503) - log.Printf("ERROR: No 'system overlay' set for node %s\n", node.Fqdn) + log.Printf("ERROR: No 'system system-overlay' set for node %s\n", node.Fqdn) } return diff --git a/cmd/warewulfd/warewulfd.go b/cmd/warewulfd/warewulfd.go index 1cf34ed0..b9b84552 100644 --- a/cmd/warewulfd/warewulfd.go +++ b/cmd/warewulfd/warewulfd.go @@ -66,8 +66,8 @@ func main() { http.HandleFunc("/kernel/", kernelSend) http.HandleFunc("/kmods/", kmodsSend) http.HandleFunc("/vnfs/", vnfsSend) - http.HandleFunc("/overlay-system/", systemOverlaySend) - http.HandleFunc("/overlay-runtime", runtimeOverlaySend) + http.HandleFunc("/system-overlay-system/", systemOverlaySend) + http.HandleFunc("/system-overlay-runtime", runtimeOverlaySend) http.ListenAndServe(":9873", nil) } diff --git a/cmd/wwbuild/overlay-runtime.go b/cmd/wwbuild/overlay-runtime.go index 6e077c87..1b738b01 100644 --- a/cmd/wwbuild/overlay-runtime.go +++ b/cmd/wwbuild/overlay-runtime.go @@ -19,7 +19,9 @@ func overlayRuntime(node assets.NodeInfo, replace map[string]string, wg *sync.Wa /* destModTime := time.Time{} destMod, err := os.Stat(OverlayFile) - if err == nil { + if err == n + + il { destModTime = destMod.ModTime() } configMod, err := os.Stat("/etc/warewulf/nodes.conf") @@ -43,7 +45,7 @@ func overlayRuntime(node assets.NodeInfo, replace map[string]string, wg *sync.Wa // if sourceModTime.After(destModTime) || configModTime.After(destModTime) { fmt.Printf("RUNTIME: %s\n", node.Fqdn) - overlayDest := "/tmp/.overlay-" + util.RandomString(16) + overlayDest := "/tmp/.system-overlay-" + util.RandomString(16) BuildOverlayDir(OverlayDir, overlayDest, replace) cmd := fmt.Sprintf("cd %s && find . | cpio --quiet -o -H newc -F \"%s\"", overlayDest, OverlayFile) diff --git a/cmd/wwbuild/overlay-system.go b/cmd/wwbuild/overlay-system.go index 0cfaca23..1fbb0f23 100644 --- a/cmd/wwbuild/overlay-system.go +++ b/cmd/wwbuild/overlay-system.go @@ -45,7 +45,7 @@ func overlaySystem(node assets.NodeInfo, replace map[string]string, wg *sync.Wai if sourceModTime.After(destModTime) || configModTime.After(destModTime) { fmt.Printf("SYSTEM: %s\n", node.Fqdn) - overlayDest := "/tmp/.overlay-" + util.RandomString(16) + overlayDest := "/tmp/.system-overlay-" + util.RandomString(16) BuildOverlayDir(OverlayDir, overlayDest, replace) cmd := fmt.Sprintf("cd %s && find . | cpio --quiet -o -H newc -F \"%s\"", overlayDest, OverlayFile) diff --git a/cmd/wwbuild/vnfs-local.go b/cmd/wwbuild/vnfs-local.go deleted file mode 100644 index 91e5babb..00000000 --- a/cmd/wwbuild/vnfs-local.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - "github.com/hpcng/warewulf/internal/pkg/vnfs" - "os" - "path" - "sync" -) - -func vnfsLocalBuild(vnfsPath string, wg *sync.WaitGroup) { - defer wg.Done() - v := vnfs.New(vnfsPath) - - if _, err := os.Stat(vnfsPath); err == nil { - // TODO: Build VNFS to temporary file and move to real location when complete atomically - // TODO: Check time stamps of sourcedir and build file to see if we need to rebuild or skip - - //vnfsDestination := fmt.Sprintf("%s/provision/vnfs/%s.img.gz", LocalStateDir, path.Base(vnfsPath)) - - fmt.Printf("Building local Container: %s\n", vnfsPath) - - err := os.MkdirAll(path.Dir(v.Image()), 0755) - if err != nil { - fmt.Printf("ERROR: %s\n", err) - return - } - - err = buildVnfs(vnfsPath, v.Image()) - if err != nil { - fmt.Printf("ERROR: %s\n", err) - os.Exit(1) - } - - err = buildLinks(v, vnfsPath) - if err != nil { - fmt.Printf("ERROR: %s\n", err) - os.Exit(1) - } - - } else { - fmt.Printf("SKIPPING VNFS: (bad path) %s\n", vnfsPath) - } -} diff --git a/cmd/wwbuild/wwbuild.go b/cmd/wwbuild/wwbuild.go index 8daa7e4f..bc4bcced 100644 --- a/cmd/wwbuild/wwbuild.go +++ b/cmd/wwbuild/wwbuild.go @@ -1,5 +1,10 @@ package main +import "os" + +import "os" + + import ( "fmt" "github.com/hpcng/warewulf/internal/pkg/assets" @@ -16,7 +21,7 @@ import ( func main() { if len(os.Args) < 2 { - fmt.Printf("USAGE: %s [vnfs/kernel/overlay] (node regex)\n", os.Args[0]) + fmt.Printf("USAGE: %s [vnfs/kernel/system-overlay] (node regex)\n", os.Args[0]) return } @@ -107,7 +112,7 @@ func main() { } } } - } else if os.Args[1] == "overlay" { + } else if os.Args[1] == "system-overlay" { var nodeList []assets.NodeInfo var wg sync.WaitGroup diff --git a/cmd/wwclient/wwclient.go b/cmd/wwclient/wwclient.go index fab560f3..0f71894b 100644 --- a/cmd/wwclient/wwclient.go +++ b/cmd/wwclient/wwclient.go @@ -19,7 +19,7 @@ func main() { 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("Runtime system-overlay is being put in '/warewulf/wwclient-test' rather than '/'\n") os.MkdirAll("/warewulf/wwclient-test", 0755) os.Chdir("/warewulf/wwclient-test") } @@ -61,7 +61,7 @@ func main() { for true { var err error - getString := fmt.Sprintf("http://%s:%d/overlay-runtime", config.Ipaddr, config.Port) + getString := fmt.Sprintf("http://%s:%d/system-overlay-runtime", config.Ipaddr, config.Port) resp, err = webclient.Get(getString) if err == nil { break @@ -84,7 +84,7 @@ func main() { if resp.StatusCode != 200 { - log.Printf("Not updating runtime overlay, got status code: %d\n", resp.StatusCode) + log.Printf("Not updating runtime system-overlay, got status code: %d\n", resp.StatusCode) time.Sleep(60000 * time.Millisecond) continue } diff --git a/cmd/wwctl/main.go b/cmd/wwctl/main.go index 8f72ea22..a8718e31 100644 --- a/cmd/wwctl/main.go +++ b/cmd/wwctl/main.go @@ -6,7 +6,6 @@ import ( func main() { - root := wwctl.GetRootCommand() root.Execute() diff --git a/internal/app/wwctl/build/build.go b/internal/app/wwctl/build/build.go new file mode 100644 index 00000000..323c6095 --- /dev/null +++ b/internal/app/wwctl/build/build.go @@ -0,0 +1,79 @@ +package build + +import ( + "github.com/hpcng/warewulf/internal/app/wwctl/build/kernel" + runtime_overlay "github.com/hpcng/warewulf/internal/app/wwctl/build/runtime-overlay" + system_overlay "github.com/hpcng/warewulf/internal/app/wwctl/build/system-overlay" + "github.com/hpcng/warewulf/internal/app/wwctl/build/vnfs" + "github.com/hpcng/warewulf/internal/pkg/assets" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "github.com/spf13/cobra" + "os" +) + + +func CobraRunE(cmd *cobra.Command, args []string) error { + var nodeList []assets.NodeInfo + + if buildAll == true { + wwlog.Printf(wwlog.VERBOSE, "Building all components\n") + buildVnfs = true + buildKernel = true + buildSystemOverlay = true; + buildRuntimeOverlay = true; + } + + if len(args) >= 1 { + nodeList, _ = assets.SearchByName(args[0]) + } else { + nodeList, _ = assets.FindAllNodes() + } + + if len(nodeList) == 0 { + wwlog.Printf(wwlog.ERROR, "No nodes found matching: '%s'\n", args[0]) + os.Exit(255) + } else { + wwlog.Printf(wwlog.VERBOSE, "Found matching nodes for build: %d\n", len(nodeList)) + } + + if buildVnfs == true { +// wwlog.Printf(wwlog.INFO, "===============================================================================\n") + + err := vnfs.Build(nodeList, buildForce) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + os.Exit(255) + } + } + + if buildKernel == true { +// wwlog.Printf(wwlog.INFO, "===============================================================================\n") + + err := kernel.Build(nodeList, buildForce) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + os.Exit(255) + } + } + + if buildSystemOverlay == true { +// wwlog.Printf(wwlog.INFO, "===============================================================================\n") + err := system_overlay.Build(nodeList, buildForce) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + os.Exit(255) + } + } + + if buildRuntimeOverlay == true { +// wwlog.Printf(wwlog.INFO, "===============================================================================\n") + err := runtime_overlay.Build(nodeList, buildForce) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + os.Exit(255) + } + } + + + return nil +} \ No newline at end of file diff --git a/internal/app/wwctl/build/kernel/kernel.go b/internal/app/wwctl/build/kernel/kernel.go new file mode 100644 index 00000000..3023264f --- /dev/null +++ b/internal/app/wwctl/build/kernel/kernel.go @@ -0,0 +1,74 @@ +package kernel + +import ( + "fmt" + "github.com/hpcng/warewulf/internal/pkg/assets" + "github.com/hpcng/warewulf/internal/pkg/config" + "github.com/hpcng/warewulf/internal/pkg/util" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "os" + "os/exec" + "path" +) + +const kernelProvisionPath = "/provision/kernel/" + +func Build(nodeList []assets.NodeInfo, force bool) error { + set := make(map[string]int) + + wwlog.Printf(wwlog.INFO, "Importing Kernels:\n") + wwlog.SetIndent(4) + + for _, node := range nodeList { + if node.KernelVersion != "" { + set[node.KernelVersion] ++ + wwlog.Printf(wwlog.DEBUG, "Node '%s' has KernelVersion '%s'\n", node.Fqdn, node.KernelVersion) + } + } + + for kernelVersion := range set { + kernelImage := "/boot/vmlinuz-"+kernelVersion + kernelDrivers := "/lib/modules/"+kernelVersion + kernelDestination := path.Join(config.LocalStateDir, kernelProvisionPath, "vmlinuz-"+kernelVersion) + driversDestination := path.Join(config.LocalStateDir, kernelProvisionPath, "kmods-"+kernelVersion+".img") + + + // Create the kernel destination path just in case it doesn't exist + os.MkdirAll(path.Join(config.LocalStateDir, kernelProvisionPath), 0755) + + if _, err := os.Stat(kernelImage); err == nil { + if util.PathIsNewer(kernelImage, kernelDestination) && force == false { + wwlog.Printf(wwlog.INFO, "%-35s: Skipping, kernel is current\n", "vmlinuz-"+kernelVersion) + + } else { + err := util.CopyFile(kernelImage, kernelDestination) + if err != nil { + wwlog.Printf(wwlog.ERROR, "Failed copying kernel image: %s\n", err) + continue + } + wwlog.Printf(wwlog.INFO, "%-35s: Done\n", "vmlinuz-"+kernelVersion) + + } + } + + if _, err := os.Stat(kernelDrivers); err == nil { + if util.PathIsNewer(kernelDrivers, driversDestination) && force == false { + wwlog.Printf(wwlog.INFO, "%-35s: Skipping, kernel Drivers are current\n", "kmods-"+kernelVersion+".img") + + } else { + cmd := fmt.Sprintf("cd /; find .%s | cpio --quiet -o -H newc -F \"%s\"", kernelDrivers, driversDestination) + err := exec.Command("/bin/sh", "-c", cmd).Run() + if err != nil { + wwlog.Printf(wwlog.ERROR, "Could not generate kernel driver overlay: %s\n", err) + continue + } + wwlog.Printf(wwlog.INFO, "%-35s: Done\n", "kmods-"+kernelVersion+".img") + } + } + + } + + wwlog.SetIndent(0) + + return nil +} \ No newline at end of file diff --git a/internal/app/wwctl/build/root.go b/internal/app/wwctl/build/root.go index 4eee8699..10244ce9 100644 --- a/internal/app/wwctl/build/root.go +++ b/internal/app/wwctl/build/root.go @@ -1,7 +1,6 @@ package build import ( - "fmt" "github.com/spf13/cobra" ) @@ -9,16 +8,26 @@ var ( buildCmd = &cobra.Command{ Use: "build", Short: "Warewulf build subcommand", - Long: "Warewulf build is used to build VNFS, kernel, and overlay objects for provisioning.", - RunE: buildRunE, + Long: "Warewulf build is used to build VNFS, kernel, and system-overlay objects for\n" + + "provisioning. The default usage will be to build and/or update anything\n" + + "that seems to be needed.", + RunE: CobraRunE, } - option string + buildVnfs bool + buildKernel bool + buildRuntimeOverlay bool + buildSystemOverlay bool + buildAll bool + buildForce bool ) func init() { - //buildCmd.PersistentFlags().StringVar(&flagHost, FlagHost, "localhost:7331", "Address of the Fuzzball Host.") - - buildCmd.PersistentFlags().StringVarP(&option, "testopt", "t", "default", "This is a test option") + buildCmd.PersistentFlags().BoolVarP(&buildVnfs, "vnfs", "V", false, "Build and/or update VNFS images.") + buildCmd.PersistentFlags().BoolVarP(&buildKernel, "kernel", "K", false, "Build and/or update Kernel images.") + buildCmd.PersistentFlags().BoolVarP(&buildRuntimeOverlay, "runtime", "R", false, "Build and/or update runtime overlays") + buildCmd.PersistentFlags().BoolVarP(&buildSystemOverlay, "system", "S", false, "Build and/or update system overlays") + buildCmd.PersistentFlags().BoolVarP(&buildAll, "all", "A", false, "Build and/or update all components") + buildCmd.PersistentFlags().BoolVarP(&buildForce, "force", "f", false, "Force build even if nothing has been updated.") } @@ -26,10 +35,3 @@ func init() { func GetCommand() *cobra.Command { return buildCmd } - -func buildRunE(cmd *cobra.Command, args []string) error { - fmt.Printf("Hello World. Got Option: %s, %s\n", option, args) - - return nil -} - diff --git a/internal/app/wwctl/build/runtime-overlay/runtime-overlay.go b/internal/app/wwctl/build/runtime-overlay/runtime-overlay.go new file mode 100644 index 00000000..6e553eee --- /dev/null +++ b/internal/app/wwctl/build/runtime-overlay/runtime-overlay.go @@ -0,0 +1,133 @@ +package runtime_overlay + +import ( + "fmt" + "github.com/hpcng/warewulf/internal/pkg/assets" + "github.com/hpcng/warewulf/internal/pkg/config" + "github.com/hpcng/warewulf/internal/pkg/util" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strings" + "text/template" +) + +func Build(nodeList []assets.NodeInfo, force bool) error { + wwlog.Printf(wwlog.INFO, "Building Runtime Overlays:\n") + wwlog.SetIndent(4) + + for _, node := range nodeList { + if node.RuntimeOverlay != "" { + OverlayDir := path.Join(config.LocalStateDir, "/overlays/runtime/", node.RuntimeOverlay) + OverlayFile := path.Join(config.LocalStateDir, "/provision/overlays/runtime/", node.Fqdn+".img") + + wwlog.Printf(wwlog.VERBOSE, "Building Runtime Overlay for: %s\n", node.Fqdn) + + tmpDir, err := ioutil.TempDir(os.TempDir(), ".wwctl-runtime-overlay-") + if err != nil { + return err + } + + if _, err := os.Stat(OverlayDir); err != nil { + return err + } + + err = os.MkdirAll(path.Dir(OverlayFile), 0755) + if err != nil { + return err + } + + if force == false { + wwlog.Printf(wwlog.DEBUG, "Checking if overlay is required\n") + } + if util.PathIsNewer(OverlayDir, OverlayFile) { + if force == false { + wwlog.Printf(wwlog.INFO, "%-35s: Skipping, overlay is current\n", node.Fqdn) + continue + } + } + + wwlog.Printf(wwlog.DEBUG, "Changing directory to OverlayDir: %s\n", OverlayDir) + err = os.Chdir(OverlayDir) + if err != nil { + wwlog.Printf(wwlog.ERROR, "Could not chdir() to OverlayDir: %s\n", OverlayDir) + continue + } + + wwlog.Printf(wwlog.DEBUG, "Walking the file system: %s\n", OverlayDir) + err = filepath.Walk(".", func(location string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + wwlog.Printf(wwlog.DEBUG, "Found directory: %s\n", location) + + err := os.MkdirAll(path.Join(tmpDir, location), info.Mode()) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + + } else if filepath.Ext(location) == ".ww" { + wwlog.Printf(wwlog.DEBUG, "Found template file: %s\n", location) + + destFile := strings.TrimSuffix(location, ".ww") + + tmpl, err := template.ParseGlob(path.Join(OverlayDir, destFile) + ".ww*") + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + + w, err := os.OpenFile(path.Join(tmpDir, destFile), os.O_RDWR|os.O_CREATE, info.Mode()) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + defer w.Close() + + err = tmpl.Execute(w, node) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + + } else if b, _ := regexp.MatchString(`\.ww[a-zA-Z0-9\-\._]*$`, location); b == true { + wwlog.Printf(wwlog.DEBUG, "Ignoring WW template file: %s\n", location) + } else { + wwlog.Printf(wwlog.DEBUG, "Found file: %s\n", location) + + err := util.CopyFile(path.Join(OverlayDir, location), path.Join(tmpDir, location)) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + + } + + return nil + }) + wwlog.Printf(wwlog.VERBOSE, "Finished generating overlay directory for: %s\n", node.Fqdn) + + cmd := fmt.Sprintf("cd \"%s\"; find . | cpio --quiet -o -H newc -F \"%s\"", tmpDir, OverlayFile) + wwlog.Printf(wwlog.DEBUG, "RUNNING: %s\n", cmd) + err = exec.Command("/bin/sh", "-c", cmd).Run() + if err != nil { + wwlog.Printf(wwlog.ERROR, "Could not generate runtime image overlay: %s\n", err) + continue + } + wwlog.Printf(wwlog.INFO, "%-3s: Done\n", node.Fqdn) + + wwlog.Printf(wwlog.DEBUG, "Removing temporary directory: %s\n", tmpDir) + os.RemoveAll(tmpDir) + } + } + + wwlog.SetIndent(0) + return nil +} diff --git a/internal/app/wwctl/build/system-overlay/system-overlay.go b/internal/app/wwctl/build/system-overlay/system-overlay.go new file mode 100644 index 00000000..e20d1e1c --- /dev/null +++ b/internal/app/wwctl/build/system-overlay/system-overlay.go @@ -0,0 +1,133 @@ +package system_overlay + +import ( + "fmt" + "github.com/hpcng/warewulf/internal/pkg/assets" + "github.com/hpcng/warewulf/internal/pkg/config" + "github.com/hpcng/warewulf/internal/pkg/util" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strings" + "text/template" +) + +func Build(nodeList []assets.NodeInfo, force bool) error { + wwlog.Printf(wwlog.INFO, "Building System Overlays:\n") + wwlog.SetIndent(4) + + for _, node := range nodeList { + if node.SystemOverlay != "" { + OverlayDir := path.Join(config.LocalStateDir, "/overlays/system/", node.SystemOverlay) + OverlayFile := path.Join(config.LocalStateDir, "/provision/overlays/system/", node.Fqdn+".img") + + wwlog.Printf(wwlog.VERBOSE, "Building System Overlay for: %s\n", node.Fqdn) + + tmpDir, err := ioutil.TempDir(os.TempDir(), ".wwctl-system-overlay-") + if err != nil { + return err + } + + if _, err := os.Stat(OverlayDir); err != nil { + return err + } + + err = os.MkdirAll(path.Dir(OverlayFile), 0755) + if err != nil { + return err + } + + if force == false { + wwlog.Printf(wwlog.DEBUG, "Checking if overlay is required\n") + } + if util.PathIsNewer(OverlayDir, OverlayFile) { + if force == false { + wwlog.Printf(wwlog.INFO, "%-35s: Skipping, overlay is current\n", node.Fqdn) + continue + } + } + + wwlog.Printf(wwlog.DEBUG, "Changing directory to OverlayDir: %s\n", OverlayDir) + err = os.Chdir(OverlayDir) + if err != nil { + wwlog.Printf(wwlog.ERROR, "Could not chdir() to OverlayDir: %s\n", OverlayDir) + continue + } + + wwlog.Printf(wwlog.DEBUG, "Walking the file system: %s\n", OverlayDir) + err = filepath.Walk(".", func(location string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + wwlog.Printf(wwlog.DEBUG, "Found directory: %s\n", location) + + err := os.MkdirAll(path.Join(tmpDir, location), info.Mode()) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + + } else if filepath.Ext(location) == ".ww" { + wwlog.Printf(wwlog.DEBUG, "Found template file: %s\n", location) + + destFile := strings.TrimSuffix(location, ".ww") + + tmpl, err := template.ParseGlob(path.Join(OverlayDir, destFile) + ".ww*") + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + + w, err := os.OpenFile(path.Join(tmpDir, destFile), os.O_RDWR|os.O_CREATE, info.Mode()) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + defer w.Close() + + err = tmpl.Execute(w, node) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + + } else if b, _ := regexp.MatchString(`\.ww[a-zA-Z0-9\-\._]*$`, location); b == true { + wwlog.Printf(wwlog.DEBUG, "Ignoring WW template file: %s\n", location) + } else { + wwlog.Printf(wwlog.DEBUG, "Found file: %s\n", location) + + err := util.CopyFile(path.Join(OverlayDir, location), path.Join(tmpDir, location)) + if err != nil { + wwlog.Printf(wwlog.ERROR, "%s\n", err) + return err + } + + } + + return nil + }) + wwlog.Printf(wwlog.VERBOSE, "Finished generating overlay directory for: %s\n", node.Fqdn) + + cmd := fmt.Sprintf("cd \"%s\"; find . | cpio --quiet -o -H newc -F \"%s\"", tmpDir, OverlayFile) + wwlog.Printf(wwlog.DEBUG, "RUNNING: %s\n", cmd) + err = exec.Command("/bin/sh", "-c", cmd).Run() + if err != nil { + wwlog.Printf(wwlog.ERROR, "Could not generate system image overlay: %s\n", err) + continue + } + wwlog.Printf(wwlog.INFO, "%-35s: Done\n", node.Fqdn) + + wwlog.Printf(wwlog.DEBUG, "Removing temporary directory: %s\n", tmpDir) + os.RemoveAll(tmpDir) + } + } + + wwlog.SetIndent(0) + return nil +} diff --git a/internal/app/wwctl/build/vnfs/containerdir.go b/internal/app/wwctl/build/vnfs/containerdir.go new file mode 100644 index 00000000..b3cb6be4 --- /dev/null +++ b/internal/app/wwctl/build/vnfs/containerdir.go @@ -0,0 +1,50 @@ +package vnfs + +import ( + "fmt" + "github.com/hpcng/warewulf/internal/pkg/util" + "github.com/hpcng/warewulf/internal/pkg/vnfs" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "os" + "path" +) + +func BuildContainerdir(v vnfs.VnfsObject) { + + if _, err := os.Stat(v.Source()); err != nil { + wwlog.Printf(wwlog.INFO, "%-35s: Skipping (bad path)\n", v.Name()) + return + } + + wwlog.Printf(wwlog.DEBUG, "Checking if there have been any updates to the VNFS directory\n") + if util.PathIsNewer(v.Source(), v.Image()) { + if buildForce == false { + wwlog.Printf(wwlog.INFO, "%-35s: Skipping, VNFS is current\n", v.Name()) + return + } + } + + wwlog.Printf(wwlog.DEBUG, "Making the directory: %s\n", path.Dir(v.Image())) + err := os.MkdirAll(path.Dir(v.Image()), 0755) + if err != nil { + fmt.Printf("ERROR: %s\n", err) + return + } + + wwlog.Printf(wwlog.DEBUG, "Building VNFS image: '%s' -> '%s'\n", v.Source(), v.Image()) + err = buildVnfs(v.Source(), v.Image()) + if err != nil { + fmt.Printf("ERROR: %s\n", err) + os.Exit(1) + } + + wwlog.Printf(wwlog.DEBUG, "Building links for Warewulf access to chroot\n") + err = buildLinks(v, v.Source()) + if err != nil { + fmt.Printf("ERROR: %s\n", err) + os.Exit(1) + } + + wwlog.Printf(wwlog.INFO, "%-35s: Done\n", v.Name()) + +} diff --git a/cmd/wwbuild/vnfs-oci.go b/internal/app/wwctl/build/vnfs/docker.go similarity index 66% rename from cmd/wwbuild/vnfs-oci.go rename to internal/app/wwctl/build/vnfs/docker.go index 399fbc0c..79ea8825 100644 --- a/cmd/wwbuild/vnfs-oci.go +++ b/internal/app/wwctl/build/vnfs/docker.go @@ -1,4 +1,4 @@ -package main +package vnfs import ( "context" @@ -6,10 +6,9 @@ import ( "github.com/hpcng/warewulf/internal/pkg/config" "github.com/hpcng/warewulf/internal/pkg/oci" "github.com/hpcng/warewulf/internal/pkg/vnfs" - "log" + "github.com/hpcng/warewulf/internal/pkg/wwlog" "os" "path" - "sync" ) const ( @@ -17,21 +16,17 @@ const ( VnfsHashDir = config.LocalStateDir + "/oci/vnfs/hash/" ) -func vnfsOciBuild(OciPath string, wg *sync.WaitGroup) { - v := vnfs.New(OciPath) - - // vnfsDestination := fmt.Sprintf("%s/provision/vnfs/%s.img.gz", LocalStateDir, path.Base(OciPath)) - defer wg.Done() - - log.Printf("Building OCI Container: %s\n", OciPath) +func BuildDocker(v vnfs.VnfsObject) { + wwlog.Printf(wwlog.VERBOSE, "Building OCI Container: %s\n", v.Source()) c, err := oci.NewCache(oci.OptSetCachePath(OciCacheDir)) if err != nil { fmt.Printf("ERROR: %s\n", err) os.Exit(1) } - log.Printf("Downloading OCI container layers\n") - sourcePath, err := c.Pull(context.Background(), OciPath, nil) + + wwlog.Printf(wwlog.VERBOSE, "Downloading OCI container layers\n") + sourcePath, err := c.Pull(context.Background(), v.Source(), nil) if err != nil { fmt.Printf("ERROR: %s\n", err) os.Exit(1) @@ -42,7 +37,7 @@ func vnfsOciBuild(OciPath string, wg *sync.WaitGroup) { name, err := os.Readlink(v.Image()) if err == nil { if name == hashDestination { - log.Printf("Container already built, no update available\n") + wwlog.Printf(wwlog.INFO, "%-35s: Skipping, VNFS is current\n", v.Name()) return } } @@ -63,7 +58,7 @@ func vnfsOciBuild(OciPath string, wg *sync.WaitGroup) { return } - log.Printf("Building bootable VNFS image\n") + wwlog.Printf(wwlog.VERBOSE, "Building bootable VNFS image\n") err = buildVnfs(sourcePath, hashDestination) if err != nil { @@ -71,23 +66,24 @@ func vnfsOciBuild(OciPath string, wg *sync.WaitGroup) { os.Exit(1) } - log.Printf("Finalizing Build\n") + wwlog.Printf(wwlog.VERBOSE, "Finalizing Build\n") _ = os.Remove(v.Image() + "-link") err = os.Symlink(hashDestination, v.Image()+"-link") if err != nil { - fmt.Printf("ERROR: %s\n", err) + wwlog.Printf(wwlog.ERROR, "%s\n", err) os.Exit(1) } err = os.Rename(v.Image()+"-link", v.Image()) err = buildLinks(v, sourcePath) if err != nil { - fmt.Printf("ERROR: %s\n", err) + wwlog.Printf(wwlog.ERROR, "%s\n", err) os.Exit(1) } - log.Printf("Completed building VNFS: %s\n", path.Base(OciPath)) + wwlog.Printf(wwlog.INFO, "%-35s: Done\n", v.Name()) return } + diff --git a/cmd/wwbuild/vnfs.go b/internal/app/wwctl/build/vnfs/util.go similarity index 86% rename from cmd/wwbuild/vnfs.go rename to internal/app/wwctl/build/vnfs/util.go index f9ab7f67..8bd44e3d 100644 --- a/cmd/wwbuild/vnfs.go +++ b/internal/app/wwctl/build/vnfs/util.go @@ -1,10 +1,12 @@ -package main +package vnfs + import ( "fmt" "github.com/hpcng/warewulf/internal/pkg/vnfs" "os" "os/exec" + "path" ) func buildVnfs(source string, dest string) error { @@ -20,6 +22,9 @@ func buildLinks(v vnfs.VnfsObject, source string) error { // Just incase the temporary link location is present, remove it if we can _ = os.Remove(v.Root() + "-link") + // Just incase the directory doesn't exist, make it + _ = os.MkdirAll(path.Dir(v.Root()), 0755) + // Link to a temporary location so we can atomically move the link into place err := os.Symlink(source, v.Root()+"-link") if err != nil { diff --git a/internal/app/wwctl/build/vnfs/vnfs.go b/internal/app/wwctl/build/vnfs/vnfs.go new file mode 100644 index 00000000..aa044208 --- /dev/null +++ b/internal/app/wwctl/build/vnfs/vnfs.go @@ -0,0 +1,51 @@ +package vnfs + +import ( + "github.com/hpcng/warewulf/internal/pkg/assets" + "github.com/hpcng/warewulf/internal/pkg/vnfs" + "github.com/hpcng/warewulf/internal/pkg/wwlog" + "strings" +) + +var buildForce bool + +func Build(nodeList []assets.NodeInfo, force bool) (error) { + set := make(map[string]int) + + wwlog.Printf(wwlog.INFO, "Importing VNFS Images:\n") + wwlog.SetIndent(4) + + buildForce = force + + for _, node := range nodeList { + if node.Vnfs != "" { + set[node.Vnfs] ++ + wwlog.Printf(wwlog.DEBUG, "Node '%s' has VNFS '%s'\n", node.Fqdn, node.Vnfs) + } + } + + for uri := range set { + v := vnfs.New(uri) + wwlog.Printf(wwlog.VERBOSE, "VNFS found: %s (nodes: %d)\n", uri, set[uri]) + if strings.HasPrefix(uri, "docker://") { + BuildDocker(v) + + } else if strings.HasPrefix(uri, "docker-daemon://") { + //wwlog.Printf(wwlog.INFO, "Building VNFS from Docker service: %s\n", uri) + wwlog.Printf(wwlog.INFO, "Building VNFS from Docker service is not supported yet: %s\n", uri) + + } else if strings.HasPrefix(uri, "/") { + if strings.HasSuffix(uri, "tar.gz") { + //wwlog.Printf(wwlog.WARN, "Building VNFS from local tarball: %s\n", uri) + wwlog.Printf(wwlog.WARN, "Building VNFS from local tarball is not supported yet: %s\n", uri) + + } else { + BuildContainerdir(v) + } + } + } + + wwlog.SetIndent(0) + + return nil +} \ No newline at end of file diff --git a/internal/app/wwctl/root.go b/internal/app/wwctl/root.go index f0b736fb..bd214392 100644 --- a/internal/app/wwctl/root.go +++ b/internal/app/wwctl/root.go @@ -9,16 +9,14 @@ import ( var ( rootCmd = &cobra.Command{ Use: "wwctl", - Short: "Warewulf CTL", - Long: "Fuzzball CLI is an application for interacting with a Fuzzball Service.", + Short: "Warewulf Control", + Long: "Control interface to the Cluster Warewulf Provisioning System.", PersistentPreRunE: rootPersistentPreRunE, } verboseArg bool debugArg bool ) - - func init() { rootCmd.PersistentFlags().BoolVarP(&verboseArg, "verbose", "v", false, "Run with increased verbosity.") rootCmd.PersistentFlags().BoolVarP(&debugArg, "debug", "d", false, "Run with debugging messages enabled.") diff --git a/internal/pkg/assets/assets.go b/internal/pkg/assets/assets.go index eb08c4a3..6c19acdf 100644 --- a/internal/pkg/assets/assets.go +++ b/internal/pkg/assets/assets.go @@ -31,8 +31,8 @@ type nodeGroup struct { Comment string Vnfs string Ipxe string `yaml:"ipxe template"` - SystemOverlay string `yaml:"system overlay""` - RuntimeOverlay string `yaml:"runtime overlay""` + SystemOverlay string `yaml:"system system-overlay""` + RuntimeOverlay string `yaml:"runtime system-overlay""` DomainSuffix string `yaml:"domain suffix"` KernelVersion string `yaml:"kernel version"` Nodes map[string]nodeEntry @@ -42,8 +42,8 @@ type nodeEntry struct { Hostname string Vnfs string Ipxe string `yaml:"ipxe template"` - SystemOverlay string `yaml:"system overlay"` - RuntimeOverlay string `yaml:"runtime overlay"` + SystemOverlay string `yaml:"system system-overlay"` + RuntimeOverlay string `yaml:"runtime system-overlay"` DomainSuffix string `yaml:"domain suffix"` KernelVersion string `yaml:"kernel version"` IpmiIpaddr string `yaml:"ipmi ipaddr"` diff --git a/internal/pkg/util/util.go b/internal/pkg/util/util.go index 0fdfca2d..52a3e017 100644 --- a/internal/pkg/util/util.go +++ b/internal/pkg/util/util.go @@ -1,6 +1,7 @@ package util import ( + "github.com/hpcng/warewulf/internal/pkg/wwlog" "io" "math/rand" "os" @@ -31,6 +32,22 @@ func DirModTime(path string) (time.Time, error) { return lastTime, nil } +func PathIsNewer(source string, compare string) bool { + time1, err := DirModTime(source) + if err != nil { + wwlog.Printf(wwlog.DEBUG, "%s\n", err) + return false + } + + time2, err := DirModTime(compare) + if err != nil { + wwlog.Printf(wwlog.DEBUG, "%s\n", err) + return false + } + + return time1.Before(time2) +} + func RandomString(n int) string { var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") @@ -42,6 +59,7 @@ func RandomString(n int) string { } func CopyFile(source string, dest string) error { + wwlog.Printf(wwlog.DEBUG, "Copying '%s' to '%s'\n", source, dest) sourceFD, err := os.Open(source) if err != nil { return err diff --git a/internal/pkg/vnfs/vnfs.go b/internal/pkg/vnfs/vnfs.go index 5d7c01ea..2868280e 100644 --- a/internal/pkg/vnfs/vnfs.go +++ b/internal/pkg/vnfs/vnfs.go @@ -7,45 +7,67 @@ import ( ) type VnfsObject struct { - Source string - RootPath string - ImagePath string + SourcePath string + RootPath string + ImagePath string } func New(s string) VnfsObject { var ret VnfsObject - ret.Source = s + ret.SourcePath = s return ret } + func (self *VnfsObject) Name() string { - if self.Source == "" { + if self.SourcePath == "" { return "" } - if strings.HasPrefix(self.Source, "/") { - return path.Base(self.Source) + if strings.HasPrefix(self.SourcePath, "/") { + return path.Base(self.SourcePath) } - uri := strings.Split(self.Source, "://") - - return strings.ReplaceAll(uri[0]+":"+uri[1], "/", "_") + return self.SourcePath } -func (self *VnfsObject) Image() string { - if self.Source == "" { +func (self *VnfsObject) NameClean() string { + if self.SourcePath == "" { return "" } - return config.LocalStateDir + "/provision/vnfs/" + self.Name() + ".img.gz" + if strings.HasPrefix(self.SourcePath, "/") { + return path.Base(self.SourcePath) + } + + uri := strings.Split(self.SourcePath, "://") + + return strings.ReplaceAll(uri[1], "/", "_") +} + +func (self *VnfsObject) Source() string { + if self.SourcePath == "" { + return "" + } + + return self.SourcePath +} + + +func (self *VnfsObject) Image() string { + if self.SourcePath == "" { + return "" + } + + return config.LocalStateDir + "/provision/vnfs/" + self.NameClean() + ".img.gz" } func (self *VnfsObject) Root() string { - if self.Source == "" { + if self.SourcePath == "" { return "" } - return config.LocalStateDir + "/chroots/" + self.Name() + return config.LocalStateDir + "/chroots/" + self.NameClean() } diff --git a/internal/pkg/wwlog/wwlog.go b/internal/pkg/wwlog/wwlog.go index 44feb1a8..14f36204 100644 --- a/internal/pkg/wwlog/wwlog.go +++ b/internal/pkg/wwlog/wwlog.go @@ -2,6 +2,7 @@ package wwlog import ( "log" + "strings" ) const ( @@ -15,32 +16,33 @@ const ( var ( logLevel uint + Indent string ) func SetLevel(level uint) { logLevel = level - if logLevel == DEBUG { - log.SetFlags(log.Lmicroseconds | log.Llongfile) - } else { - log.SetFlags(0) - } - Printf(VERBOSE, "Set log level to: %d\n", logLevel) + log.SetFlags(0) + Printf(DEBUG, "Set log level to: %d\n", logLevel) +} + +func SetIndent(i int) { + Indent = strings.Repeat(" ", i) } func prefixLevel(level uint) { if level == DEBUG { - log.SetPrefix("[DEBUG] ") + log.SetPrefix("[DEBUG] "+Indent) } else if level == VERBOSE { - log.SetPrefix("[VERBOSE] ") + log.SetPrefix("[VERBOSE] "+Indent) } else if level == INFO { - log.SetPrefix("[INFO] ") + log.SetPrefix("[INFO] "+Indent) } else if level == WARN { - log.SetPrefix("[WARN] ") + log.SetPrefix("[WARN] "+Indent) } else if level == ERROR { - log.SetPrefix("[ERROR] ") + log.SetPrefix("[ERROR] "+Indent) } else if level == CRITICAL { - log.SetPrefix("[CRITICAL] ") + log.SetPrefix("[CRITICAL] "+Indent) } } @@ -49,6 +51,8 @@ func Println(level uint, message string) { prefixLevel(level) log.Println(message) } + + log.SetPrefix("[LOG] "+Indent) } func Printf(level uint, message string, a...interface{}) { @@ -56,4 +60,6 @@ func Printf(level uint, message string, a...interface{}) { prefixLevel(level) log.Printf(message, a...) } + + log.SetPrefix("[LOG] "+Indent) }