basic function comparing uids of host/container

This commit is contained in:
Christian Goll
2022-02-28 15:17:33 +01:00
committed by Christian Goll
parent 95050a44ce
commit 8c7fd7d140
7 changed files with 330 additions and 9 deletions

View File

@@ -128,7 +128,13 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
wwlog.Printf(wwlog.WARN, "Could not copy /etc/resolv.conf into container: %s\n", err) 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) fmt.Printf("Building container: %s\n", name)
err = container.Build(name, true) err = container.Build(name, true)
if err != nil { if err != nil {

View File

@@ -5,20 +5,20 @@ import "github.com/spf13/cobra"
var ( var (
baseCmd = &cobra.Command{ baseCmd = &cobra.Command{
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Use: "import [OPTIONS] SOURCE [NAME]", Use: "import [OPTIONS] SOURCE [NAME]",
Short: "Import a container into Warewulf", Short: "Import a container into Warewulf",
Long: Long: `This command will pull and import a container into Warewulf from SOURCE,
`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. optionally renaming it to NAME. The SOURCE must be in a supported URI format.
Imported containers are used to create bootable VNFS images.`, Imported containers are used to create bootable VNFS images.`,
Example: "wwctl container import docker://warewulf/centos-8 my_container", Example: "wwctl container import docker://warewulf/centos-8 my_container",
RunE: CobraRunE, RunE: CobraRunE,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
} }
SetForce bool SetForce bool
SetUpdate bool SetUpdate bool
SetBuild bool SetBuild bool
SetDefault bool SetDefault bool
NoSyncUser bool
) )
func init() { func init() {
@@ -26,6 +26,7 @@ func init() {
baseCmd.PersistentFlags().BoolVarP(&SetUpdate, "update", "u", false, "Update and overwrite an existing container") 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().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(&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. // GetRootCommand returns the root cobra.Command for the application.

View File

@@ -8,6 +8,7 @@ import (
"github.com/hpcng/warewulf/internal/app/wwctl/container/list" "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/shell"
"github.com/hpcng/warewulf/internal/app/wwctl/container/show" "github.com/hpcng/warewulf/internal/app/wwctl/container/show"
"github.com/hpcng/warewulf/internal/app/wwctl/container/syncuser"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -31,6 +32,7 @@ func init() {
baseCmd.AddCommand(shell.GetCommand()) baseCmd.AddCommand(shell.GetCommand())
baseCmd.AddCommand(delete.GetCommand()) baseCmd.AddCommand(delete.GetCommand())
baseCmd.AddCommand(show.GetCommand()) baseCmd.AddCommand(show.GetCommand())
baseCmd.AddCommand(syncuser.GetCommand())
} }

View File

@@ -21,8 +21,7 @@ More information about the conainer can be shown with the '-a' option.`,
return list, cobra.ShellCompDirectiveNoFileComp return list, cobra.ShellCompDirectiveNoFileComp
}, },
Aliases: []string{"sh", "chroot"}, Args: cobra.MinimumNArgs(1),
Args: cobra.MinimumNArgs(1),
} }
ShowAll bool ShowAll bool
) )

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,8 +1,16 @@
package container package container
import ( import (
"bufio"
"fmt"
"io/fs"
"io/ioutil" "io/ioutil"
"os" "os"
"path"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -70,3 +78,250 @@ func DeleteSource(name string) error {
wwlog.Printf(wwlog.VERBOSE, "Removing path: %s\n", fullPath) wwlog.Printf(wwlog.VERBOSE, "Removing path: %s\n", fullPath)
return os.RemoveAll(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
}