From 8c7fd7d140556e34940a7433fe6cd3b192edaee4 Mon Sep 17 00:00:00 2001 From: Christian Goll Date: Mon, 28 Feb 2022 15:17:33 +0100 Subject: [PATCH] basic function comparing uids of host/container --- internal/app/wwctl/container/imprt/main.go | 8 +- internal/app/wwctl/container/imprt/root.go | 13 +- internal/app/wwctl/container/root.go | 2 + internal/app/wwctl/container/show/root.go | 3 +- internal/app/wwctl/container/syncuser/main.go | 23 ++ internal/app/wwctl/container/syncuser/root.go | 35 +++ internal/pkg/container/util.go | 255 ++++++++++++++++++ 7 files changed, 330 insertions(+), 9 deletions(-) create mode 100644 internal/app/wwctl/container/syncuser/main.go create mode 100644 internal/app/wwctl/container/syncuser/root.go diff --git a/internal/app/wwctl/container/imprt/main.go b/internal/app/wwctl/container/imprt/main.go index b91a5bd5..24c465f8 100644 --- a/internal/app/wwctl/container/imprt/main.go +++ b/internal/app/wwctl/container/imprt/main.go @@ -128,7 +128,13 @@ func CobraRunE(cmd *cobra.Command, args []string) error { if err != nil { wwlog.Printf(wwlog.WARN, "Could not copy /etc/resolv.conf into container: %s\n", err) } - + if !NoSyncUser { + err = container.SyncUids(name) + if err != nil { + wwlog.Printf(wwlog.ERROR, "Error in user sync, fix error and run 'syncuser' manually: %s\n", err) + os.Exit(1) + } + } fmt.Printf("Building container: %s\n", name) err = container.Build(name, true) if err != nil { diff --git a/internal/app/wwctl/container/imprt/root.go b/internal/app/wwctl/container/imprt/root.go index 061c0efe..ee9c960b 100644 --- a/internal/app/wwctl/container/imprt/root.go +++ b/internal/app/wwctl/container/imprt/root.go @@ -5,20 +5,20 @@ import "github.com/spf13/cobra" var ( baseCmd = &cobra.Command{ DisableFlagsInUseLine: true, - Use: "import [OPTIONS] SOURCE [NAME]", - Short: "Import a container into Warewulf", - Long: -`This command will pull and import a container into Warewulf from SOURCE, + Use: "import [OPTIONS] SOURCE [NAME]", + Short: "Import a container into Warewulf", + Long: `This command will pull and import a container into Warewulf from SOURCE, optionally renaming it to NAME. The SOURCE must be in a supported URI format. Imported containers are used to create bootable VNFS images.`, Example: "wwctl container import docker://warewulf/centos-8 my_container", - RunE: CobraRunE, - Args: cobra.MinimumNArgs(1), + RunE: CobraRunE, + Args: cobra.MinimumNArgs(1), } SetForce bool SetUpdate bool SetBuild bool SetDefault bool + NoSyncUser bool ) func init() { @@ -26,6 +26,7 @@ func init() { baseCmd.PersistentFlags().BoolVarP(&SetUpdate, "update", "u", false, "Update and overwrite an existing container") baseCmd.PersistentFlags().BoolVarP(&SetBuild, "build", "b", false, "Build container when after pulling") baseCmd.PersistentFlags().BoolVar(&SetDefault, "setdefault", false, "Set this container for the default profile") + baseCmd.PersistentFlags().BoolVar(&NoSyncUser, "nosyncuser", false, "Don't synchronize uis/gods from host to container") } // GetRootCommand returns the root cobra.Command for the application. diff --git a/internal/app/wwctl/container/root.go b/internal/app/wwctl/container/root.go index 64034cb7..363f82b5 100644 --- a/internal/app/wwctl/container/root.go +++ b/internal/app/wwctl/container/root.go @@ -8,6 +8,7 @@ import ( "github.com/hpcng/warewulf/internal/app/wwctl/container/list" "github.com/hpcng/warewulf/internal/app/wwctl/container/shell" "github.com/hpcng/warewulf/internal/app/wwctl/container/show" + "github.com/hpcng/warewulf/internal/app/wwctl/container/syncuser" "github.com/spf13/cobra" ) @@ -31,6 +32,7 @@ func init() { baseCmd.AddCommand(shell.GetCommand()) baseCmd.AddCommand(delete.GetCommand()) baseCmd.AddCommand(show.GetCommand()) + baseCmd.AddCommand(syncuser.GetCommand()) } diff --git a/internal/app/wwctl/container/show/root.go b/internal/app/wwctl/container/show/root.go index 92db694b..ddd5a9c0 100644 --- a/internal/app/wwctl/container/show/root.go +++ b/internal/app/wwctl/container/show/root.go @@ -21,8 +21,7 @@ More information about the conainer can be shown with the '-a' option.`, return list, cobra.ShellCompDirectiveNoFileComp }, - Aliases: []string{"sh", "chroot"}, - Args: cobra.MinimumNArgs(1), + Args: cobra.MinimumNArgs(1), } ShowAll bool ) diff --git a/internal/app/wwctl/container/syncuser/main.go b/internal/app/wwctl/container/syncuser/main.go new file mode 100644 index 00000000..6e270ed8 --- /dev/null +++ b/internal/app/wwctl/container/syncuser/main.go @@ -0,0 +1,23 @@ +package syncuser + +import ( + "fmt" + "os" + + "github.com/hpcng/warewulf/internal/pkg/container" + "github.com/spf13/cobra" +) + +func CobraRunE(cmd *cobra.Command, args []string) error { + containerName := args[0] + if !container.ValidName(containerName) { + return fmt.Errorf("%s is not a valid container", containerName) + } + err := container.SyncUids(containerName) + if err != nil { + fmt.Sprint(err) + os.Exit(1) + } + + return nil +} diff --git a/internal/app/wwctl/container/syncuser/root.go b/internal/app/wwctl/container/syncuser/root.go new file mode 100644 index 00000000..08ba2a74 --- /dev/null +++ b/internal/app/wwctl/container/syncuser/root.go @@ -0,0 +1,35 @@ +package syncuser + +import ( + "github.com/hpcng/warewulf/internal/pkg/container" + "github.com/spf13/cobra" +) + +var ( + baseCmd = &cobra.Command{ + DisableFlagsInUseLine: true, + Use: "syncuser [OPTIONS] CONTAINER", + Short: "Synchronizes user in container", + Long: `Synchronize the uids and gids from the host to the container. +Users/groups which are only present in the container will be preserved if no +uid/gid collision is detected. File ownerships are also changed.`, + RunE: CobraRunE, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + list, _ := container.ListSources() + return list, cobra.ShellCompDirectiveNoFileComp + }, + + Args: cobra.MinimumNArgs(1), + } +) + +func init() { +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetCommand() *cobra.Command { + return baseCmd +} diff --git a/internal/pkg/container/util.go b/internal/pkg/container/util.go index c132459b..d57c31bf 100644 --- a/internal/pkg/container/util.go +++ b/internal/pkg/container/util.go @@ -1,8 +1,16 @@ package container import ( + "bufio" + "fmt" + "io/fs" "io/ioutil" "os" + "path" + "path/filepath" + "strconv" + "strings" + "syscall" "github.com/pkg/errors" @@ -70,3 +78,250 @@ func DeleteSource(name string) error { wwlog.Printf(wwlog.VERBOSE, "Removing path: %s\n", fullPath) return os.RemoveAll(fullPath) } + +type completeUserInfo struct { + Name string + UidHost int `access:"r,w"` + GidHost int `access:"r,w"` + UidCont int `access:"r,w"` + GidCont int `access:"r,w"` + FileListUid []string `access:"r,w"` + FileListGid []string `access:"r,w"` +} + +type simpleUserInfo struct { + name string + uid int + gid int +} + +/* +sync the uids,gids from the host to the container +*/ +func SyncUids(name string) error { + var userDb []completeUserInfo + passwdName := "/etc/passwd" + groupName := "/etc/group" + fullPath := RootFsDir(name) + hostName, err := createPasswdMap(passwdName) + if err != nil { + wwlog.Printf(wwlog.ERROR, "Could not open "+passwdName) + return err + } + // populate db with the user of the + for _, user := range hostName { + userDb = append(userDb, completeUserInfo{Name: user.name, + UidHost: user.uid, GidHost: user.gid, UidCont: -1, GidCont: -1}) + } + + contName, err := createPasswdMap(path.Join(fullPath, passwdName)) + if err != nil { + wwlog.Printf(wwlog.ERROR, "Could not open "+path.Join(fullPath, passwdName)) + return err + } + for _, userCont := range contName { + foundUser := false + for idxHost, userHost := range userDb { + if userCont.name == userHost.Name { + foundUser = true + (&userDb[idxHost]).UidCont = userCont.uid + (&userDb[idxHost]).GidCont = userCont.gid + } + } + if !foundUser { + userDb = append(userDb, completeUserInfo{Name: userCont.name, + UidHost: -1, GidHost: -1, UidCont: userCont.uid, GidCont: userCont.gid}) + wwlog.Printf(wwlog.WARN, "user: %s:%v:%v not present on host", userCont.name, userCont.uid, userCont.gid) + } + + } + // find out which user/goup are only in the container + var userOnlyCont []string + for _, user := range userDb { + if user.UidHost == -1 { + userOnlyCont = append(userOnlyCont, user.Name) + for _, userCheck := range userDb { + if userCheck.UidHost == user.UidCont { + wwlog.Printf(wwlog.WARN, fmt.Sprintf("uid(%v) collision for host: %s and container: %s\n", + user.UidCont, user.Name, userCheck.Name)) + return errors.New(fmt.Sprintf("user %s only present in container has same uid(%v) as user %s on host,\n"+ + "add this user to /etc/passwd on host", user.Name, user.UidCont, userCheck.Name)) + } + } + } + if user.GidHost == -1 { + for _, userCheck := range userDb { + if userCheck.GidHost == user.GidCont { + wwlog.Printf(wwlog.WARN, fmt.Sprintf("gid(%v) collision for host: %s and container: %s\n", + user.GidCont, user.Name, userCheck.Name)) + return errors.New(fmt.Sprintf("user %s only present in container has same gid(%v) as user %s on host,\n"+ + " add this group to /etc/group on host", user.Name, user.GidCont, userCheck.Name)) + } + } + } + + } + // create list of files which need changed ownerships in order to change them later what + // avoid uid/gid collisions + for idx, user := range userDb { + if (user.UidHost != user.UidCont && user.UidCont != -1) || (user.GidHost != user.GidCont && user.GidCont != -1) { + wwlog.Printf(wwlog.VERBOSE, fmt.Sprintf("host %s:%v:%v <-> container %s:%v:%v\n", + user.Name, user.UidHost, user.GidHost, user.Name, user.UidCont, user.GidCont)) + err = filepath.Walk(fullPath, func(filePath string, info fs.FileInfo, err error) error { + // root is always good, if we failt to get UID/GID of a file + var uid, gid int + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + uid = int(stat.Uid) + gid = int(stat.Gid) + } + if uid == user.UidCont { + (&userDb[idx]).FileListUid = append((&userDb[idx]).FileListUid, filePath) + } + if gid == user.GidCont { + (&userDb[idx]).FileListGid = append((&userDb[idx]).FileListGid, filePath) + } + return nil + }) + } + } + // change uids and gid of file + for _, user := range userDb { + if len(user.FileListUid) != 0 { + fmt.Printf("uidList(%s): %v\n", user.Name, user.FileListUid) + for _, file := range user.FileListUid { + fsInfo, err := os.Stat(file) + if err != nil { + return err + } + var gid int + if stat, ok := fsInfo.Sys().(*syscall.Stat_t); ok { + gid = int(stat.Gid) + } + wwlog.Printf(wwlog.DEBUG, "%s chown(%v,%v)\n", file, user.UidHost, gid) + err = os.Chown(file, user.UidHost, gid) + if err != nil { + return err + } + } + } + if len(user.FileListGid) != 0 { + fmt.Printf("gidList(%s): %v\n", user.Name, user.FileListGid) + for _, file := range user.FileListGid { + fsInfo, err := os.Stat(file) + if err != nil { + return err + } + var uid int + if stat, ok := fsInfo.Sys().(*syscall.Stat_t); ok { + uid = int(stat.Uid) + } + wwlog.Printf(wwlog.DEBUG, "%s chown(%v,%v)\n", file, user.UidHost, uid) + err = os.Chown(file, uid, user.GidHost) + if err != nil { + return err + } + } + + } + + } + // get the entries for the passwd/group file before copy over + passwdEntries, err := getEntires(path.Join(fullPath, passwdName), userOnlyCont) + if err != nil { + return err + } + // implicitly assuming that users/groups which only exists on the host have the same name + groupEntries, err := getEntires(path.Join(fullPath, groupName), userOnlyCont) + if err != nil { + return err + } + if err = util.CopyFile(passwdName, path.Join(fullPath, passwdName)); err != nil { + return err + } + if err = util.CopyFile(groupName, path.Join(fullPath, groupName)); err != nil { + return err + } + if err = appendLines(passwdName, passwdEntries); err != nil { + return err + } + if err = appendLines(groupName, groupEntries); err != nil { + return err + } + return nil + +} + +/* +creates simple user db []simpleUserInfo for a /etc/{passwd|group} file +*/ +func createPasswdMap(fileName string) ([]simpleUserInfo, error) { + var nameDb []simpleUserInfo + file, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer file.Close() + fileScanner := bufio.NewScanner(file) + for fileScanner.Scan() { + line := fileScanner.Text() + entries := strings.Split(line, ":") + name := entries[0] + uid, err := strconv.Atoi(entries[2]) + if err != nil { + wwlog.Printf(wwlog.WARN, "could not parse uid(%s) for %s\n", entries[2], name) + } + gid, err := strconv.Atoi(entries[3]) + if err != nil { + wwlog.Printf(wwlog.WARN, "could not parse gid(%s) for %s\n", entries[2], name) + } + if name != "" { + nameDb = append(nameDb, simpleUserInfo{name: name, uid: uid, gid: gid}) + + } + } + wwlog.Printf(wwlog.DEBUG, fmt.Sprintf("created uid/gid map with %v entries from %s\n", len(nameDb), fileName)) + return nameDb, nil +} + +/* +Creates a slice with the entries of of passwd for the given slice of user names +*/ +func getEntires(fileName string, names []string) ([]string, error) { + file, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer file.Close() + var entries []string + fileScanner := bufio.NewScanner(file) + for fileScanner.Scan() { + line := fileScanner.Text() + entries := strings.Split(line, ":") + for _, name := range names { + if entries[0] == name { + entries = append(entries, line) + } + } + } + return entries, nil + +} + +/* +Appending the lines to the given file +*/ +func appendLines(fileName string, lines []string) error { + wwlog.Printf(wwlog.VERBOSE, "appending %v lines to %s\n", len(lines), fileName) + file, err := os.OpenFile(fileName, os.O_APPEND, 0644) + if err != nil { + return err + } + defer file.Close() + for _, line := range lines { + if _, err := file.WriteString(line); err != nil { + return err + } + + } + return nil +}