tremplin: PasswdDatabase access filesystem directly instead of via LXD
Per linked bug, going through LXD for filesystem access sometimes
introduces errors we can't catch. Since we're running as root and on the
same device, access the filesystem directly instead of going through LXD
+ HTTP.
BUG=chromium:1241710
TEST=Create user, log in to existing user. Check files + permissions inside container
Change-Id: I6d5e88c16032d2b5e2767cead627d015f55be9b9
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/tremplin/+/3128561
Reviewed-by: Fergus Dall <sidereal@google.com>
Commit-Queue: David Munro <davidmunro@google.com>
Tested-by: David Munro <davidmunro@google.com>
diff --git a/src/chromiumos/tremplin/container_file_server.go b/src/chromiumos/tremplin/container_file_server.go
new file mode 100644
index 0000000..b2dfbf7
--- /dev/null
+++ b/src/chromiumos/tremplin/container_file_server.go
@@ -0,0 +1,187 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "syscall"
+
+ lxd "github.com/lxc/lxd/client"
+ "github.com/lxc/lxd/shared"
+)
+
+type ContainerFileServer interface {
+ GetContainerFile(path string) (content io.ReadCloser, resp *lxd.ContainerFileResponse, err error)
+ CreateContainerFile(path string, args lxd.ContainerFileArgs) (err error)
+ DeleteContainerFile(path string) (err error)
+}
+
+type FsContainerFileServer struct {
+ containerName string
+}
+
+var cfsOverride ContainerFileServer
+
+// OverrideContainerFileServerForTesting overrides the container file server
+// that NewContainerFileServer will create, so you can inject a
+// mock/fake/stub/etc for testing.
+func OverrideContainerFileServerForTesting(cfs ContainerFileServer) {
+ cfsOverride = cfs
+}
+
+// NewContainerFileServer creates a new FsContainerFileServer (i.e. one backed
+// by the filesystem).
+func NewContainerFileServer(containerName string) ContainerFileServer {
+ if cfsOverride != nil {
+ return cfsOverride
+ }
+ return &FsContainerFileServer{
+ containerName: containerName,
+ }
+}
+
+// mapIdToContainerNamespace maps a uid or gid from host number to container number.
+func (cfs *FsContainerFileServer) mapIdToContainerNamespace(id int) int {
+ if id == PrimaryUserID || id == ChronosAccessID || id == AndroidRootID || id == AndroidEverybodyID {
+ return id
+ }
+ return id + 1000000
+}
+
+// mapIdFromContainerNamespace maps a uid or gid from container number to host number.
+func (cfs *FsContainerFileServer) mapIdFromContainerNamespace(id int) int {
+ if id == PrimaryUserID || id == ChronosAccessID || id == AndroidRootID || id == AndroidEverybodyID {
+ return id
+ } else if id < 1000000 {
+ // Before we first start the container our IDs will all be unmapped
+ // (e.g. /etc/skel in the container is owned by UID 0), so if our ID is
+ // below 1000000 assume it's not mapped otherwise assume it is.
+ // TODO(crbug/1244460): Try getting the actual UID map from LXD and using
+ // that.
+ return id
+ }
+ return id - 1000000
+}
+
+// containerPathToHost maps a path from something relative to the container root
+// to an absolute path.
+func (cfs *FsContainerFileServer) containerPathToHost(path string) string {
+ return shared.VarPath("containers", cfs.containerName, "rootfs", path)
+}
+
+// GetContainerFile reads a file or directory from the container given a path
+// relative to the container root. If the file is a directory then content is
+// empty and resp.entries contains a list of files, otherwise content contains
+// the file contents and entries is empty. If you call this on anything other
+// than a file or directory (e.g. a symlink) it probably won't do what you want.
+// Note: Mode inside resp is the mode you get from the stat syscall, this is
+// not a FileMode, and different from what os.Stat gives you.
+// Note: GID and UID in resp are container IDs (e.g. a file owned by container
+// root will have UID 0, even though on the filesystem its UID will be 1000000).
+func (cfs *FsContainerFileServer) GetContainerFile(path string) (content io.ReadCloser, resp *lxd.ContainerFileResponse, err error) {
+ p := cfs.containerPathToHost(path)
+ var s syscall.Stat_t
+ if err := syscall.Stat(p, &s); err != nil {
+ return nil, nil, err
+ }
+ var t string
+ var entries []string
+ var reader io.ReadCloser
+ // Careful! s.Mode from syscall.Stat is very similar, but not quite the
+ // same, as the FileMode you get from os.Stat. So we do our own masking
+ // instead of using FileMode.IsDir().
+ if (s.Mode & syscall.S_IFDIR) != 0 {
+ t = "directory"
+ list, err := ioutil.ReadDir(p)
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, l := range list {
+ entries = append(entries, l.Name())
+ }
+ reader = nil
+ } else {
+ t = "file"
+ entries = make([]string, 0)
+ reader, err = os.Open(p)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+ resp = &lxd.ContainerFileResponse{
+ UID: int64(cfs.mapIdFromContainerNamespace(int(s.Uid))),
+ GID: int64(cfs.mapIdFromContainerNamespace(int(s.Gid))),
+ Mode: int(s.Mode),
+ Type: t,
+ Entries: entries,
+ }
+ return reader, resp, nil
+}
+
+// CreateContainerFile creates a file or directory at the given path relative to
+// the container root. if args.Type is "directory" it will create the directory
+// and any parents if needed, if args.Type is "file" then a file will be created
+// with the contents of args.Content, unless args.Content is nil, in which case
+// the file will be created with the specified mode if it doesn't exist, or if
+// the file does already exist nothing will happen.
+// The mode, UID and GID of the target will always be set to that provided, even
+// if the file or directory already existed. If parent directories were created
+// then the parents will have the specified mode but the owner will be the user
+// tremplin is running as, any parent directories which already existed will be
+// unchanged.
+// Note: UID and GID are container ids (e.g. root is 0, not 1000000).
+// Note: Only file and directory are supported, anything else e.g. symlinks will
+// panic.
+// Note: if args.Type is "file" and args.Content != nil then args.WriteMode must
+// be "overwrite", anything else is unsupported. If args.Type is "directory" or
+// args.Type is "file" and args.Content is nil then args.WriteMode is ignored.
+func (cfs *FsContainerFileServer) CreateContainerFile(path string, args lxd.ContainerFileArgs) (err error) {
+ p := cfs.containerPathToHost(path)
+ mode := os.FileMode(args.Mode)
+ if args.Type == "directory" {
+ if err := os.MkdirAll(p, mode); err != nil {
+ return err
+ }
+ } else if args.Type == "file" {
+ if args.Content != nil {
+ if args.WriteMode != "overwrite" {
+ log.Panic("Only overwrite supported by CreateContainerFile")
+ }
+ var b []byte
+ b, err = ioutil.ReadAll(args.Content)
+ if err != nil {
+ return err
+ }
+ if err := ioutil.WriteFile(p, b, mode); err != nil {
+ return err
+ }
+ } else {
+ file, err := os.OpenFile(p, os.O_CREATE, mode)
+ if err != nil {
+ return err
+ }
+ file.Close()
+ }
+ } else {
+ log.Panic("Only file and directory types are supported by CreateContainerFile")
+ }
+ if err := os.Chown(p, cfs.mapIdToContainerNamespace(int(args.UID)), cfs.mapIdToContainerNamespace(int(args.GID))); err != nil {
+ return err
+ }
+ log.Printf("Creating container file with metadata: %+v\n", args)
+ // We set the mode above, but if the file already exists its mode won't be
+ // changed so we always set the mode again here.
+ return os.Chmod(p, mode)
+}
+
+// DeleteContainerFile deletes a file or directory in the container at path
+// relative to the container root. If the target is a directory will also delete
+// everything within the directory,
+func (cfs *FsContainerFileServer) DeleteContainerFile(path string) (err error) {
+ return os.RemoveAll(cfs.containerPathToHost(path))
+}
diff --git a/src/chromiumos/tremplin/passwd_db.go b/src/chromiumos/tremplin/passwd_db.go
index 48855a5..5d77032 100644
--- a/src/chromiumos/tremplin/passwd_db.go
+++ b/src/chromiumos/tremplin/passwd_db.go
@@ -8,7 +8,6 @@
"bytes"
"encoding"
"fmt"
- "io"
"io/ioutil"
"log"
"path"
@@ -26,35 +25,28 @@
shadowPath = "/etc/shadow"
)
-type containerFileServer interface {
- GetContainerFile(containerName string, path string) (content io.ReadCloser, resp *lxd.ContainerFileResponse, err error)
- CreateContainerFile(containerName string, path string, args lxd.ContainerFileArgs) (err error)
- DeleteContainerFile(containerName string, path string) (err error)
-}
-
type PasswdDatabase struct {
passwd shadow.PasswdFile
shadow shadow.ShadowFile
group shadow.GroupFile
groupShadow shadow.GroupShadowFile
- containerName string
- lxd containerFileServer
+ cfs ContainerFileServer
}
func daysSinceEpoch() uint64 {
return uint64(time.Since(time.Unix(0, 0)).Hours()) / 24
}
-// NewPasswdDatabase loads PasswdDatabase state from the provided LXD container name.
-func NewPasswdDatabase(lxd containerFileServer, containerName string) (*PasswdDatabase, error) {
+// NewPasswdDatabase reads/writes PasswdDatabase state in a container using the
+// using the provided container file server.
+func NewPasswdDatabase(cfs ContainerFileServer) (*PasswdDatabase, error) {
pd := &PasswdDatabase{
- containerName: containerName,
- lxd: lxd,
+ cfs: cfs,
}
loadContainerFile := func(path string, u encoding.TextUnmarshaler) error {
- r, _, err := lxd.GetContainerFile(containerName, path)
+ r, _, err := cfs.GetContainerFile(path)
if err != nil {
return fmt.Errorf("failed to find %q: %v", path, err)
}
@@ -114,7 +106,7 @@
if err != nil {
return fmt.Errorf("failed to marshal for %s: %v", path, err)
}
- if err := pd.lxd.CreateContainerFile(pd.containerName, path, lxd.ContainerFileArgs{
+ if err := pd.cfs.CreateContainerFile(path, lxd.ContainerFileArgs{
Content: bytes.NewReader(b),
UID: 0,
GID: gid,
@@ -224,7 +216,7 @@
}
func (pd *PasswdDatabase) recursiveCopy(src, dst string, uid uint32) error {
- r, s, err := pd.lxd.GetContainerFile(pd.containerName, src)
+ r, s, err := pd.cfs.GetContainerFile(src)
if err != nil {
return fmt.Errorf("failed to find %q: %v", src, err)
}
@@ -236,7 +228,7 @@
return fmt.Errorf("failed to read in file %q: %v", src, err)
}
- if err := pd.lxd.CreateContainerFile(pd.containerName, dst, lxd.ContainerFileArgs{
+ if err := pd.cfs.CreateContainerFile(dst, lxd.ContainerFileArgs{
Content: bytes.NewReader(b),
UID: int64(uid),
GID: int64(uid),
@@ -247,7 +239,7 @@
return fmt.Errorf("failed to write %s to container %q: %v", s.Type, dst, err)
}
case "directory":
- if err := pd.lxd.CreateContainerFile(pd.containerName, dst, lxd.ContainerFileArgs{
+ if err := pd.cfs.CreateContainerFile(dst, lxd.ContainerFileArgs{
UID: int64(uid),
GID: int64(uid),
Mode: s.Mode,
@@ -297,7 +289,7 @@
homedir = fmt.Sprintf("/home/%s", username)
shell = "/bin/bash"
- _, s, err := pd.lxd.GetContainerFile(pd.containerName, homedir)
+ _, s, err := pd.cfs.GetContainerFile(homedir)
// If there's a non-directory file where the home
// directory needs to go, get rid of it. If there's
@@ -307,14 +299,14 @@
createHomeDir := !(err == nil && s.Type == "directory")
if removeFile {
- err := pd.lxd.DeleteContainerFile(pd.containerName, homedir)
+ err := pd.cfs.DeleteContainerFile(homedir)
if err != nil {
return fmt.Errorf("%v type file at path %v must be removed to create home directory, but could not be: %v", s.Type, homedir, err)
}
}
if createHomeDir {
- if err := pd.lxd.CreateContainerFile(pd.containerName, homedir, lxd.ContainerFileArgs{
+ if err := pd.cfs.CreateContainerFile(homedir, lxd.ContainerFileArgs{
UID: int64(uid),
GID: int64(uid),
Mode: 0755,
@@ -324,7 +316,7 @@
}
// Copy home directory skeleton from /etc/skel if it exists.
- _, s, err = pd.lxd.GetContainerFile(pd.containerName, "/etc/skel")
+ _, s, err = pd.cfs.GetContainerFile("/etc/skel")
if err == nil && s.Type == "directory" {
if err := pd.recursiveCopy("/etc/skel", homedir, uid); err != nil {
return fmt.Errorf("failed to populate homedir: %v", err)
diff --git a/src/chromiumos/tremplin/passwd_db_test.go b/src/chromiumos/tremplin/passwd_db_test.go
index b14f3de..f982b3c 100644
--- a/src/chromiumos/tremplin/passwd_db_test.go
+++ b/src/chromiumos/tremplin/passwd_db_test.go
@@ -14,7 +14,7 @@
"strings"
"testing"
- "github.com/lxc/lxd/client"
+ lxd "github.com/lxc/lxd/client"
)
const testPasswd string = `root:x:0:0:root:/root:/bin/bash
@@ -289,7 +289,7 @@
return cwd, nil
}
-func (s *fakeContainerFileServer) GetContainerFile(containerName string, filePath string) (io.ReadCloser, *lxd.ContainerFileResponse, error) {
+func (s *fakeContainerFileServer) GetContainerFile(filePath string) (io.ReadCloser, *lxd.ContainerFileResponse, error) {
entry, err := s.findEntry(filePath, &s.root)
if err != nil {
return nil, nil, err
@@ -319,7 +319,7 @@
return content, resp, nil
}
-func (s *fakeContainerFileServer) CreateContainerFile(containerName string, filePath string, args lxd.ContainerFileArgs) error {
+func (s *fakeContainerFileServer) CreateContainerFile(filePath string, args lxd.ContainerFileArgs) error {
dirPath, filename := path.Split(filePath)
entry, err := s.findEntry(dirPath, &s.root)
@@ -384,7 +384,7 @@
return nil
}
-func (s *fakeContainerFileServer) DeleteContainerFile(containerName string, filePath string) error {
+func (s *fakeContainerFileServer) DeleteContainerFile(filePath string) error {
return errors.New("not yet implemented")
}
@@ -479,7 +479,7 @@
}
func (s *fakeContainerFileServer) assertContainerPath(filePath string, expect lxd.ContainerFileResponse) {
- _, resp, err := s.GetContainerFile("", filePath)
+ _, resp, err := s.GetContainerFile(filePath)
if err != nil {
s.t.Fatalf("Failed to get %s: %v", filePath, err)
}
@@ -529,7 +529,7 @@
// Create a directory and ensure it can be read back.
testDir := "/home/foo"
- if err := s.CreateContainerFile("", testDir, lxd.ContainerFileArgs{
+ if err := s.CreateContainerFile(testDir, lxd.ContainerFileArgs{
Content: bytes.NewReader([]byte("foo")),
UID: int64(1000),
GID: int64(1000),
@@ -552,7 +552,7 @@
func TestUserOverwrite(t *testing.T) {
s := NewFakeFileServer(t)
- pd, err := NewPasswdDatabase(s, "")
+ pd, err := NewPasswdDatabase(s)
if err != nil {
t.Fatalf("Failed to load passwd db: %v", err)
}
@@ -590,7 +590,7 @@
func TestHomedirCreation(t *testing.T) {
s := NewFakeFileServer(t)
- pd, err := NewPasswdDatabase(s, "")
+ pd, err := NewPasswdDatabase(s)
if err != nil {
t.Fatalf("Failed to load passwd db: %v", err)
}
@@ -614,7 +614,7 @@
t.Fatalf("Failed creating testuser2: %v", err)
}
- _, _, err = s.GetContainerFile("", "/home/testuser2")
+ _, _, err = s.GetContainerFile("/home/testuser2")
if err == nil {
t.Fatalf("Expected no homedir for testuser2")
}
@@ -623,7 +623,7 @@
func TestPersistence(t *testing.T) {
s := NewFakeFileServer(t)
- pd, err := NewPasswdDatabase(s, "")
+ pd, err := NewPasswdDatabase(s)
if err != nil {
t.Fatalf("Failed to load passwd db: %v", err)
}
@@ -645,7 +645,7 @@
if err := pd.Save(); err != nil {
t.Fatalf("Failed to save PasswdDatabase: %v", err)
}
- pd, err = NewPasswdDatabase(s, "")
+ pd, err = NewPasswdDatabase(s)
if err != nil {
t.Fatalf("Failed to reload passwd db: %v", err)
}
@@ -668,7 +668,7 @@
func TestGroupAddition(t *testing.T) {
s := NewFakeFileServer(t)
- pd, err := NewPasswdDatabase(s, "")
+ pd, err := NewPasswdDatabase(s)
if err != nil {
t.Fatalf("Failed to load passwd db: %v", err)
}
@@ -712,7 +712,7 @@
func TestGroupOverwrite(t *testing.T) {
s := NewFakeFileServer(t)
- pd, err := NewPasswdDatabase(s, "")
+ pd, err := NewPasswdDatabase(s)
if err != nil {
t.Fatalf("Failed to load passwd db: %v", err)
}
diff --git a/src/chromiumos/tremplin/start_lxd.go b/src/chromiumos/tremplin/start_lxd.go
index 8c81090..92c8680 100644
--- a/src/chromiumos/tremplin/start_lxd.go
+++ b/src/chromiumos/tremplin/start_lxd.go
@@ -287,6 +287,10 @@
func initProfile(c lxd.ContainerServer) error {
var defaultProfile api.ProfilesPost
+ // NOTE: If you change idmap then you must also update the map in
+ // container_file_server.go (idToContainer and idFromContainer). Also, this
+ // will trigger a remap so strongly considering migrating users to shiftfs
+ // beforehand.
if err := json.Unmarshal([]byte(`{
"name": "default",
"config": {
diff --git a/src/chromiumos/tremplin/tremplin.go b/src/chromiumos/tremplin/tremplin.go
index 9be325f..cb036c3 100644
--- a/src/chromiumos/tremplin/tremplin.go
+++ b/src/chromiumos/tremplin/tremplin.go
@@ -53,10 +53,10 @@
setupFinishedKey = "tremplinSetupFinished"
lingerPath = "/var/lib/systemd/linger"
- primaryUserID = 1000
- chronosAccessID = 1001
- androidRootID = 655360
- androidEverybodyID = 665357
+ PrimaryUserID = 1000
+ ChronosAccessID = 1001
+ AndroidRootID = 655360
+ AndroidEverybodyID = 665357
exportWriterBufferSize = 16 * 1024 * 1024 // 16Mib.
@@ -1124,14 +1124,15 @@
return response, nil
}
- pd, err := NewPasswdDatabase(s.lxd, in.ContainerName)
+ cfs := NewContainerFileServer(in.ContainerName)
+ pd, err := NewPasswdDatabase(cfs)
if err != nil {
response.Status = pb.GetContainerUsernameResponse_FAILED
response.FailureReason = fmt.Sprintf("failed to get container passwd db: %v", err)
return response, nil
}
- p := pd.PasswdForUid(primaryUserID)
+ p := pd.PasswdForUid(PrimaryUserID)
if p == nil {
response.Status = pb.GetContainerUsernameResponse_USER_NOT_FOUND
response.FailureReason = "failed to find user for uid"
@@ -1151,29 +1152,32 @@
response := &pb.SetUpUserResponse{}
- pd, err := NewPasswdDatabase(s.lxd, in.ContainerName)
+ cfs := NewContainerFileServer(in.ContainerName)
+ pd, err := NewPasswdDatabase(cfs)
if err != nil {
response.Status = pb.SetUpUserResponse_UNKNOWN
response.FailureReason = fmt.Sprintf("failed to get container passwd db: %v", err)
return response, nil
}
- p := pd.PasswdForUid(primaryUserID)
+ p := pd.PasswdForUid(PrimaryUserID)
if p != nil {
response.Username = p.Name
} else {
response.Username = in.ContainerUsername
}
+ // Note: If you add/remove users you probably also need to update raw.idmap
+ // in start_lxd.go and the id maps in container_file_server.go.
users := []struct {
name string
uid uint32
loginEnabled bool
}{
- {response.Username, primaryUserID, true},
- {"chronos-access", chronosAccessID, false},
- {"android-everybody", androidEverybodyID, false},
- {"android-root", androidRootID, false},
+ {response.Username, PrimaryUserID, true},
+ {"chronos-access", ChronosAccessID, false},
+ {"android-everybody", AndroidEverybodyID, false},
+ {"android-root", AndroidRootID, false},
}
for _, user := range users {
@@ -1220,7 +1224,7 @@
}
// Enable loginctl linger for the target user.
- if err := pd.lxd.CreateContainerFile(in.ContainerName, lingerPath, lxd.ContainerFileArgs{
+ if err := cfs.CreateContainerFile(lingerPath, lxd.ContainerFileArgs{
UID: 0,
GID: 0,
Mode: 0755,
@@ -1231,11 +1235,12 @@
return response, nil
}
userLingerPath := path.Join(lingerPath, response.Username)
- if err := pd.lxd.CreateContainerFile(in.ContainerName, userLingerPath, lxd.ContainerFileArgs{
- UID: 0,
- GID: 0,
- Mode: 0644,
- Type: "file",
+ if err := cfs.CreateContainerFile(userLingerPath, lxd.ContainerFileArgs{
+ Content: nil,
+ UID: 0,
+ GID: 0,
+ Mode: 0644,
+ Type: "file",
}); err != nil {
response.Status = pb.SetUpUserResponse_FAILED
response.FailureReason = fmt.Sprintf("failed to create linger file: %v", err)
diff --git a/src/chromiumos/tremplin/upgrade_container.go b/src/chromiumos/tremplin/upgrade_container.go
index 9c1b8bc..8b98e55 100644
--- a/src/chromiumos/tremplin/upgrade_container.go
+++ b/src/chromiumos/tremplin/upgrade_container.go
@@ -60,13 +60,14 @@
}
// Find the user's home directory so we can put the log file there.
- pw_db, err := NewPasswdDatabase(s.lxd, containerName)
+ cfs := NewContainerFileServer(containerName)
+ pw_db, err := NewPasswdDatabase(cfs)
if err != nil {
return pb.UpgradeContainerResponse_FAILED, fmt.Sprintf("Failed to parse /etc/passwd in container %q: %v", containerName, err)
}
var homedir string
for _, entry := range pw_db.passwd.Entries {
- if entry.Uid == primaryUserID {
+ if entry.Uid == PrimaryUserID {
homedir = entry.Homedir
break
}
@@ -81,7 +82,7 @@
for i := sourceVersionEnum + 1; i <= targetVersion; i++ {
bashCmd = append(bashCmd, i.String())
}
- bashCmd = append(bashCmd, "|", "sudo", "-u", "\\#"+strconv.Itoa(primaryUserID), "-g", "\\#"+strconv.Itoa(primaryUserID), "--", "tee", "--append", homedir+"/container-upgrade.log")
+ bashCmd = append(bashCmd, "|", "sudo", "-u", "\\#"+strconv.Itoa(PrimaryUserID), "-g", "\\#"+strconv.Itoa(PrimaryUserID), "--", "tee", "--append", homedir+"/container-upgrade.log")
execArgs = append(execArgs, strings.Join(bashCmd, " "))
// If we are retrying an incomplete upgrade it might look like
diff --git a/src/chromiumos/tremplin/upgrade_container_test.go b/src/chromiumos/tremplin/upgrade_container_test.go
index 47b3da4..21f1ba5 100644
--- a/src/chromiumos/tremplin/upgrade_container_test.go
+++ b/src/chromiumos/tremplin/upgrade_container_test.go
@@ -11,6 +11,7 @@
"fmt"
"io"
"io/ioutil"
+ "log"
"strings"
"testing"
"time"
@@ -41,6 +42,10 @@
validator func(pb.UpgradeContainerProgress)
}
+type containerFileServerStub struct {
+ lxd *lxdStub
+}
+
func (s *lxdStub) ExecContainer(containerName string, exec api.ContainerExecPost, args *lxd.ContainerExecArgs) (lxd.Operation, error) {
s.operation.out = args.Stdout
s.lastExec = &exec
@@ -65,6 +70,20 @@
return reader, &resp, nil
}
+func (s *containerFileServerStub) GetContainerFile(path string) (io.ReadCloser, *lxd.ContainerFileResponse, error) {
+ return s.lxd.GetContainerFile("unused", path)
+}
+
+func (s *containerFileServerStub) CreateContainerFile(path string, args lxd.ContainerFileArgs) (err error) {
+ log.Fatal("Not implemented")
+ return nil
+}
+
+func (s *containerFileServerStub) DeleteContainerFile(path string) (err error) {
+ log.Fatal("Not implemented")
+ return nil
+}
+
func (s operationStub) Wait() (err error) {
time.Sleep(s.waitTime)
s.out.Write([]byte("Last in-progress message\nDone message\n"))
@@ -111,6 +130,9 @@
osRelease: makeOsRelease("stretch"),
}
+ cfs := &containerFileServerStub{}
+ OverrideContainerFileServerForTesting(cfs)
+
server := &tremplinServer{
lxd: lxd,
upgradeStatus: *NewTransactionMap(),