Merge branch '15964-fix-docs' refs #15964
[arvados.git] / lib / cloud / azure / azure_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4 //
5 //
6 // How to manually run individual tests against the real cloud:
7 //
8 // $ go test -v git.arvados.org/arvados.git/lib/cloud/azure -live-azure-cfg azconfig.yml -check.f=TestCreate
9 //
10 // Tests should be run individually and in the order they are listed in the file:
11 //
12 // Example azconfig.yml:
13 //
14 // ImageIDForTestSuite: "https://example.blob.core.windows.net/system/Microsoft.Compute/Images/images/zzzzz-compute-osDisk.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.vhd"
15 // DriverParameters:
16 //       SubscriptionID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
17 //       ClientID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
18 //       Location: centralus
19 //       CloudEnvironment: AzurePublicCloud
20 //       ClientSecret: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
21 //       TenantId: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
22 //       ResourceGroup: zzzzz
23 //       Network: zzzzz
24 //       Subnet: zzzzz-subnet-private
25 //       StorageAccount: example
26 //       BlobContainer: vhds
27 //       DeleteDanglingResourcesAfter: 20s
28 //       AdminUsername: crunch
29
30 package azure
31
32 import (
33         "context"
34         "encoding/json"
35         "errors"
36         "flag"
37         "io/ioutil"
38         "log"
39         "net"
40         "net/http"
41         "os"
42         "strings"
43         "testing"
44         "time"
45
46         "git.arvados.org/arvados.git/lib/cloud"
47         "git.arvados.org/arvados.git/lib/dispatchcloud/test"
48         "git.arvados.org/arvados.git/sdk/go/arvados"
49         "git.arvados.org/arvados.git/sdk/go/config"
50         "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-07-01/compute"
51         "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-06-01/network"
52         "github.com/Azure/azure-sdk-for-go/storage"
53         "github.com/Azure/go-autorest/autorest"
54         "github.com/Azure/go-autorest/autorest/azure"
55         "github.com/Azure/go-autorest/autorest/to"
56         "github.com/sirupsen/logrus"
57         "golang.org/x/crypto/ssh"
58         check "gopkg.in/check.v1"
59 )
60
61 // Gocheck boilerplate
62 func Test(t *testing.T) {
63         check.TestingT(t)
64 }
65
66 type AzureInstanceSetSuite struct{}
67
68 var _ = check.Suite(&AzureInstanceSetSuite{})
69
70 const testNamePrefix = "compute-test123-"
71
72 type VirtualMachinesClientStub struct{}
73
74 func (*VirtualMachinesClientStub) createOrUpdate(ctx context.Context,
75         resourceGroupName string,
76         VMName string,
77         parameters compute.VirtualMachine) (result compute.VirtualMachine, err error) {
78         parameters.ID = &VMName
79         parameters.Name = &VMName
80         return parameters, nil
81 }
82
83 func (*VirtualMachinesClientStub) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
84         return nil, nil
85 }
86
87 func (*VirtualMachinesClientStub) listComplete(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultIterator, err error) {
88         return compute.VirtualMachineListResultIterator{}, nil
89 }
90
91 type InterfacesClientStub struct{}
92
93 func (*InterfacesClientStub) createOrUpdate(ctx context.Context,
94         resourceGroupName string,
95         nicName string,
96         parameters network.Interface) (result network.Interface, err error) {
97         parameters.ID = to.StringPtr(nicName)
98         (*parameters.IPConfigurations)[0].PrivateIPAddress = to.StringPtr("192.168.5.5")
99         return parameters, nil
100 }
101
102 func (*InterfacesClientStub) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
103         return nil, nil
104 }
105
106 func (*InterfacesClientStub) listComplete(ctx context.Context, resourceGroupName string) (result network.InterfaceListResultIterator, err error) {
107         return network.InterfaceListResultIterator{}, nil
108 }
109
110 type BlobContainerStub struct{}
111
112 func (*BlobContainerStub) GetBlobReference(name string) *storage.Blob {
113         return nil
114 }
115
116 func (*BlobContainerStub) ListBlobs(params storage.ListBlobsParameters) (storage.BlobListResponse, error) {
117         return storage.BlobListResponse{}, nil
118 }
119
120 type testConfig struct {
121         ImageIDForTestSuite string
122         DriverParameters    json.RawMessage
123 }
124
125 var live = flag.String("live-azure-cfg", "", "Test with real azure API, provide config file")
126
127 func GetInstanceSet() (cloud.InstanceSet, cloud.ImageID, arvados.Cluster, error) {
128         cluster := arvados.Cluster{
129                 InstanceTypes: arvados.InstanceTypeMap(map[string]arvados.InstanceType{
130                         "tiny": arvados.InstanceType{
131                                 Name:         "tiny",
132                                 ProviderType: "Standard_D1_v2",
133                                 VCPUs:        1,
134                                 RAM:          4000000000,
135                                 Scratch:      10000000000,
136                                 Price:        .02,
137                                 Preemptible:  false,
138                         },
139                 })}
140         if *live != "" {
141                 var exampleCfg testConfig
142                 err := config.LoadFile(&exampleCfg, *live)
143                 if err != nil {
144                         return nil, cloud.ImageID(""), cluster, err
145                 }
146
147                 ap, err := newAzureInstanceSet(exampleCfg.DriverParameters, "test123", nil, logrus.StandardLogger())
148                 return ap, cloud.ImageID(exampleCfg.ImageIDForTestSuite), cluster, err
149         }
150         ap := azureInstanceSet{
151                 azconfig: azureInstanceSetConfig{
152                         BlobContainer: "vhds",
153                 },
154                 dispatcherID: "test123",
155                 namePrefix:   testNamePrefix,
156                 logger:       logrus.StandardLogger(),
157                 deleteNIC:    make(chan string),
158                 deleteBlob:   make(chan storage.Blob),
159                 deleteDisk:   make(chan compute.Disk),
160         }
161         ap.ctx, ap.stopFunc = context.WithCancel(context.Background())
162         ap.vmClient = &VirtualMachinesClientStub{}
163         ap.netClient = &InterfacesClientStub{}
164         ap.blobcont = &BlobContainerStub{}
165         return &ap, cloud.ImageID("blob"), cluster, nil
166 }
167
168 func (*AzureInstanceSetSuite) TestCreate(c *check.C) {
169         ap, img, cluster, err := GetInstanceSet()
170         if err != nil {
171                 c.Fatal("Error making provider", err)
172         }
173
174         pk, _ := test.LoadTestKey(c, "../../dispatchcloud/test/sshkey_dispatch")
175         c.Assert(err, check.IsNil)
176
177         inst, err := ap.Create(cluster.InstanceTypes["tiny"],
178                 img, map[string]string{
179                         "TestTagName": "test tag value",
180                 }, "umask 0600; echo -n test-file-data >/var/run/test-file", pk)
181
182         c.Assert(err, check.IsNil)
183
184         tags := inst.Tags()
185         c.Check(tags["TestTagName"], check.Equals, "test tag value")
186         c.Logf("inst.String()=%v Address()=%v Tags()=%v", inst.String(), inst.Address(), tags)
187
188 }
189
190 func (*AzureInstanceSetSuite) TestListInstances(c *check.C) {
191         ap, _, _, err := GetInstanceSet()
192         if err != nil {
193                 c.Fatal("Error making provider", err)
194         }
195
196         l, err := ap.Instances(nil)
197
198         c.Assert(err, check.IsNil)
199
200         for _, i := range l {
201                 tg := i.Tags()
202                 log.Printf("%v %v %v", i.String(), i.Address(), tg)
203         }
204 }
205
206 func (*AzureInstanceSetSuite) TestManageNics(c *check.C) {
207         ap, _, _, err := GetInstanceSet()
208         if err != nil {
209                 c.Fatal("Error making provider", err)
210         }
211
212         ap.(*azureInstanceSet).manageNics()
213         ap.Stop()
214 }
215
216 func (*AzureInstanceSetSuite) TestManageBlobs(c *check.C) {
217         ap, _, _, err := GetInstanceSet()
218         if err != nil {
219                 c.Fatal("Error making provider", err)
220         }
221
222         ap.(*azureInstanceSet).manageBlobs()
223         ap.Stop()
224 }
225
226 func (*AzureInstanceSetSuite) TestDestroyInstances(c *check.C) {
227         ap, _, _, err := GetInstanceSet()
228         if err != nil {
229                 c.Fatal("Error making provider", err)
230         }
231
232         l, err := ap.Instances(nil)
233         c.Assert(err, check.IsNil)
234
235         for _, i := range filterInstances(c, l) {
236                 c.Check(i.Destroy(), check.IsNil)
237         }
238 }
239
240 func (*AzureInstanceSetSuite) TestDeleteFake(c *check.C) {
241         ap, _, _, err := GetInstanceSet()
242         if err != nil {
243                 c.Fatal("Error making provider", err)
244         }
245
246         _, err = ap.(*azureInstanceSet).netClient.delete(context.Background(), "fakefakefake", "fakefakefake")
247
248         de, ok := err.(autorest.DetailedError)
249         if ok {
250                 rq := de.Original.(*azure.RequestError)
251
252                 log.Printf("%v %q %q", rq.Response.StatusCode, rq.ServiceError.Code, rq.ServiceError.Message)
253         }
254 }
255
256 func (*AzureInstanceSetSuite) TestWrapError(c *check.C) {
257         retryError := autorest.DetailedError{
258                 Original: &azure.RequestError{
259                         DetailedError: autorest.DetailedError{
260                                 Response: &http.Response{
261                                         StatusCode: 429,
262                                         Header:     map[string][]string{"Retry-After": []string{"123"}},
263                                 },
264                         },
265                         ServiceError: &azure.ServiceError{},
266                 },
267         }
268         wrapped := wrapAzureError(retryError)
269         _, ok := wrapped.(cloud.RateLimitError)
270         c.Check(ok, check.Equals, true)
271
272         quotaError := autorest.DetailedError{
273                 Original: &azure.RequestError{
274                         DetailedError: autorest.DetailedError{
275                                 Response: &http.Response{
276                                         StatusCode: 503,
277                                 },
278                         },
279                         ServiceError: &azure.ServiceError{
280                                 Message: "No more quota",
281                         },
282                 },
283         }
284         wrapped = wrapAzureError(quotaError)
285         _, ok = wrapped.(cloud.QuotaError)
286         c.Check(ok, check.Equals, true)
287 }
288
289 func (*AzureInstanceSetSuite) TestSetTags(c *check.C) {
290         ap, _, _, err := GetInstanceSet()
291         if err != nil {
292                 c.Fatal("Error making provider", err)
293         }
294
295         l, err := ap.Instances(nil)
296         c.Assert(err, check.IsNil)
297         l = filterInstances(c, l)
298         if len(l) > 0 {
299                 err = l[0].SetTags(map[string]string{"foo": "bar"})
300                 if err != nil {
301                         c.Fatal("Error setting tags", err)
302                 }
303         }
304
305         l, err = ap.Instances(nil)
306         c.Assert(err, check.IsNil)
307         l = filterInstances(c, l)
308
309         if len(l) > 0 {
310                 tg := l[0].Tags()
311                 log.Printf("tags are %v", tg)
312         }
313 }
314
315 func (*AzureInstanceSetSuite) TestSSH(c *check.C) {
316         ap, _, _, err := GetInstanceSet()
317         if err != nil {
318                 c.Fatal("Error making provider", err)
319         }
320         l, err := ap.Instances(nil)
321         c.Assert(err, check.IsNil)
322         l = filterInstances(c, l)
323
324         if len(l) > 0 {
325                 sshclient, err := SetupSSHClient(c, l[0])
326                 c.Assert(err, check.IsNil)
327                 defer sshclient.Conn.Close()
328
329                 sess, err := sshclient.NewSession()
330                 c.Assert(err, check.IsNil)
331                 defer sess.Close()
332                 _, err = sess.Output("find /var/run/test-file -maxdepth 0 -user root -perm 0600")
333                 c.Assert(err, check.IsNil)
334
335                 sess, err = sshclient.NewSession()
336                 c.Assert(err, check.IsNil)
337                 defer sess.Close()
338                 out, err := sess.Output("sudo cat /var/run/test-file")
339                 c.Assert(err, check.IsNil)
340                 c.Check(string(out), check.Equals, "test-file-data")
341         }
342 }
343
344 func SetupSSHClient(c *check.C, inst cloud.Instance) (*ssh.Client, error) {
345         addr := inst.Address() + ":2222"
346         if addr == "" {
347                 return nil, errors.New("instance has no address")
348         }
349
350         f, err := os.Open("azconfig_sshkey")
351         c.Assert(err, check.IsNil)
352
353         keybytes, err := ioutil.ReadAll(f)
354         c.Assert(err, check.IsNil)
355
356         priv, err := ssh.ParsePrivateKey(keybytes)
357         c.Assert(err, check.IsNil)
358
359         var receivedKey ssh.PublicKey
360         client, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
361                 User: "crunch",
362                 Auth: []ssh.AuthMethod{
363                         ssh.PublicKeys(priv),
364                 },
365                 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
366                         receivedKey = key
367                         return nil
368                 },
369                 Timeout: time.Minute,
370         })
371
372         if err != nil {
373                 return nil, err
374         } else if receivedKey == nil {
375                 return nil, errors.New("BUG: key was never provided to HostKeyCallback")
376         }
377
378         err = inst.VerifyHostKey(receivedKey, client)
379         c.Assert(err, check.IsNil)
380
381         return client, nil
382 }
383
384 func filterInstances(c *check.C, instances []cloud.Instance) []cloud.Instance {
385         var r []cloud.Instance
386         for _, i := range instances {
387                 if !strings.HasPrefix(i.String(), testNamePrefix) {
388                         c.Logf("ignoring instance %s", i)
389                         continue
390                 }
391                 r = append(r, i)
392         }
393         return r
394 }