Rename "container" to "image"
- Updated `wwctl upgrade` to handle updates - Maintained `.Container` and `.ContainerName` in tstruct - Added `ContainerName()` methods to node and profile objects - Added `--container`, `-C` aliases to wwctl commands (`<node|profile> <add|set>`) - Added `wwctl container` alias - Added support for `container_exit.sh` if `image_exit.sh` is not found Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
52
internal/pkg/image/build.go
Normal file
52
internal/pkg/image/build.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/warewulf/warewulf/internal/pkg/util"
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
|
||||
func Build(name string, buildForce bool) error {
|
||||
wwlog.Info("Building image: %s", name)
|
||||
|
||||
rootfsPath := RootFsDir(name)
|
||||
imagePath := ImageFile(name)
|
||||
|
||||
if !ValidSource(name) {
|
||||
return errors.Errorf("Image does not exist: %s", name)
|
||||
}
|
||||
|
||||
if !buildForce {
|
||||
wwlog.Debug("Checking if there have been any updates to the image source directory")
|
||||
if util.PathIsNewer(rootfsPath, imagePath) {
|
||||
wwlog.Info("Skipping (Image is current)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
ignore := []string{}
|
||||
excludes_file := path.Join(rootfsPath, "./etc/warewulf/excludes")
|
||||
if util.IsFile(excludes_file) {
|
||||
var err error
|
||||
ignore, err = util.ReadFile(excludes_file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed creating directory: %s: %w", imagePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
err := util.BuildFsImage(
|
||||
"Image "+name,
|
||||
rootfsPath,
|
||||
imagePath,
|
||||
[]string{"*"},
|
||||
ignore,
|
||||
// ignore cross-device files
|
||||
true,
|
||||
"newc")
|
||||
|
||||
return err
|
||||
}
|
||||
33
internal/pkg/image/config.go
Normal file
33
internal/pkg/image/config.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
)
|
||||
|
||||
func SourceParentDir() string {
|
||||
conf := warewulfconf.Get()
|
||||
return conf.Paths.WWChrootdir
|
||||
}
|
||||
|
||||
func SourceDir(name string) string {
|
||||
return path.Join(SourceParentDir(), name)
|
||||
}
|
||||
|
||||
func RootFsDir(name string) string {
|
||||
return path.Join(SourceDir(name), "rootfs")
|
||||
}
|
||||
|
||||
func RunDir(name string) string {
|
||||
return path.Join(SourceDir(name), "run")
|
||||
}
|
||||
|
||||
func ImageParentDir() string {
|
||||
conf := warewulfconf.Get()
|
||||
return path.Join(conf.Paths.WWProvisiondir, "image")
|
||||
}
|
||||
|
||||
func ImageFile(name string) string {
|
||||
return path.Join(ImageParentDir(), name+".img")
|
||||
}
|
||||
80
internal/pkg/image/imprt.go
Normal file
80
internal/pkg/image/imprt.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage/drivers/copy"
|
||||
"github.com/containers/storage/pkg/reexec"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/oci"
|
||||
"github.com/warewulf/warewulf/internal/pkg/util"
|
||||
)
|
||||
|
||||
func ImportDocker(uri string, name string, sCtx *types.SystemContext) error {
|
||||
OciBlobCacheDir := warewulfconf.Get().Paths.OciBlobCachedir()
|
||||
|
||||
err := os.MkdirAll(OciBlobCacheDir, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ValidName(name) {
|
||||
return errors.New("Image name contains illegal characters: " + name)
|
||||
}
|
||||
|
||||
fullPath := RootFsDir(name)
|
||||
|
||||
err = os.MkdirAll(fullPath, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p, err := oci.NewPuller(
|
||||
oci.OptSetBlobCachePath(OciBlobCacheDir),
|
||||
oci.OptSetSystemContext(sCtx),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := p.GenerateID(context.Background(), uri); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.Pull(context.Background(), uri, fullPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ImportDirectory(uri string, name string) error {
|
||||
fullPath := RootFsDir(name)
|
||||
|
||||
err := os.MkdirAll(fullPath, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !util.IsDir(uri) {
|
||||
return errors.New("Import directory does not exist: " + uri)
|
||||
}
|
||||
|
||||
if !util.IsFile(path.Join(uri, "/bin/sh")) {
|
||||
return errors.New("Source directory has no /bin/sh: " + uri)
|
||||
}
|
||||
if reexec.Init() {
|
||||
return errors.New("couldn't init reexec")
|
||||
}
|
||||
err = copy.DirCopy(uri, fullPath, copy.Content, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
55
internal/pkg/image/imprt_test.go
Normal file
55
internal/pkg/image/imprt_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||
"github.com/warewulf/warewulf/internal/pkg/util"
|
||||
)
|
||||
|
||||
func Test_ImportImageDir(t *testing.T) {
|
||||
var tests = map[string]struct {
|
||||
files []string
|
||||
sockets []string
|
||||
}{
|
||||
"empty image": {
|
||||
files: nil,
|
||||
sockets: nil,
|
||||
},
|
||||
"sockets": {
|
||||
files: nil,
|
||||
sockets: []string{
|
||||
"/tmp/testsocket",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
src := "/tmp/testImage"
|
||||
env.CreateFile(filepath.Join(src, "/bin/sh"))
|
||||
for _, file := range tt.files {
|
||||
env.CreateFile(filepath.Join(src, file))
|
||||
}
|
||||
for _, socket := range tt.sockets {
|
||||
env.MkdirAll(filepath.Dir(filepath.Join(src, socket)))
|
||||
assert.NoError(t, unix.Mknod(env.GetPath(filepath.Join(src, socket)), unix.S_IFSOCK|0777, 0))
|
||||
}
|
||||
assert.NoError(t, ImportDirectory(env.GetPath(src), "testImage"))
|
||||
rootfs := "/var/lib/warewulf/chroots/testImage/rootfs"
|
||||
for _, file := range tt.files {
|
||||
assert.True(t, util.IsFile(env.GetPath(filepath.Join(rootfs, file))))
|
||||
}
|
||||
for _, socket := range tt.sockets {
|
||||
assert.True(t, util.IsFile(env.GetPath(filepath.Join(rootfs, socket))))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
87
internal/pkg/image/initramfs.go
Normal file
87
internal/pkg/image/initramfs.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
|
||||
var (
|
||||
initramfsSearchPaths = []string{
|
||||
"/boot/initramfs-*",
|
||||
"/boot/initrd-*",
|
||||
}
|
||||
|
||||
versionPattern *regexp.Regexp
|
||||
)
|
||||
|
||||
func init() {
|
||||
versionPattern = regexp.MustCompile(`\d+\.\d+\.\d+(-[\d\.]+|)`)
|
||||
}
|
||||
|
||||
type Initramfs struct {
|
||||
Path string
|
||||
imageName string
|
||||
}
|
||||
|
||||
func (this *Initramfs) version() *version.Version {
|
||||
matches := versionPattern.FindAllString(this.Path, -1)
|
||||
for i := len(matches) - 1; i >= 0; i-- {
|
||||
if version_, err := version.NewVersion(strings.TrimSuffix(matches[i], ".")); err == nil {
|
||||
return version_
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Initramfs) Version() string {
|
||||
version := this.version()
|
||||
if version == nil {
|
||||
return ""
|
||||
} else {
|
||||
return version.String()
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Initramfs) FullPath() string {
|
||||
root := RootFsDir(this.imageName)
|
||||
return filepath.Join(root, this.Path)
|
||||
}
|
||||
|
||||
func FindInitramfsFromPattern(imageName string, version string, pattern string) (initramfs *Initramfs) {
|
||||
wwlog.Debug("FindInitramfsFromPattern(%v, %v, %v)", imageName, version, pattern)
|
||||
root := RootFsDir(imageName)
|
||||
fullPaths, err := filepath.Glob(filepath.Join(root, pattern))
|
||||
wwlog.Debug("%v: fullPaths: %v", filepath.Join(root, pattern), fullPaths)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, fullPath := range fullPaths {
|
||||
path, err := filepath.Rel(root, fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
} else {
|
||||
initramfs := &Initramfs{Path: filepath.Join("/", path), imageName: imageName}
|
||||
wwlog.Info("%v", initramfs)
|
||||
if strings.HasPrefix(initramfs.Version(), version) {
|
||||
return initramfs
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindInitramfs returns the Initramfs for a given image and (kernel) version
|
||||
func FindInitramfs(imageName string, version string) *Initramfs {
|
||||
for _, pattern := range initramfsSearchPaths {
|
||||
initramfs := FindInitramfsFromPattern(imageName, version, pattern)
|
||||
if initramfs != nil {
|
||||
return initramfs
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
82
internal/pkg/image/initramfs_test.go
Normal file
82
internal/pkg/image/initramfs_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||
)
|
||||
|
||||
func TestFindInitramfs(t *testing.T) {
|
||||
conf := warewulfconf.Get()
|
||||
temp, err := os.MkdirTemp(os.TempDir(), "ww-conf-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(temp)
|
||||
conf.Paths.WWChrootdir = temp
|
||||
|
||||
assert.NoError(t, os.MkdirAll(filepath.Join(RootFsDir("image"), "boot"), 0700))
|
||||
|
||||
tests := map[string]struct {
|
||||
name string
|
||||
initramfs []string
|
||||
ver string
|
||||
path string
|
||||
}{
|
||||
"ok case 1": {
|
||||
initramfs: []string{"/boot/initramfs-1.1.1.aarch64.img"},
|
||||
ver: "1.1.1",
|
||||
path: "/boot/initramfs-1.1.1.aarch64.img",
|
||||
},
|
||||
"ok case 2": {
|
||||
initramfs: []string{"/boot/initrd-1.1.1.aarch64"},
|
||||
ver: "1.1.1",
|
||||
path: "/boot/initrd-1.1.1.aarch64",
|
||||
},
|
||||
"ok case 3": {
|
||||
initramfs: []string{"/boot/initramfs-1.1.1.aarch64"},
|
||||
ver: "1.1.1",
|
||||
path: "/boot/initramfs-1.1.1.aarch64",
|
||||
},
|
||||
"ok case 4": {
|
||||
initramfs: []string{"/boot/initrd-1.1.1.aarch64.img"},
|
||||
ver: "1.1.1",
|
||||
path: "/boot/initrd-1.1.1.aarch64.img",
|
||||
},
|
||||
"prefix match": {
|
||||
initramfs: []string{"/boot/initrd-1.1.1.aarch64.img"},
|
||||
ver: "1.1",
|
||||
path: "/boot/initrd-1.1.1.aarch64.img",
|
||||
},
|
||||
"error case, wrong init name": {
|
||||
initramfs: []string{"/boot/initrr-1.1.1.aarch64.img"},
|
||||
ver: "1.1.1",
|
||||
path: "",
|
||||
},
|
||||
"error case, wrong ver": {
|
||||
initramfs: []string{"/boot/initrd-1.1.1.aarch64.img"},
|
||||
ver: "1.1.2",
|
||||
path: "",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
for _, init := range tt.initramfs {
|
||||
env.CreateFile(filepath.Join("/var/lib/warewulf/chroots/image/rootfs", init))
|
||||
}
|
||||
|
||||
initramfs := FindInitramfs("image", tt.ver)
|
||||
if tt.path == "" {
|
||||
assert.Nil(t, initramfs)
|
||||
} else {
|
||||
assert.NotNil(t, initramfs)
|
||||
assert.Equal(t, tt.path, initramfs.Path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
41
internal/pkg/image/mountpoints.go
Normal file
41
internal/pkg/image/mountpoints.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
|
||||
/*
|
||||
Create a slice iof MntDetails from a string slice with following
|
||||
format "source:[:destination][:readonly]" if destination is not
|
||||
given, the source is used as destination
|
||||
*/
|
||||
func InitMountPnts(binds []string) (mounts []*warewulfconf.MountEntry) {
|
||||
wwlog.Debug("Trying to mount following mount points: %s", mounts)
|
||||
for _, b := range binds {
|
||||
bind := strings.Split(b, ":")
|
||||
dest := bind[0]
|
||||
if len(bind) >= 2 {
|
||||
dest = bind[1]
|
||||
}
|
||||
readonly := false
|
||||
copy_ := false
|
||||
if len(bind) >= 3 {
|
||||
if bind[2] == "ro" {
|
||||
readonly = true
|
||||
} else if bind[2] == "copy" {
|
||||
copy_ = true
|
||||
}
|
||||
}
|
||||
mntPnt := warewulfconf.MountEntry{
|
||||
Source: bind[0],
|
||||
Dest: dest,
|
||||
ReadOnlyP: &readonly,
|
||||
CopyP: ©_,
|
||||
}
|
||||
mounts = append(mounts, &mntPnt)
|
||||
}
|
||||
return mounts
|
||||
}
|
||||
97
internal/pkg/image/shimgrub.go
Normal file
97
internal/pkg/image/shimgrub.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
|
||||
func shimDirs() []string {
|
||||
return []string{
|
||||
`/usr/share/efi/*/`,
|
||||
`/usr/lib64/efi/`,
|
||||
`/boot/efi/EFI/*/`,
|
||||
}
|
||||
}
|
||||
func shimNames() []string {
|
||||
return []string{
|
||||
`shim.efi`,
|
||||
`shim-sles.efi`,
|
||||
`shimx64.efi`,
|
||||
`shim-susesigned.efi`,
|
||||
}
|
||||
}
|
||||
|
||||
func grubDirs() []string {
|
||||
return []string{
|
||||
`/usr/lib64/efi/`,
|
||||
`/usr/share/grub2/*-efi/`,
|
||||
`/usr/share/efi/*/`,
|
||||
`/boot/efi/EFI/*/`,
|
||||
}
|
||||
}
|
||||
func grubNames() []string {
|
||||
return []string{
|
||||
`grub-tpm.efi`,
|
||||
`grub.efi`,
|
||||
`grubx64.efi`,
|
||||
`grubia32.efi`,
|
||||
`grubaa64.efi`,
|
||||
`grubarm.efi`,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
find the path of the shim binary in image
|
||||
*/
|
||||
func ShimFind(image string) string {
|
||||
var image_path string
|
||||
if image != "" {
|
||||
image_path = RootFsDir(image)
|
||||
} else {
|
||||
image_path = "/"
|
||||
}
|
||||
wwlog.Debug("Finding shim under paths: %s", image_path)
|
||||
return BootLoaderFindPath(image_path, shimNames, shimDirs)
|
||||
}
|
||||
|
||||
/*
|
||||
find a grub.efi in the used image
|
||||
*/
|
||||
func GrubFind(image string) string {
|
||||
var image_path string
|
||||
if image != "" {
|
||||
image_path = RootFsDir(image)
|
||||
} else {
|
||||
image_path = "/"
|
||||
}
|
||||
wwlog.Debug("Finding grub under paths: %s", image_path)
|
||||
return BootLoaderFindPath(image_path, grubNames, grubDirs)
|
||||
}
|
||||
|
||||
/*
|
||||
find the path of the shim binary in image
|
||||
*/
|
||||
func BootLoaderFindPath(cpath string, names func() []string, paths func() []string) string {
|
||||
for _, bdir := range paths() {
|
||||
wwlog.Debug("Checking directory: %s", bdir)
|
||||
for _, bname := range names() {
|
||||
wwlog.Debug("Checking for bootloader name: %s", path.Join(cpath, bdir, bname))
|
||||
shimPaths, err := filepath.Glob(path.Join(cpath, bdir, bname))
|
||||
if err != nil {
|
||||
wwlog.Debug("Got error when globing %s: %s", path.Join(cpath, bdir, bname), err)
|
||||
}
|
||||
for _, shimPath := range shimPaths {
|
||||
wwlog.Debug("Checking for bootloader path: %s", shimPath)
|
||||
// Only succeeds if shimPath exists and, if a
|
||||
// symlink, links to a path that also exists
|
||||
if _, err := os.Stat(shimPath); err == nil {
|
||||
return shimPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
56
internal/pkg/image/shimgrub_test.go
Normal file
56
internal/pkg/image/shimgrub_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||
)
|
||||
|
||||
func Test_Find_ShimX86(t *testing.T) {
|
||||
testenv.New(t)
|
||||
conf := warewulfconf.Get()
|
||||
_ = os.MkdirAll(path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/lib64/efi/"), 0755)
|
||||
shimF, err := os.Create(path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/lib64/efi/shim.efi"))
|
||||
assert.NoError(t, err)
|
||||
_, _ = shimF.WriteString("shim.efi")
|
||||
assert.FileExists(t, path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/lib64/efi/shim.efi"))
|
||||
shimPath := ShimFind("suse")
|
||||
assert.Equal(t, path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/lib64/efi/shim.efi"), shimPath)
|
||||
}
|
||||
func Test_Find_ShimArch64(t *testing.T) {
|
||||
testenv.New(t)
|
||||
conf := warewulfconf.Get()
|
||||
_ = os.MkdirAll(path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/aarch64"), 0755)
|
||||
shimF, err := os.Create(path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/aarch64/shim.efi"))
|
||||
assert.NoError(t, err)
|
||||
_, _ = shimF.WriteString("shim.efi")
|
||||
assert.FileExists(t, path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/aarch64/shim.efi"))
|
||||
shimPath := ShimFind("suse")
|
||||
assert.Equal(t, path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/aarch64/shim.efi"), shimPath)
|
||||
}
|
||||
func Test_Find_GrubX86(t *testing.T) {
|
||||
testenv.New(t)
|
||||
conf := warewulfconf.Get()
|
||||
_ = os.MkdirAll(path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/x86_64"), 0755)
|
||||
shimF, err := os.Create(path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/x86_64/grub.efi"))
|
||||
assert.NoError(t, err)
|
||||
_, _ = shimF.WriteString("grub.efi")
|
||||
assert.FileExists(t, path.Join(conf.Paths.WWChrootdir, "suse/rootfs//usr/share/efi/x86_64/grub.efi"))
|
||||
shimPath := GrubFind("suse")
|
||||
assert.Equal(t, path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/x86_64/grub.efi"), shimPath)
|
||||
}
|
||||
func Test_Find_GrubAarch64(t *testing.T) {
|
||||
testenv.New(t)
|
||||
conf := warewulfconf.Get()
|
||||
_ = os.MkdirAll(path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/aarch64/"), 0755)
|
||||
shimF, err := os.Create(path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/aarch64/grub.efi"))
|
||||
assert.NoError(t, err)
|
||||
_, _ = shimF.WriteString("grub.efi")
|
||||
assert.FileExists(t, path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/aarch64/grub.efi"))
|
||||
shimPath := GrubFind("suse")
|
||||
assert.Equal(t, path.Join(conf.Paths.WWChrootdir, "suse/rootfs/usr/share/efi/aarch64/grub.efi"), shimPath)
|
||||
}
|
||||
428
internal/pkg/image/syncuids.go
Normal file
428
internal/pkg/image/syncuids.go
Normal file
@@ -0,0 +1,428 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/warewulf/warewulf/internal/pkg/util"
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
|
||||
const passwdPath = "/etc/passwd"
|
||||
const groupPath = "/etc/group"
|
||||
|
||||
// SyncUids updates the /etc/passwd and /etc/group files in the
|
||||
// image identified by imageName by installing the equivalent
|
||||
// files from the host and appending names only in the
|
||||
// image. Files in the image are updated to match the new
|
||||
// numeric id assignments.
|
||||
//
|
||||
// If write is false, the image is not actually updated, but
|
||||
// relevant log entries and output are generated.
|
||||
//
|
||||
// Any I/O errors are returned unmodified.
|
||||
//
|
||||
// A conflict arises if the image has an entry with the same id as
|
||||
// an entry in the host and the host does not have an entry with the
|
||||
// same name. In this case, an error is returned.
|
||||
func SyncUids(imageName string, write bool) error {
|
||||
wwlog.Debug("SyncUids(imageName=%v, write=%v)", imageName, write)
|
||||
imagePath := RootFsDir(imageName)
|
||||
imagePasswdPath := path.Join(imagePath, passwdPath)
|
||||
imageGroupPath := path.Join(imagePath, groupPath)
|
||||
|
||||
passwdSync := make(syncDB)
|
||||
if err := passwdSync.readFromHost(passwdPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := passwdSync.readFromimage(imagePasswdPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := passwdSync.checkConflicts(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groupSync := make(syncDB)
|
||||
if err := groupSync.readFromHost(groupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := groupSync.readFromimage(imageGroupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := groupSync.checkConflicts(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := passwdSync.findUserFiles(imagePath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := groupSync.findGroupFiles(imagePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
passwdSync.log("passwd")
|
||||
groupSync.log("group")
|
||||
|
||||
if write {
|
||||
if err := passwdSync.chownUserFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := groupSync.chownGroupFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := passwdSync.update(imagePasswdPath, passwdPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := groupSync.update(imageGroupPath, groupPath); err != nil {
|
||||
return err
|
||||
}
|
||||
wwlog.Info("uid/gid synced for image %s", imageName)
|
||||
} else {
|
||||
if passwdSync.needsSync() || groupSync.needsSync() {
|
||||
wwlog.Info("uid/gid not synced: run `wwctl image syncuser --write %s`", imageName)
|
||||
} else {
|
||||
wwlog.Info("uid/gid already synced")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// A syncDB maps user or group names to syncInfo instances, which
|
||||
// correlate IDs between host and image and track affected
|
||||
// files. This can be used for either /etc/passwd or /etc/group IDs.
|
||||
type syncDB map[string]syncInfo
|
||||
|
||||
// checkConflicts inspects a syncDB map for irreconcilable
|
||||
// conflicts. A conflict arises if the image has an entry with the
|
||||
// same id as an entry in the host and the host does not have an entry
|
||||
// with the same name.
|
||||
func (db syncDB) checkConflicts() error {
|
||||
for nameInimage, imageIds := range db {
|
||||
if !imageIds.inimage() || imageIds.inHost() {
|
||||
continue
|
||||
}
|
||||
|
||||
for nameInHost, hostIds := range db {
|
||||
if hostIds.HostID == imageIds.imageID {
|
||||
wwlog.Warn("syncuser cannot determine what name should be assigned to id number %v: on the local host it is assigned to '%s'; but in the image it is assigned to '%s'. Since '%s' does not exist on the local host, it cannot be reassigned to a different id number. To resolve the conflict, add '%s' to the local host and re-run syncuser.", imageIds.imageID, nameInHost, nameInimage, nameInimage, nameInimage)
|
||||
return errors.New(fmt.Sprintf("id(%v) collision: host(%s) image(%s)", imageIds.imageID, nameInHost, nameInimage))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// log generates debug and verbose logs inspecting a syncDB map.
|
||||
//
|
||||
// The provided prefix is prepended to log entries to provide context
|
||||
// for the given syncDB map. (e.g., to differentiate between a user or
|
||||
// group map)
|
||||
func (db syncDB) log(prefix string) {
|
||||
var onlyimage, onlyHost, match, differ []string
|
||||
for name, ids := range db {
|
||||
wwlog.Debug("%s: %s: host(%v) image(%v) files: %v", prefix, name, ids.HostID, ids.imageID, ids.imageFiles)
|
||||
if ids.onlyimage() {
|
||||
onlyimage = append(onlyimage, name)
|
||||
}
|
||||
if ids.onlyHost() {
|
||||
onlyHost = append(onlyHost, name)
|
||||
}
|
||||
if ids.match() {
|
||||
match = append(match, name)
|
||||
}
|
||||
if ids.differ() {
|
||||
differ = append(differ, name)
|
||||
}
|
||||
}
|
||||
|
||||
wwlog.Verbose("%s: image only: %v", prefix, onlyimage)
|
||||
wwlog.Verbose("%s: host only: %v", prefix, onlyHost)
|
||||
wwlog.Verbose("%s: match: %v", prefix, match)
|
||||
wwlog.Verbose("%s: differ: %v", prefix, differ)
|
||||
}
|
||||
|
||||
// read reads fileName (an /etc/passwd or /etc/group file) into a
|
||||
// syncDB map. Since the name and numerical id for both files are
|
||||
// stored at fields 0 and 2, the same function can read both files.
|
||||
//
|
||||
// fromimage identifies whether the given fileName includes
|
||||
// entries from the host or the image.
|
||||
func (db syncDB) read(fileName string, fromimage bool) error {
|
||||
wwlog.Debug("read %s (image: %t)", fileName, fromimage)
|
||||
if file, err := os.Open(fileName); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer file.Close()
|
||||
fileScanner := bufio.NewScanner(file)
|
||||
for fileScanner.Scan() {
|
||||
line := fileScanner.Text()
|
||||
fields := strings.Split(line, ":")
|
||||
if len(fields) != 7 && len(fields) != 4 {
|
||||
wwlog.Debug("malformed line in passwd/group: %s", line)
|
||||
continue
|
||||
}
|
||||
name := fields[0]
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
// ignore ldap/nis/sssd line
|
||||
if strings.HasPrefix(fields[0], "+") || strings.HasPrefix(fields[0], "-") {
|
||||
wwlog.Verbose("Ignoring line %s (unhandled compat-style NIS reference)", line)
|
||||
continue
|
||||
}
|
||||
id, err := strconv.Atoi(fields[2])
|
||||
if err != nil {
|
||||
wwlog.Warn("Ignoring line %s (parse error)", line)
|
||||
continue
|
||||
}
|
||||
entry, ok := db[name]
|
||||
if !ok {
|
||||
entry = syncInfo{HostID: -1, imageID: -1}
|
||||
}
|
||||
|
||||
if fromimage {
|
||||
if entry.imageID == -1 {
|
||||
entry.imageID = id
|
||||
} else if entry.imageID != id {
|
||||
wwlog.Warn("Ignoring image id(%v) for %s from %s after existing value %v", id, name, fileName, entry.imageID)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if entry.HostID == -1 {
|
||||
entry.HostID = id
|
||||
} else if entry.HostID != id {
|
||||
wwlog.Warn("Ignoring host id(%v) for %s from %s after existing value %v", id, name, fileName, entry.HostID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
db[name] = entry
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// readFromHost reads fileName into a syncDB map.
|
||||
//
|
||||
// Equivalent to read(fileName, false)
|
||||
func (db syncDB) readFromHost(fileName string) error { return db.read(fileName, false) }
|
||||
|
||||
// readFromimage reads fileName into a syncDB map.
|
||||
//
|
||||
// Equivalent to read(fileName, true)
|
||||
func (db syncDB) readFromimage(fileName string) error { return db.read(fileName, true) }
|
||||
|
||||
// findFiles finds files under imagePath that are owned by each
|
||||
// tracked image ID.
|
||||
//
|
||||
// If byGid is true, files with a matching gid are
|
||||
// recorded. Otherwise, files with a matching uid are recorded.
|
||||
func (db syncDB) findFiles(imagePath string, byGid bool) error {
|
||||
wwlog.Debug("findFiles(imagePath=%v, byGid=%v)", imagePath, byGid)
|
||||
syncIDs := make(map[int]string)
|
||||
for name, info := range db {
|
||||
if info.inHost() && info.inimage() && !info.match() {
|
||||
wwlog.Debug("syncID[%v] = %v", info.imageID, name)
|
||||
syncIDs[info.imageID] = name
|
||||
}
|
||||
}
|
||||
|
||||
return filepath.Walk(imagePath, func(filePath string, fileInfo fs.FileInfo, err error) error {
|
||||
if stat, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
|
||||
var id int
|
||||
if byGid {
|
||||
id = int(stat.Gid)
|
||||
} else {
|
||||
id = int(stat.Uid)
|
||||
}
|
||||
if name, ok := syncIDs[id]; ok {
|
||||
info := db[name]
|
||||
wwlog.Debug("findFiles: %s: (%v -> %v, gid: %v)", filePath, info.imageID, info.HostID, byGid)
|
||||
info.imageFiles = append(info.imageFiles, filePath)
|
||||
db[name] = info
|
||||
} else {
|
||||
wwlog.Debug("findFiles: %s", filePath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// findUserFiles is equivalent to findFiles(imagePath, false)
|
||||
func (db syncDB) findUserFiles(imagePath string) error { return db.findFiles(imagePath, false) }
|
||||
|
||||
// findGroupFiles is equivalent to findFiles(imagePath, true)
|
||||
func (db syncDB) findGroupFiles(imagePath string) error { return db.findFiles(imagePath, true) }
|
||||
|
||||
// chownFiles updates found files (see findFiles) to reflect ownership
|
||||
// using host ids.
|
||||
//
|
||||
// If byGid is true, file gids are updated. Otherwise, file uids are
|
||||
// updated.
|
||||
func (db syncDB) chownFiles(byGid bool) error {
|
||||
for _, ids := range db {
|
||||
if err := ids.chownFiles(byGid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// chownUserFiles is equivalent to chownFiles(false)
|
||||
func (db syncDB) chownUserFiles() error { return db.chownFiles(false) }
|
||||
|
||||
// chownUserFiles is equivalent to chownFiles(true)
|
||||
func (db syncDB) chownGroupFiles() error { return db.chownFiles(true) }
|
||||
|
||||
// getOnlyimageLines returns the lines from fileName (either an
|
||||
// /etc/passwd or /etc/group file) for names that occur only in the
|
||||
// image.
|
||||
//
|
||||
// These lines are added to the respective file from the host when
|
||||
// updating the image.
|
||||
func (db syncDB) getOnlyimageLines(fileName string) ([]string, error) {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
fileScanner := bufio.NewScanner(file)
|
||||
|
||||
var lines []string
|
||||
for fileScanner.Scan() {
|
||||
line := fileScanner.Text()
|
||||
fields := strings.Split(line, ":")
|
||||
for name, ids := range db {
|
||||
if fields[0] == name {
|
||||
if ids.onlyimage() {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
wwlog.Debug("file: %s, entries only in image: %v", fileName, lines)
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// update replaces imagePath with hostPath and adds lines from
|
||||
// getOnlyimageLines to the end of imagePath. This is used to
|
||||
// replace /etc/passwd (or /etc/group) in the image with the
|
||||
// equivalent file from the host while retaining names unique to the
|
||||
// image.
|
||||
func (db syncDB) update(imagePath string, hostPath string) error {
|
||||
wwlog.Debug("update %s from %s)", imagePath, hostPath)
|
||||
if lines, err := db.getOnlyimageLines(imagePath); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := os.Remove(imagePath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := util.CopyFile(hostPath, imagePath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := util.AppendLines(imagePath, lines); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// needsSync returns true if the syncDB map indicates that ids between
|
||||
// the image and host are out-of-sync.
|
||||
func (db syncDB) needsSync() bool {
|
||||
for name, ids := range db {
|
||||
if ids.onlyHost() {
|
||||
wwlog.Debug("sync required: %s only in host", name)
|
||||
return true
|
||||
}
|
||||
if ids.differ() {
|
||||
wwlog.Debug("sync required: %s is %v in host and %v in image", name, ids.HostID, ids.imageID)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// sycncInfo correlates the numerical id of a name on the host
|
||||
// (HostID) and the image (imageID), along with the files in
|
||||
// the image that are owned by that name. This allows affected
|
||||
// files to be updated when the HostID is applied to the image.
|
||||
type syncInfo struct {
|
||||
HostID int `access:"r,w"`
|
||||
imageID int `access:"r,w"`
|
||||
imageFiles []string `access:"r,w"`
|
||||
}
|
||||
|
||||
// inHost returns true if info has a record of an id for this name in
|
||||
// the host.
|
||||
func (info *syncInfo) inHost() bool {
|
||||
return info.HostID != -1
|
||||
}
|
||||
|
||||
// inimage returns true if info has a record of an id for this
|
||||
// name in the image.
|
||||
func (info *syncInfo) inimage() bool {
|
||||
return info.imageID != -1
|
||||
}
|
||||
|
||||
// onlyHost returns true if info has a record of an id for this name
|
||||
// in the host and not in the image.
|
||||
func (info *syncInfo) onlyHost() bool {
|
||||
return info.inHost() && !info.inimage()
|
||||
}
|
||||
|
||||
// onlyHost returns true if info has a record of an id for this name
|
||||
// in the image and not in the host.
|
||||
func (info *syncInfo) onlyimage() bool {
|
||||
return info.inimage() && !info.inHost()
|
||||
}
|
||||
|
||||
// match returns true if info represents a name that exists with the
|
||||
// same numerical id in both the host and the image.
|
||||
func (info *syncInfo) match() bool {
|
||||
return info.inimage() && info.inHost() && info.HostID == info.imageID
|
||||
}
|
||||
|
||||
// differ returns true if info represents a name that exists in both
|
||||
// the host and the image but with different numerical ids.
|
||||
func (info *syncInfo) differ() bool {
|
||||
return info.inimage() && info.inHost() && info.HostID != info.imageID
|
||||
}
|
||||
|
||||
// chownFiles updates the files recorded in info.imageFiles to use
|
||||
// the numerical IDs from the host.
|
||||
//
|
||||
// If byGid is true, the file's gid is updated; otherwise, the file's
|
||||
// uid is updated.
|
||||
func (info *syncInfo) chownFiles(byGid bool) error {
|
||||
for _, file := range info.imageFiles {
|
||||
if fileInfo, err := os.Stat(file); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if fileInfo.IsDir() || fileInfo.Mode().IsRegular() {
|
||||
var newUid, newGid int
|
||||
if byGid {
|
||||
newUid = -1
|
||||
newGid = info.HostID
|
||||
} else {
|
||||
newUid = info.HostID
|
||||
newGid = -1
|
||||
}
|
||||
if err := os.Chown(file, newUid, newGid); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
286
internal/pkg/image/syncuids_test.go
Normal file
286
internal/pkg/image/syncuids_test.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
|
||||
func writeTempFile(t *testing.T, input string) string {
|
||||
tempFile, createTempError := os.CreateTemp("", "syncuids-*")
|
||||
assert.NoError(t, createTempError)
|
||||
_, writeError := tempFile.Write([]byte(input))
|
||||
assert.NoError(t, writeError)
|
||||
assert.NoError(t, tempFile.Sync())
|
||||
return tempFile.Name()
|
||||
}
|
||||
|
||||
func makeSyncDB(t *testing.T, hostInput string, imageInput string) syncDB {
|
||||
hostFileName := writeTempFile(t, hostInput)
|
||||
defer os.Remove(hostFileName)
|
||||
imageFileName := writeTempFile(t, imageInput)
|
||||
defer os.Remove(imageFileName)
|
||||
db := make(syncDB)
|
||||
var err error
|
||||
err = db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
err = db.readFromimage(imageFileName)
|
||||
assert.NoError(t, err)
|
||||
return db
|
||||
}
|
||||
|
||||
func Test_readFromHost_single(t *testing.T) {
|
||||
hostFileName := writeTempFile(t, `testuser:x:1001:1001::/home/testuser:/bin/bash`)
|
||||
defer os.Remove(hostFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
err := db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, db, 1)
|
||||
assert.Equal(t, 1001, db["testuser"].HostID)
|
||||
assert.Equal(t, -1, db["testuser"].imageID)
|
||||
}
|
||||
|
||||
func Test_readFromHost_multiple(t *testing.T) {
|
||||
hostFileName := writeTempFile(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(hostFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
err := db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, db, 2)
|
||||
assert.Equal(t, 1001, db["testuser1"].HostID)
|
||||
assert.Equal(t, -1, db["testuser1"].imageID)
|
||||
assert.Equal(t, 1002, db["testuser2"].HostID)
|
||||
assert.Equal(t, -1, db["testuser2"].imageID)
|
||||
}
|
||||
|
||||
func Test_readFromimage_single(t *testing.T) {
|
||||
imageFileName := writeTempFile(t, `testuser:x:1001:1001::/home/testuser:/bin/bash`)
|
||||
defer os.Remove(imageFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
err := db.readFromimage(imageFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, db, 1)
|
||||
assert.Equal(t, 1001, db["testuser"].imageID)
|
||||
assert.Equal(t, -1, db["testuser"].HostID)
|
||||
}
|
||||
|
||||
func Test_readFromimage_multiple(t *testing.T) {
|
||||
imageFileName := writeTempFile(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(imageFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
err := db.readFromimage(imageFileName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, db, 2)
|
||||
assert.Equal(t, 1001, db["testuser1"].imageID)
|
||||
assert.Equal(t, -1, db["testuser1"].HostID)
|
||||
assert.Equal(t, 1002, db["testuser2"].imageID)
|
||||
assert.Equal(t, -1, db["testuser2"].HostID)
|
||||
}
|
||||
|
||||
func Test_readFromBoth_multiple(t *testing.T) {
|
||||
imageFileName := writeTempFile(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(imageFileName)
|
||||
|
||||
hostFileName := writeTempFile(t, `
|
||||
testuser1:x:2001:2001::/home/testuser:/bin/bash
|
||||
testuser3:x:2003:2003::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(hostFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
var err error
|
||||
err = db.readFromimage(imageFileName)
|
||||
assert.NoError(t, err)
|
||||
err = db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, db, 3)
|
||||
assert.Equal(t, 1001, db["testuser1"].imageID)
|
||||
assert.Equal(t, 2001, db["testuser1"].HostID)
|
||||
assert.Equal(t, 1002, db["testuser2"].imageID)
|
||||
assert.Equal(t, -1, db["testuser2"].HostID)
|
||||
assert.Equal(t, -1, db["testuser3"].imageID)
|
||||
assert.Equal(t, 2003, db["testuser3"].HostID)
|
||||
}
|
||||
|
||||
func Test_checkConflicts_empty(t *testing.T) {
|
||||
db := makeSyncDB(t, "", "")
|
||||
assert.NoError(t, db.checkConflicts())
|
||||
}
|
||||
|
||||
func Test_checkConflicts_single(t *testing.T) {
|
||||
db := makeSyncDB(t, "", "testuser:x:1001:1001::/home/testuser:/bin/bash")
|
||||
assert.NoError(t, db.checkConflicts())
|
||||
}
|
||||
|
||||
func Test_checkConflicts_match(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser:x:1001:1001::/home/testuser:/bin/bash",
|
||||
"testuser:x:1001:1001::/home/testuser:/bin/bash")
|
||||
assert.NoError(t, db.checkConflicts())
|
||||
}
|
||||
|
||||
func Test_checkConflicts_conflict(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser2:x:1001:1001::/home/testuser:/bin/bash",
|
||||
"testuser1:x:1001:1001::/home/testuser:/bin/bash")
|
||||
assert.Error(t, db.checkConflicts())
|
||||
}
|
||||
|
||||
func Test_getOnlyimageLines(t *testing.T) {
|
||||
imageFileName := writeTempFile(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(imageFileName)
|
||||
|
||||
hostFileName := writeTempFile(t, `
|
||||
testuser1:x:2001:2001::/home/testuser:/bin/bash
|
||||
testuser3:x:2003:2003::/home/testuser:/bin/bash
|
||||
`)
|
||||
defer os.Remove(hostFileName)
|
||||
|
||||
db := make(syncDB)
|
||||
var err error
|
||||
err = db.readFromimage(imageFileName)
|
||||
assert.NoError(t, err)
|
||||
err = db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
lines, err := db.getOnlyimageLines(imageFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, lines, 1)
|
||||
assert.Equal(t, lines[0], "testuser2:x:1002:1002::/home/testuser:/bin/bash")
|
||||
}
|
||||
|
||||
func Test_needsSync_empty(t *testing.T) {
|
||||
db := makeSyncDB(t, "", "")
|
||||
assert.False(t, db.needsSync())
|
||||
}
|
||||
|
||||
func Test_needsSync_imageOnly(t *testing.T) {
|
||||
db := makeSyncDB(t, "", `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash`)
|
||||
|
||||
assert.False(t, db.needsSync())
|
||||
}
|
||||
|
||||
func Test_needsSync_hostOnly(t *testing.T) {
|
||||
db := makeSyncDB(t, `
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash`, "")
|
||||
|
||||
assert.True(t, db.needsSync())
|
||||
}
|
||||
|
||||
func Test_needsSync_match(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser:x:1001:1001::/home/testuser:/bin/bash",
|
||||
"testuser:x:1001:1001::/home/testuser:/bin/bash")
|
||||
|
||||
assert.False(t, db.needsSync())
|
||||
}
|
||||
|
||||
func Test_needsSync_differ(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
`
|
||||
testuser1:x:2001:2001::/home/testuser:/bin/bash
|
||||
testuser3:x:2003:2003::/home/testuser:/bin/bash`,
|
||||
`
|
||||
testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
testuser2:x:1002:1002::/home/testuser:/bin/bash`)
|
||||
|
||||
assert.True(t, db.needsSync())
|
||||
}
|
||||
|
||||
func Test_onlyHost(t *testing.T) {
|
||||
db := makeSyncDB(t, "testuser1:x:2001:2001::/home/testuser:/bin/bash", "")
|
||||
entry := db["testuser1"]
|
||||
assert.True(t, entry.inHost())
|
||||
assert.False(t, entry.inimage())
|
||||
assert.True(t, entry.onlyHost())
|
||||
assert.False(t, entry.onlyimage())
|
||||
assert.False(t, entry.match())
|
||||
assert.False(t, entry.differ())
|
||||
}
|
||||
|
||||
func Test_onlyimage(t *testing.T) {
|
||||
db := makeSyncDB(t, "", "testuser1:x:2001:2001::/home/testuser:/bin/bash")
|
||||
entry := db["testuser1"]
|
||||
assert.False(t, entry.inHost())
|
||||
assert.True(t, entry.inimage())
|
||||
assert.False(t, entry.onlyHost())
|
||||
assert.True(t, entry.onlyimage())
|
||||
assert.False(t, entry.match())
|
||||
assert.False(t, entry.differ())
|
||||
}
|
||||
|
||||
func Test_match(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser1:x:2001:2001::/home/testuser:/bin/bash",
|
||||
"testuser1:x:2001:2001::/home/testuser:/bin/bash")
|
||||
entry := db["testuser1"]
|
||||
assert.True(t, entry.inHost())
|
||||
assert.True(t, entry.inimage())
|
||||
assert.False(t, entry.onlyHost())
|
||||
assert.False(t, entry.onlyimage())
|
||||
assert.True(t, entry.match())
|
||||
assert.False(t, entry.differ())
|
||||
}
|
||||
|
||||
func Test_differ(t *testing.T) {
|
||||
db := makeSyncDB(t,
|
||||
"testuser1:x:1001:1001::/home/testuser:/bin/bash",
|
||||
"testuser1:x:2001:2001::/home/testuser:/bin/bash")
|
||||
entry := db["testuser1"]
|
||||
assert.True(t, entry.inHost())
|
||||
assert.True(t, entry.inimage())
|
||||
assert.False(t, entry.onlyHost())
|
||||
assert.False(t, entry.onlyimage())
|
||||
assert.False(t, entry.match())
|
||||
assert.True(t, entry.differ())
|
||||
}
|
||||
|
||||
func Test_malformed_passwd(t *testing.T) {
|
||||
hostInput := `"testuser1:x:1001:1001::/home/testuser:/bin/bash"
|
||||
asdf`
|
||||
hostFileName := writeTempFile(t, hostInput)
|
||||
defer os.Remove(hostFileName)
|
||||
db := make(syncDB)
|
||||
err := db.readFromHost(hostFileName)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func Test_network_passwd(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
wwlog.SetLogWriter(buf)
|
||||
hostInput := `testuser1:x:1001:1001::/home/testuser:/bin/bash
|
||||
+::::::
|
||||
-::::::`
|
||||
hostFileName := writeTempFile(t, hostInput)
|
||||
defer os.Remove(hostFileName)
|
||||
db := make(syncDB)
|
||||
err := db.readFromHost(hostFileName)
|
||||
assert.NotContains(t, buf.String(), "parse error")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
116
internal/pkg/image/util.go
Normal file
116
internal/pkg/image/util.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/warewulf/warewulf/internal/pkg/util"
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
|
||||
func ValidName(name string) bool {
|
||||
if !util.ValidString(name, "^[\\w\\-\\.\\:]+$") {
|
||||
wwlog.Warn("Image name has illegal characters: %s", name)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ListSources() ([]string, error) {
|
||||
var ret []string
|
||||
|
||||
err := os.MkdirAll(SourceParentDir(), 0755)
|
||||
if err != nil {
|
||||
return ret, errors.New("Could not create image source parent directory: " + SourceParentDir())
|
||||
}
|
||||
wwlog.Debug("Searching for image rootfs directories: %s", SourceParentDir())
|
||||
|
||||
sources, err := os.ReadDir(SourceParentDir())
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
wwlog.Verbose("Found image source: %s", source.Name())
|
||||
|
||||
if !ValidName(source.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ValidSource(source.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, source.Name())
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func DoesSourceExist(name string) bool {
|
||||
fullPath := RootFsDir(name)
|
||||
return util.IsDir(fullPath)
|
||||
}
|
||||
|
||||
func ValidSource(name string) bool {
|
||||
if !ValidName(name) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !DoesSourceExist(name) {
|
||||
wwlog.Verbose("Location is not an image source directory: %s", name)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
Delete the chroot of an image
|
||||
*/
|
||||
func DeleteSource(name string) error {
|
||||
fullPath := SourceDir(name)
|
||||
|
||||
wwlog.Verbose("Removing path: %s", fullPath)
|
||||
return os.RemoveAll(fullPath)
|
||||
}
|
||||
|
||||
func Duplicate(name string, destination string) error {
|
||||
fullPathImageSource := RootFsDir(name)
|
||||
|
||||
wwlog.Info("Copying sources...")
|
||||
err := ImportDirectory(fullPathImageSource, destination)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Delete the image of an image
|
||||
*/
|
||||
func DeleteImage(name string) error {
|
||||
imageFile := ImageFile(name)
|
||||
if util.IsFile(imageFile) {
|
||||
wwlog.Verbose("removing %s for image %s", imageFile, name)
|
||||
errImg := os.Remove(imageFile)
|
||||
wwlog.Verbose("removing %s for image %s", imageFile+".gz", name)
|
||||
errGz := os.Remove(imageFile + ".gz")
|
||||
if errImg != nil {
|
||||
return errors.Errorf("Problems delete %s for image %s: %s\n", imageFile, name, errImg)
|
||||
}
|
||||
if errGz != nil {
|
||||
return errors.Errorf("Problems delete %s for image %s: %s\n", imageFile+".gz", name, errGz)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.Errorf("Image %s of image %s doesn't exist\n", imageFile, name)
|
||||
}
|
||||
|
||||
func IsWriteAble(name string) bool {
|
||||
return !util.IsFile(filepath.Join(SourceDir(name), "readonly"))
|
||||
}
|
||||
Reference in New Issue
Block a user