// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 // // // How to manually run individual tests against the real cloud: // // $ go test -v git.arvados.org/arvados.git/lib/cloud/azure -live-azure-cfg azconfig.yml -check.f=TestCreate // // Tests should be run individually and in the order they are listed in the file: // // Example azconfig.yml: // // ImageIDForTestSuite: "https://example.blob.core.windows.net/system/Microsoft.Compute/Images/images/zzzzz-compute-osDisk.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.vhd" // DriverParameters: // SubscriptionID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX // ClientID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX // Location: centralus // CloudEnvironment: AzurePublicCloud // ClientSecret: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // TenantId: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX // ResourceGroup: zzzzz // Network: zzzzz // Subnet: zzzzz-subnet-private // StorageAccount: example // BlobContainer: vhds // DeleteDanglingResourcesAfter: 20s // AdminUsername: crunch package azure import ( "context" "encoding/json" "errors" "flag" "io/ioutil" "log" "net" "net/http" "os" "strings" "testing" "time" "git.arvados.org/arvados.git/lib/cloud" "git.arvados.org/arvados.git/lib/dispatchcloud/test" "git.arvados.org/arvados.git/sdk/go/arvados" "git.arvados.org/arvados.git/sdk/go/config" "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-06-01/network" "github.com/Azure/azure-sdk-for-go/storage" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/to" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" check "gopkg.in/check.v1" ) // Gocheck boilerplate func Test(t *testing.T) { check.TestingT(t) } type AzureInstanceSetSuite struct{} var _ = check.Suite(&AzureInstanceSetSuite{}) const testNamePrefix = "compute-test123-" type VirtualMachinesClientStub struct { vmParameters compute.VirtualMachine } func (stub *VirtualMachinesClientStub) createOrUpdate(ctx context.Context, resourceGroupName string, VMName string, parameters compute.VirtualMachine) (result compute.VirtualMachine, err error) { parameters.ID = &VMName parameters.Name = &VMName stub.vmParameters = parameters return parameters, nil } func (*VirtualMachinesClientStub) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) { return nil, nil } func (*VirtualMachinesClientStub) listComplete(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultIterator, err error) { return compute.VirtualMachineListResultIterator{}, nil } type InterfacesClientStub struct{} func (*InterfacesClientStub) createOrUpdate(ctx context.Context, resourceGroupName string, nicName string, parameters network.Interface) (result network.Interface, err error) { parameters.ID = to.StringPtr(nicName) (*parameters.IPConfigurations)[0].PrivateIPAddress = to.StringPtr("192.168.5.5") return parameters, nil } func (*InterfacesClientStub) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) { return nil, nil } func (*InterfacesClientStub) listComplete(ctx context.Context, resourceGroupName string) (result network.InterfaceListResultIterator, err error) { return network.InterfaceListResultIterator{}, nil } type BlobContainerStub struct{} func (*BlobContainerStub) GetBlobReference(name string) *storage.Blob { return nil } func (*BlobContainerStub) ListBlobs(params storage.ListBlobsParameters) (storage.BlobListResponse, error) { return storage.BlobListResponse{}, nil } type testConfig struct { ImageIDForTestSuite string DriverParameters json.RawMessage } var live = flag.String("live-azure-cfg", "", "Test with real azure API, provide config file") func GetInstanceSet() (*azureInstanceSet, cloud.ImageID, arvados.Cluster, error) { cluster := arvados.Cluster{ InstanceTypes: arvados.InstanceTypeMap(map[string]arvados.InstanceType{ "tiny": { Name: "tiny", ProviderType: "Standard_D1_v2", VCPUs: 1, RAM: 4000000000, Scratch: 10000000000, Price: .02, Preemptible: false, }, "tinyp": { Name: "tiny", ProviderType: "Standard_D1_v2", VCPUs: 1, RAM: 4000000000, Scratch: 10000000000, Price: .002, Preemptible: true, }, })} if *live != "" { var exampleCfg testConfig err := config.LoadFile(&exampleCfg, *live) if err != nil { return nil, cloud.ImageID(""), cluster, err } ap, err := newAzureInstanceSet(exampleCfg.DriverParameters, "test123", nil, logrus.StandardLogger(), nil) return ap.(*azureInstanceSet), cloud.ImageID(exampleCfg.ImageIDForTestSuite), cluster, err } ap := azureInstanceSet{ azconfig: azureInstanceSetConfig{ BlobContainer: "vhds", }, dispatcherID: "test123", namePrefix: testNamePrefix, logger: logrus.StandardLogger(), deleteNIC: make(chan string), deleteBlob: make(chan storage.Blob), deleteDisk: make(chan compute.Disk), } ap.ctx, ap.stopFunc = context.WithCancel(context.Background()) ap.vmClient = &VirtualMachinesClientStub{} ap.netClient = &InterfacesClientStub{} ap.blobcont = &BlobContainerStub{} return &ap, cloud.ImageID("blob"), cluster, nil } func (*AzureInstanceSetSuite) TestCreate(c *check.C) { ap, img, cluster, err := GetInstanceSet() if err != nil { c.Fatal("Error making provider", err) } pk, _ := test.LoadTestKey(c, "../../dispatchcloud/test/sshkey_dispatch") c.Assert(err, check.IsNil) inst, err := ap.Create(cluster.InstanceTypes["tiny"], img, map[string]string{ "TestTagName": "test tag value", }, "umask 0600; echo -n test-file-data >/var/run/test-file", pk) c.Assert(err, check.IsNil) tags := inst.Tags() c.Check(tags["TestTagName"], check.Equals, "test tag value") c.Logf("inst.String()=%v Address()=%v Tags()=%v", inst.String(), inst.Address(), tags) if *live == "" { c.Check(ap.vmClient.(*VirtualMachinesClientStub).vmParameters.VirtualMachineProperties.OsProfile.LinuxConfiguration.SSH, check.NotNil) } instPreemptable, err := ap.Create(cluster.InstanceTypes["tinyp"], img, map[string]string{ "TestTagName": "test tag value", }, "umask 0600; echo -n test-file-data >/var/run/test-file", nil) c.Assert(err, check.IsNil) tags = instPreemptable.Tags() c.Check(tags["TestTagName"], check.Equals, "test tag value") c.Logf("instPreemptable.String()=%v Address()=%v Tags()=%v", instPreemptable.String(), instPreemptable.Address(), tags) if *live == "" { // Should not have set SSH option, because publickey // arg was nil c.Check(ap.vmClient.(*VirtualMachinesClientStub).vmParameters.VirtualMachineProperties.OsProfile.LinuxConfiguration.SSH, check.IsNil) } } func (*AzureInstanceSetSuite) TestListInstances(c *check.C) { ap, _, _, err := GetInstanceSet() if err != nil { c.Fatal("Error making provider", err) } l, err := ap.Instances(nil) c.Assert(err, check.IsNil) for _, i := range l { tg := i.Tags() log.Printf("%v %v %v", i.String(), i.Address(), tg) } } func (*AzureInstanceSetSuite) TestManageNics(c *check.C) { ap, _, _, err := GetInstanceSet() if err != nil { c.Fatal("Error making provider", err) } ap.manageNics() ap.Stop() } func (*AzureInstanceSetSuite) TestManageBlobs(c *check.C) { ap, _, _, err := GetInstanceSet() if err != nil { c.Fatal("Error making provider", err) } ap.manageBlobs() ap.Stop() } func (*AzureInstanceSetSuite) TestDestroyInstances(c *check.C) { ap, _, _, err := GetInstanceSet() if err != nil { c.Fatal("Error making provider", err) } l, err := ap.Instances(nil) c.Assert(err, check.IsNil) for _, i := range filterInstances(c, l) { c.Check(i.Destroy(), check.IsNil) } } func (*AzureInstanceSetSuite) TestDeleteFake(c *check.C) { ap, _, _, err := GetInstanceSet() if err != nil { c.Fatal("Error making provider", err) } _, err = ap.netClient.delete(context.Background(), "fakefakefake", "fakefakefake") de, ok := err.(autorest.DetailedError) if ok { rq := de.Original.(*azure.RequestError) log.Printf("%v %q %q", rq.Response.StatusCode, rq.ServiceError.Code, rq.ServiceError.Message) } } func (*AzureInstanceSetSuite) TestWrapError(c *check.C) { retryError := autorest.DetailedError{ Original: &azure.RequestError{ DetailedError: autorest.DetailedError{ Response: &http.Response{ StatusCode: 429, Header: map[string][]string{"Retry-After": {"123"}}, }, }, ServiceError: &azure.ServiceError{}, }, } wrapped := wrapAzureError(retryError) _, ok := wrapped.(cloud.RateLimitError) c.Check(ok, check.Equals, true) quotaError := autorest.DetailedError{ Original: &azure.RequestError{ DetailedError: autorest.DetailedError{ Response: &http.Response{ StatusCode: 503, }, }, ServiceError: &azure.ServiceError{ Message: "No more quota", }, }, } wrapped = wrapAzureError(quotaError) _, ok = wrapped.(cloud.QuotaError) c.Check(ok, check.Equals, true) } func (*AzureInstanceSetSuite) TestSetTags(c *check.C) { ap, _, _, err := GetInstanceSet() if err != nil { c.Fatal("Error making provider", err) } l, err := ap.Instances(nil) c.Assert(err, check.IsNil) l = filterInstances(c, l) if len(l) > 0 { err = l[0].SetTags(map[string]string{"foo": "bar"}) if err != nil { c.Fatal("Error setting tags", err) } } l, err = ap.Instances(nil) c.Assert(err, check.IsNil) l = filterInstances(c, l) if len(l) > 0 { tg := l[0].Tags() log.Printf("tags are %v", tg) } } func (*AzureInstanceSetSuite) TestSSH(c *check.C) { ap, _, _, err := GetInstanceSet() if err != nil { c.Fatal("Error making provider", err) } l, err := ap.Instances(nil) c.Assert(err, check.IsNil) l = filterInstances(c, l) if len(l) > 0 { sshclient, err := SetupSSHClient(c, l[0]) c.Assert(err, check.IsNil) defer sshclient.Conn.Close() sess, err := sshclient.NewSession() c.Assert(err, check.IsNil) defer sess.Close() _, err = sess.Output("find /var/run/test-file -maxdepth 0 -user root -perm 0600") c.Assert(err, check.IsNil) sess, err = sshclient.NewSession() c.Assert(err, check.IsNil) defer sess.Close() out, err := sess.Output("sudo cat /var/run/test-file") c.Assert(err, check.IsNil) c.Check(string(out), check.Equals, "test-file-data") } } func SetupSSHClient(c *check.C, inst cloud.Instance) (*ssh.Client, error) { addr := inst.Address() + ":2222" if addr == "" { return nil, errors.New("instance has no address") } f, err := os.Open("azconfig_sshkey") c.Assert(err, check.IsNil) keybytes, err := ioutil.ReadAll(f) c.Assert(err, check.IsNil) priv, err := ssh.ParsePrivateKey(keybytes) c.Assert(err, check.IsNil) var receivedKey ssh.PublicKey client, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{ User: "crunch", Auth: []ssh.AuthMethod{ ssh.PublicKeys(priv), }, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { receivedKey = key return nil }, Timeout: time.Minute, }) if err != nil { return nil, err } else if receivedKey == nil { return nil, errors.New("BUG: key was never provided to HostKeyCallback") } err = inst.VerifyHostKey(receivedKey, client) c.Assert(err, check.IsNil) return client, nil } func filterInstances(c *check.C, instances []cloud.Instance) []cloud.Instance { var r []cloud.Instance for _, i := range instances { if !strings.HasPrefix(i.String(), testNamePrefix) { c.Logf("ignoring instance %s", i) continue } r = append(r, i) } return r }