// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 package crunchrun import ( "fmt" "os" "os/exec" "strings" "sync" "time" "git.arvados.org/arvados.git/sdk/go/arvados" "git.arvados.org/arvados.git/sdk/go/arvadostest" . "gopkg.in/check.v1" ) var _ = Suite(&singularitySuite{}) type singularitySuite struct { executorSuite } func (s *singularitySuite) SetUpSuite(c *C) { _, err := exec.LookPath("singularity") if err != nil { c.Skip("looks like singularity is not installed") } s.newExecutor = func(c *C) { var err error s.executor, err = newSingularityExecutor(c.Logf) c.Assert(err, IsNil) } arvadostest.StartKeep(2, true) } func (s *singularitySuite) TearDownSuite(c *C) { if s.executor != nil { s.executor.Close() } } func (s *singularitySuite) TestEnableNetwork_Listen(c *C) { // With modern iptables, singularity (as of 4.2.1) cannot // enable networking when invoked by a regular user. Under // arvados-dispatch-cloud, crunch-run runs as root, so it's // OK. For testing, assuming tests are not running as root, we // use sudo -- but only if requested via environment variable. if os.Getuid() == 0 { // already root } else if os.Getenv("ARVADOS_TEST_PRIVESC") == "sudo" { c.Logf("ARVADOS_TEST_PRIVESC is 'sudo', invoking 'sudo singularity ...'") s.executor.(*singularityExecutor).sudo = true } else { c.Skip("test case needs to run singularity as root -- set ARVADOS_TEST_PRIVESC=sudo to enable this test") } s.executorSuite.TestEnableNetwork_Listen(c) } func (s *singularitySuite) TestInject(c *C) { path, err := exec.LookPath("nsenter") if err != nil || path != "/var/lib/arvados/bin/nsenter" { c.Skip("looks like /var/lib/arvados/bin/nsenter is not installed -- re-run `arvados-server install`?") } s.executorSuite.TestInject(c) } var _ = Suite(&singularityStubSuite{}) // singularityStubSuite tests don't really invoke singularity, so we // can run them even if singularity is not installed. type singularityStubSuite struct{} func (s *singularityStubSuite) TestSingularityExecArgs(c *C) { e, err := newSingularityExecutor(c.Logf) c.Assert(err, IsNil) err = e.Create(containerSpec{ WorkingDir: "/WorkingDir", Env: map[string]string{"FOO": "bar"}, BindMounts: map[string]bindmount{"/mnt": {HostPath: "/hostpath", ReadOnly: true}}, EnableNetwork: false, GPUStack: "cuda", GPUDeviceCount: 3, VCPUs: 2, RAM: 12345678, }) c.Check(err, IsNil) e.imageFilename = "/fake/image.sif" cmd := e.execCmd("./singularity") expectArgs := []string{"./singularity", "exec", "--containall", "--cleanenv", "--pwd=/WorkingDir", "--net", "--network=none", "--nv"} if cgroupSupport["cpu"] { expectArgs = append(expectArgs, "--cpus", "2") } if cgroupSupport["memory"] { expectArgs = append(expectArgs, "--memory", "12345678") } expectArgs = append(expectArgs, "--bind", "/hostpath:/mnt:ro", "/fake/image.sif") c.Check(cmd.Args, DeepEquals, expectArgs) c.Check(cmd.Env, DeepEquals, []string{ "SINGULARITYENV_FOO=bar", "SINGULARITY_NO_EVAL=1", "XDG_RUNTIME_DIR=" + os.Getenv("XDG_RUNTIME_DIR"), "DBUS_SESSION_BUS_ADDRESS=" + os.Getenv("DBUS_SESSION_BUS_ADDRESS"), }) } func (s *singularitySuite) setupMount(c *C) (mountdir string) { mountdir = c.MkDir() cmd := exec.Command("arv-mount", "--foreground", "--read-write", "--storage-classes", "default", "--mount-by-pdh", "by_id", "--mount-by-id", "by_uuid", "--disable-event-listening", mountdir) cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr err := cmd.Start() c.Assert(err, IsNil) return } func (s *singularitySuite) teardownMount(c *C, mountdir string) { exec.Command("arv-mount", "--unmount", mountdir).Run() } type singularitySuiteLoadTestSetup struct { containerClient *arvados.Client imageCacheProject *arvados.Group dockerImageID string collectionName string } func (s *singularitySuite) setupLoadTest(c *C, e *singularityExecutor) (setup singularitySuiteLoadTestSetup) { // remove symlink and converted image already written by // (executorSuite)SetupTest os.Remove(e.tmpdir + "/image.tar") os.Remove(e.tmpdir + "/image.sif") setup.containerClient = arvados.NewClientFromEnv() setup.containerClient.AuthToken = arvadostest.ActiveTokenV2 var err error setup.imageCacheProject, err = e.getImageCacheProject(arvadostest.ActiveUserUUID, setup.containerClient) c.Assert(err, IsNil) setup.dockerImageID = "sha256:388056c9a6838deea3792e8f00705b35b439cf57b3c9c2634fb4e95cfc896de6" setup.collectionName = fmt.Sprintf("singularity image for %s", setup.dockerImageID) // Remove existing cache entry, if any. var cl arvados.CollectionList err = setup.containerClient.RequestAndDecode(&cl, arvados.EndpointCollectionList.Method, arvados.EndpointCollectionList.Path, nil, arvados.ListOptions{Filters: []arvados.Filter{ arvados.Filter{"owner_uuid", "=", setup.imageCacheProject.UUID}, arvados.Filter{"name", "=", setup.collectionName}, }, Limit: 1}) c.Assert(err, IsNil) if len(cl.Items) == 1 { setup.containerClient.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+cl.Items[0].UUID, nil, nil) } return } func (s *singularitySuite) checkCacheCollectionExists(c *C, setup singularitySuiteLoadTestSetup) { var cl arvados.CollectionList err := setup.containerClient.RequestAndDecode(&cl, arvados.EndpointCollectionList.Method, arvados.EndpointCollectionList.Path, nil, arvados.ListOptions{Filters: []arvados.Filter{ arvados.Filter{"owner_uuid", "=", setup.imageCacheProject.UUID}, arvados.Filter{"name", "=", setup.collectionName}, }, Limit: 1}) c.Assert(err, IsNil) if !c.Check(cl.Items, HasLen, 1) { return } c.Check(cl.Items[0].PortableDataHash, Not(Equals), "d41d8cd98f00b204e9800998ecf8427e+0") } func (s *singularitySuite) TestImageCache_New(c *C) { mountdir := s.setupMount(c) defer s.teardownMount(c, mountdir) e, err := newSingularityExecutor(c.Logf) c.Assert(err, IsNil) setup := s.setupLoadTest(c, e) err = e.LoadImage(setup.dockerImageID, arvadostest.BusyboxDockerImage(c), arvados.Container{RuntimeUserUUID: arvadostest.ActiveUserUUID}, mountdir, setup.containerClient) c.Check(err, IsNil) _, err = os.Stat(e.tmpdir + "/image.sif") c.Check(err, NotNil) c.Check(os.IsNotExist(err), Equals, true) s.checkCacheCollectionExists(c, setup) } func (s *singularitySuite) TestImageCache_SkipEmpty(c *C) { mountdir := s.setupMount(c) defer s.teardownMount(c, mountdir) e, err := newSingularityExecutor(c.Logf) c.Assert(err, IsNil) setup := s.setupLoadTest(c, e) var emptyCollection arvados.Collection exp := time.Now().Add(24 * 7 * 2 * time.Hour) err = setup.containerClient.RequestAndDecode(&emptyCollection, arvados.EndpointCollectionCreate.Method, arvados.EndpointCollectionCreate.Path, nil, map[string]interface{}{ "collection": map[string]string{ "owner_uuid": setup.imageCacheProject.UUID, "name": setup.collectionName, "trash_at": exp.UTC().Format(time.RFC3339), }, }) c.Assert(err, IsNil) err = e.LoadImage(setup.dockerImageID, arvadostest.BusyboxDockerImage(c), arvados.Container{RuntimeUserUUID: arvadostest.ActiveUserUUID}, mountdir, setup.containerClient) c.Check(err, IsNil) c.Check(e.imageFilename, Equals, e.tmpdir+"/image.sif") // tmpdir should contain symlink to docker image archive. tarListing, err := exec.Command("tar", "tvf", e.tmpdir+"/image.tar").CombinedOutput() c.Check(err, IsNil) c.Check(string(tarListing), Matches, `(?ms).*/layer.tar.*`) // converted singularity image should be non-empty. fi, err := os.Stat(e.imageFilename) if c.Check(err, IsNil) { c.Check(int(fi.Size()), Not(Equals), 0) } } func (s *singularitySuite) TestImageCache_Concurrency_1(c *C) { s.testImageCache(c, 1) } func (s *singularitySuite) TestImageCache_Concurrency_2(c *C) { s.testImageCache(c, 2) } func (s *singularitySuite) TestImageCache_Concurrency_10(c *C) { s.testImageCache(c, 10) } func (s *singularitySuite) testImageCache(c *C, concurrency int) { mountdirs := make([]string, concurrency) execs := make([]*singularityExecutor, concurrency) setups := make([]singularitySuiteLoadTestSetup, concurrency) for i := range execs { mountdirs[i] = s.setupMount(c) defer s.teardownMount(c, mountdirs[i]) e, err := newSingularityExecutor(c.Logf) c.Assert(err, IsNil) defer e.Close() execs[i] = e setups[i] = s.setupLoadTest(c, e) } var wg sync.WaitGroup for i, e := range execs { i, e := i, e wg.Add(1) go func() { defer wg.Done() err := e.LoadImage(setups[i].dockerImageID, arvadostest.BusyboxDockerImage(c), arvados.Container{RuntimeUserUUID: arvadostest.ActiveUserUUID}, mountdirs[i], setups[i].containerClient) c.Check(err, IsNil) }() } wg.Wait() for i, e := range execs { fusepath := strings.TrimPrefix(e.imageFilename, mountdirs[i]) // imageFilename should be in the fuse mount, not // e.tmpdir. c.Check(fusepath, Not(Equals), execs[0].imageFilename) // Below fuse mountpoint, paths should all be equal. fusepath0 := strings.TrimPrefix(execs[0].imageFilename, mountdirs[0]) c.Check(fusepath, Equals, fusepath0) } }