Files
warewulf/internal/app/wwctl/container/exec/main.go
Christian Goll dc263425e2 Added cow option to bind
The option cow can now be set for files which are then
mounted during exec, but copied into the image and removed
if not modified.

Signed-off-by: Christian Goll <cgoll@suse.com>
2024-09-09 11:09:04 +02:00

225 lines
7.0 KiB
Go

//go:build linux
// +build linux
package exec
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"syscall"
"time"
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/container"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
func runChildCmd(cmd *cobra.Command, args []string) error {
child := exec.Command("/proc/self/exe", args...)
child.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}
child.Stdin = cmd.InOrStdin()
child.Stdout = cmd.OutOrStdout()
child.Stderr = cmd.ErrOrStderr()
return child.Run()
}
var childCommandFunc = runChildCmd
// Fork a child process with a new PID space
func runContainedCmd(cmd *cobra.Command, containerName string, args []string) (err error) {
wwlog.Debug("runContainedCmd:args: %v", args)
conf := warewulfconf.Get()
runDir := container.RunDir(containerName)
if err := os.Mkdir(runDir, 0750); err != nil {
if _, existerr := os.Stat(runDir); !os.IsNotExist(existerr) {
return errors.New("run directory already exists: another container command may already be running")
} else {
return fmt.Errorf("unable to create run directory: %w", err)
}
}
defer func() {
if err := os.RemoveAll(runDir); err != nil {
wwlog.Error("error removing run directory: %w", err)
}
}()
logStr := fmt.Sprint(wwlog.GetLogLevel())
childArgs := []string{"--warewulfconf", conf.GetWarewulfConf(), "--loglevel", logStr, "container", "exec", "__child"}
childArgs = append(childArgs, containerName)
for _, b := range binds {
childArgs = append(childArgs, "--bind", b)
}
if nodeName != "" {
childArgs = append(childArgs, "--node", nodeName)
}
childArgs = append(childArgs, args...)
// copy the files into the container at this stage, es in __child the
// command syscall.Exec which replaces the __child process with the
// exec command in the container. All the mounts, have to be done in
// __child so that the used mounts don't propagate outside on the host
// (see the CLONE attributes), but as for the cow copy option we need
// to see if a file was modified after it was copied into the container
// so do this here.
// At first read out conf, the parse commandline, as copy files has the
// same synatx as mount points
mountPts := conf.MountsContainer
mountPts = append(container.InitMountPnts(binds), mountPts...)
filesToCpy := getCopyFiles(containerName, mountPts)
for i, cpyFile := range filesToCpy {
if err = util.CopyFile(cpyFile.Src, path.Join(container.RootFsDir(containerName), cpyFile.FileName)); err != nil {
return fmt.Errorf("couldn't copy files into container: %w", err)
}
// we can ignore error as the file was copied
stat, _ := os.Stat(path.Join(container.RootFsDir(containerName), cpyFile.FileName))
filesToCpy[i].ModTime = stat.ModTime()
}
wwlog.Verbose("Running contained command: %s", childArgs)
retVal := childCommandFunc(cmd, childArgs)
for _, cpyFile := range filesToCpy {
if modStat, err := os.Stat(path.Join(container.RootFsDir(containerName), cpyFile.FileName)); err != nil {
wwlog.Info("copied file was removed: %s", err)
} else {
if modStat.ModTime() == cpyFile.ModTime {
if err := os.Remove(path.Join(container.RootFsDir(containerName), cpyFile.FileName)); err != nil {
wwlog.Warn("couldn't remove copied file: %s", err)
}
}
}
}
return retVal
}
func CobraRunE(cmd *cobra.Command, args []string) error {
wwlog.Debug("CobraRunE:args: %v", args)
containerName := args[0]
wwlog.Debug("CobraRunE:containerName: %v", containerName)
if !container.ValidSource(containerName) {
wwlog.Error("Unknown Warewulf container: %s", containerName)
os.Exit(1)
}
os.Setenv("WW_CONTAINER_SHELL", containerName)
containerPath := container.RootFsDir(containerName)
beforePasswdTime := getTime(path.Join(containerPath, "/etc/passwd"))
wwlog.Debug("passwdTime: %v", beforePasswdTime)
beforeGroupTime := getTime(path.Join(containerPath, "/etc/group"))
wwlog.Debug("groupTime: %v", beforeGroupTime)
err := runContainedCmd(cmd, containerName, args[1:])
if err != nil {
wwlog.Error("Failed executing container command: %s", err)
os.Exit(1)
}
if util.IsFile(path.Join(containerPath, "/etc/warewulf/container_exit.sh")) {
wwlog.Verbose("Found clean script: /etc/warewulf/container_exit.sh")
err = runContainedCmd(cmd, containerName, []string{"/bin/sh", "/etc/warewulf/container_exit.sh"})
if err != nil {
wwlog.Error("Failed executing exit script: %s", err)
os.Exit(1)
}
}
userdbChanged := false
if !beforePasswdTime.IsZero() {
afterPasswdTime := getTime(path.Join(containerPath, "/etc/passwd"))
wwlog.Debug("passwdTime: %v", afterPasswdTime)
if beforePasswdTime.Before(afterPasswdTime) {
if !SyncUser {
wwlog.Warn("/etc/passwd has been modified, maybe you want to run syncuser")
}
userdbChanged = true
}
}
if !beforeGroupTime.IsZero() {
afterGroupTime := getTime(path.Join(containerPath, "/etc/group"))
wwlog.Debug("groupTime: %v", afterGroupTime)
if beforeGroupTime.Before(afterGroupTime) {
if !SyncUser {
wwlog.Warn("/etc/group has been modified, maybe you want to run syncuser")
}
userdbChanged = true
}
}
if userdbChanged && SyncUser {
err = container.SyncUids(containerName, false)
if err != nil {
wwlog.Error("Error in user sync, fix error and run 'syncuser' manually, but trying to build container: %s", err)
}
}
fmt.Printf("Rebuilding container...\n")
err = container.Build(containerName, false)
if err != nil {
wwlog.Error("Could not build container %s: %s", containerName, err)
os.Exit(1)
}
return nil
}
func getTime(path string) time.Time {
if fileStat, err := os.Stat(path); err != nil {
return time.Time{}
} else {
unixStat := fileStat.Sys().(*syscall.Stat_t)
return time.Unix(int64(unixStat.Ctim.Sec), int64(unixStat.Ctim.Nsec))
}
}
func SetBinds(myBinds []string) {
binds = append(binds, myBinds...)
}
func SetNode(myNode string) {
nodeName = myNode
}
// file name and last modification time so we can remove the file if it wasn't modified
type cowFile struct {
FileName string
Src string
ModTime time.Time
Cow bool
}
/*
Check the objects we want to copy in, instead of mounting
*/
func getCopyFiles(containerNamer string, binds []*warewulfconf.MountEntry) (copyObjects []cowFile) {
for _, bind := range binds {
if !bind.Cow || bind.ReadOnly {
continue
}
if _, err := os.Stat(path.Join(container.RootFsDir(containerNamer), path.Dir(bind.Dest))); err != nil {
wwlog.Warn("destination directory doesn't exist: %s", err)
continue
}
if _, err := os.Stat(path.Join(container.RootFsDir(containerNamer), bind.Dest)); err == nil {
wwlog.Verbose("file exists in container: %s", bind.Dest)
continue
}
if _, err := os.Stat(bind.Source); err != nil {
wwlog.Warn("source doesn't exist: %s", err)
continue
}
copyObjects = append(copyObjects, cowFile{
FileName: bind.Dest,
Src: bind.Source,
Cow: bind.Cow,
})
}
return
}