github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/container/kvm/sync_internal_test.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package kvm
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/juju/clock/testclock"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/testing"
    19  	jc "github.com/juju/testing/checkers"
    20  	gc "gopkg.in/check.v1"
    21  
    22  	"github.com/juju/juju/core/paths"
    23  	"github.com/juju/juju/environs/imagedownloads"
    24  )
    25  
    26  // syncInternalSuite is gocheck boilerplate.
    27  type syncInternalSuite struct {
    28  	testing.IsolationSuite
    29  }
    30  
    31  var _ = gc.Suite(&syncInternalSuite{})
    32  
    33  const imageContents = "fake img file"
    34  
    35  func (syncInternalSuite) TestFetcher(c *gc.C) {
    36  	ts := newTestServer()
    37  	defer ts.Close()
    38  
    39  	md := newTestMetadata()
    40  
    41  	tmpdir, pathfinder, ok := newTmpdir()
    42  	if !ok {
    43  		c.Fatal("failed to setup temp dir in test")
    44  	}
    45  	defer func() {
    46  		err := os.RemoveAll(tmpdir)
    47  		if err != nil {
    48  			c.Errorf("got error %q when removing tmpdir %q",
    49  				err.Error(),
    50  				tmpdir)
    51  		}
    52  	}()
    53  
    54  	fetcher, err := newDefaultFetcher(md, ts.URL, pathfinder, nil)
    55  	c.Assert(err, jc.ErrorIsNil)
    56  
    57  	// setup a fake command runner.
    58  	stub := runStub{}
    59  	fetcher.image.runCmd = stub.Run
    60  
    61  	err = fetcher.Fetch()
    62  	c.Assert(err, jc.ErrorIsNil)
    63  
    64  	_, err = os.Stat(fetcher.image.tmpFile.Name())
    65  	c.Check(os.IsNotExist(err), jc.IsTrue)
    66  
    67  	// Check that our call was made as expected.
    68  	c.Assert(stub.Calls(), gc.HasLen, 1)
    69  	c.Assert(stub.Calls()[0], gc.Matches, " qemu-img convert -f qcow2 .*/juju-kvm-server.img-.* .*/guests/version-archless-backing-file.qcow")
    70  
    71  }
    72  
    73  func (syncInternalSuite) TestFetcherWriteFails(c *gc.C) {
    74  	ts := newTestServer()
    75  	defer ts.Close()
    76  
    77  	md := newTestMetadata()
    78  
    79  	tmpdir, pathfinder, ok := newTmpdir()
    80  	if !ok {
    81  		c.Fatal("failed to setup temp dir in test")
    82  	}
    83  	defer func() {
    84  		err := os.RemoveAll(tmpdir)
    85  		if err != nil {
    86  			c.Errorf("got error %q when removing tmpdir %q",
    87  				err.Error(),
    88  				tmpdir)
    89  		}
    90  	}()
    91  
    92  	fetcher, err := newDefaultFetcher(md, ts.URL, pathfinder, nil)
    93  	c.Assert(err, jc.ErrorIsNil)
    94  
    95  	// setup a fake command runner.
    96  	stub := runStub{err: errors.Errorf("boom")}
    97  	fetcher.image.runCmd = stub.Run
    98  
    99  	// Make sure we got the error.
   100  	err = fetcher.Fetch()
   101  	c.Assert(err, gc.ErrorMatches, "boom")
   102  
   103  	// Check that our call was made as expected.
   104  	c.Assert(stub.Calls(), gc.HasLen, 1)
   105  	c.Assert(stub.Calls()[0], gc.Matches, " qemu-img convert -f qcow2 .*/juju-kvm-server.img-.* .*/guests/version-archless-backing-file.qcow")
   106  
   107  }
   108  
   109  func (syncInternalSuite) TestFetcherInvalidSHA(c *gc.C) {
   110  	ts := newTestServer()
   111  	defer ts.Close()
   112  
   113  	md := newTestMetadata()
   114  	md.SHA256 = "invalid"
   115  
   116  	tmpdir, pathfinder, ok := newTmpdir()
   117  	if !ok {
   118  		c.Fatal("failed to setup temp dir in test")
   119  	}
   120  	defer func() {
   121  		err := os.RemoveAll(tmpdir)
   122  		if err != nil {
   123  			c.Errorf("got error %q when removing tmpdir %q",
   124  				err.Error(),
   125  				tmpdir)
   126  		}
   127  	}()
   128  
   129  	fetcher, err := newDefaultFetcher(md, ts.URL, pathfinder, nil)
   130  	c.Assert(err, jc.ErrorIsNil)
   131  
   132  	err = fetcher.Fetch()
   133  	c.Assert(err, gc.ErrorMatches, "hash sum mismatch for /tmp/juju-kvm-.*")
   134  }
   135  
   136  func (syncInternalSuite) TestFetcherNotFound(c *gc.C) {
   137  	ts := newTestServer()
   138  	defer ts.Close()
   139  
   140  	md := newTestMetadata()
   141  	md.Path = "not-there"
   142  
   143  	tmpdir, pathfinder, ok := newTmpdir()
   144  	if !ok {
   145  		c.Fatal("failed to setup temp dir in test")
   146  	}
   147  	defer func() {
   148  		err := os.RemoveAll(tmpdir)
   149  		if err != nil {
   150  			c.Errorf("got error %q when removing tmpdir %q",
   151  				err.Error(),
   152  				tmpdir)
   153  		}
   154  	}()
   155  
   156  	fetcher, err := newDefaultFetcher(md, ts.URL, pathfinder, nil)
   157  	c.Assert(err, jc.ErrorIsNil)
   158  
   159  	err = fetcher.Fetch()
   160  	c.Check(errors.IsNotFound(err), jc.IsTrue)
   161  	c.Assert(err, gc.ErrorMatches, `got 404 fetching image "not-there" not found`)
   162  }
   163  
   164  func newTestMetadata() *imagedownloads.Metadata {
   165  	return &imagedownloads.Metadata{
   166  		Arch:    "archless",
   167  		Release: "spammy",
   168  		Version: "version",
   169  		FType:   "ftype",
   170  		SHA256:  "5e8467e6732923e74de52ef60134ba747aeeb283812c60f69b67f4f79aca1475",
   171  		Path:    "server.img",
   172  		Size:    int64(len(imageContents)),
   173  	}
   174  }
   175  
   176  func newTestServer() *httptest.Server {
   177  	mtime := time.Unix(1000, 0).UTC()
   178  	imageFile := &fakeFileInfo{
   179  		basename: "series-image.img",
   180  		modtime:  mtime,
   181  		contents: imageContents,
   182  	}
   183  	fs := fakeFS{
   184  		"/": &fakeFileInfo{
   185  			dir:  true,
   186  			ents: []*fakeFileInfo{imageFile},
   187  		},
   188  		"/server.img": imageFile,
   189  	}
   190  	return httptest.NewServer(http.FileServer(fs))
   191  }
   192  
   193  // newTmpdir creates a tmpdir and returns pathfinder func that returns the
   194  // tmpdir.
   195  func newTmpdir() (string, pathfinderFunc, bool) {
   196  	td, err := os.MkdirTemp("", "juju-test-kvm-internalSuite")
   197  	if err != nil {
   198  		return "", nil, false
   199  	}
   200  	pathfinder := func(_ paths.OS) string { return td }
   201  	return td, pathfinder, true
   202  }
   203  
   204  type fakeFileInfo struct {
   205  	dir      bool
   206  	basename string
   207  	modtime  time.Time
   208  	ents     []*fakeFileInfo
   209  	contents string
   210  	err      error
   211  }
   212  
   213  func (f *fakeFileInfo) Name() string       { return f.basename }
   214  func (f *fakeFileInfo) Sys() interface{}   { return nil }
   215  func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
   216  func (f *fakeFileInfo) IsDir() bool        { return f.dir }
   217  func (f *fakeFileInfo) Size() int64        { return int64(len(f.contents)) }
   218  func (f *fakeFileInfo) Mode() os.FileMode {
   219  	if f.dir {
   220  		return 0755 | os.ModeDir
   221  	}
   222  	return 0644
   223  }
   224  
   225  type fakeFile struct {
   226  	io.ReadSeeker
   227  	fi     *fakeFileInfo
   228  	path   string // as opened
   229  	entpos int
   230  }
   231  
   232  func (f *fakeFile) Close() error               { return nil }
   233  func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
   234  func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
   235  	if !f.fi.dir {
   236  		return nil, os.ErrInvalid
   237  	}
   238  	var fis []os.FileInfo
   239  
   240  	limit := f.entpos + count
   241  	if count <= 0 || limit > len(f.fi.ents) {
   242  		limit = len(f.fi.ents)
   243  	}
   244  	for ; f.entpos < limit; f.entpos++ {
   245  		fis = append(fis, f.fi.ents[f.entpos])
   246  	}
   247  
   248  	if len(fis) == 0 && count > 0 {
   249  		return fis, io.EOF
   250  	}
   251  	return fis, nil
   252  }
   253  
   254  type fakeFS map[string]*fakeFileInfo
   255  
   256  func (fs fakeFS) Open(name string) (http.File, error) {
   257  	name = path.Clean(name)
   258  	f, ok := fs[name]
   259  	if !ok {
   260  		return nil, os.ErrNotExist
   261  	}
   262  	if f.err != nil {
   263  		return nil, f.err
   264  	}
   265  	return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
   266  }
   267  
   268  type progressWriterSuite struct {
   269  	testing.IsolationSuite
   270  }
   271  
   272  var _ = gc.Suite(&progressWriterSuite{})
   273  
   274  func (s *progressWriterSuite) TestOnlyPercentChanges(c *gc.C) {
   275  	cbLog := []string{}
   276  	loggingCB := func(msg string) {
   277  		cbLog = append(cbLog, msg)
   278  	}
   279  	clock := testclock.NewClock(time.Date(2007, 1, 1, 10, 20, 30, 1234, time.UTC))
   280  	// We are using clock to actually measure time, not trigger an event, which
   281  	// causes the testclock.Clock to think we're doing something wrong, so we
   282  	// just create one waiter that we'll otherwise ignore.
   283  	ignored := clock.After(10 * time.Second)
   284  	_ = ignored
   285  	writer := progressWriter{
   286  		callback: loggingCB,
   287  		url:      "http://host/path",
   288  		total:    0,
   289  		maxBytes: 100 * 1024 * 1024, // 100 MB
   290  		clock:    clock,
   291  	}
   292  	content := make([]byte, 50*1024)
   293  	// Start the clock before the first tick, that way every tick represents
   294  	// exactly 1ms and 50kiB written.
   295  	now := clock.Now()
   296  	writer.startTime = &now
   297  	for i := 0; i < 2048; i++ {
   298  		clock.Advance(time.Millisecond)
   299  		n, err := writer.Write(content)
   300  		c.Assert(err, jc.ErrorIsNil)
   301  		c.Check(n, gc.Equals, len(content))
   302  	}
   303  	expectedCB := []string{}
   304  	for i := 1; i <= 100; i++ {
   305  		// We tick every 1ms and add 50kiB each time, which is
   306  		// 50*1024 *1000/ 1000/1000  = 51MB/s
   307  		expectedCB = append(expectedCB, fmt.Sprintf("copying http://host/path %d%% (51 MB/s)", i))
   308  	}
   309  	// There are 2048 calls to Write, but there should only be 100 calls to progress update
   310  	c.Check(len(cbLog), gc.Equals, 100)
   311  	c.Check(cbLog, gc.DeepEquals, expectedCB)
   312  }