Merge branch '19792-pysdk-cookbook'
[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": {
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                         "tinyp": {
140                                 Name:         "tiny",
141                                 ProviderType: "Standard_D1_v2",
142                                 VCPUs:        1,
143                                 RAM:          4000000000,
144                                 Scratch:      10000000000,
145                                 Price:        .002,
146                                 Preemptible:  true,
147                         },
148                 })}
149         if *live != "" {
150                 var exampleCfg testConfig
151                 err := config.LoadFile(&exampleCfg, *live)
152                 if err != nil {
153                         return nil, cloud.ImageID(""), cluster, err
154                 }
155
156                 ap, err := newAzureInstanceSet(exampleCfg.DriverParameters, "test123", nil, logrus.StandardLogger())
157                 return ap, cloud.ImageID(exampleCfg.ImageIDForTestSuite), cluster, err
158         }
159         ap := azureInstanceSet{
160                 azconfig: azureInstanceSetConfig{
161                         BlobContainer: "vhds",
162                 },
163                 dispatcherID: "test123",
164                 namePrefix:   testNamePrefix,
165                 logger:       logrus.StandardLogger(),
166                 deleteNIC:    make(chan string),
167                 deleteBlob:   make(chan storage.Blob),
168                 deleteDisk:   make(chan compute.Disk),
169         }
170         ap.ctx, ap.stopFunc = context.WithCancel(context.Background())
171         ap.vmClient = &VirtualMachinesClientStub{}
172         ap.netClient = &InterfacesClientStub{}
173         ap.blobcont = &BlobContainerStub{}
174         return &ap, cloud.ImageID("blob"), cluster, nil
175 }
176
177 func (*AzureInstanceSetSuite) TestCreate(c *check.C) {
178         ap, img, cluster, err := GetInstanceSet()
179         if err != nil {
180                 c.Fatal("Error making provider", err)
181         }
182
183         pk, _ := test.LoadTestKey(c, "../../dispatchcloud/test/sshkey_dispatch")
184         c.Assert(err, check.IsNil)
185
186         inst, err := ap.Create(cluster.InstanceTypes["tiny"],
187                 img, map[string]string{
188                         "TestTagName": "test tag value",
189                 }, "umask 0600; echo -n test-file-data >/var/run/test-file", pk)
190
191         c.Assert(err, check.IsNil)
192
193         tags := inst.Tags()
194         c.Check(tags["TestTagName"], check.Equals, "test tag value")
195         c.Logf("inst.String()=%v Address()=%v Tags()=%v", inst.String(), inst.Address(), tags)
196
197         instPreemptable, err := ap.Create(cluster.InstanceTypes["tinyp"],
198                 img, map[string]string{
199                         "TestTagName": "test tag value",
200                 }, "umask 0600; echo -n test-file-data >/var/run/test-file", pk)
201
202         c.Assert(err, check.IsNil)
203
204         tags = instPreemptable.Tags()
205         c.Check(tags["TestTagName"], check.Equals, "test tag value")
206         c.Logf("instPreemptable.String()=%v Address()=%v Tags()=%v", instPreemptable.String(), instPreemptable.Address(), tags)
207
208 }
209
210 func (*AzureInstanceSetSuite) TestListInstances(c *check.C) {
211         ap, _, _, err := GetInstanceSet()
212         if err != nil {
213                 c.Fatal("Error making provider", err)
214         }
215
216         l, err := ap.Instances(nil)
217
218         c.Assert(err, check.IsNil)
219
220         for _, i := range l {
221                 tg := i.Tags()
222                 log.Printf("%v %v %v", i.String(), i.Address(), tg)
223         }
224 }
225
226 func (*AzureInstanceSetSuite) TestManageNics(c *check.C) {
227         ap, _, _, err := GetInstanceSet()
228         if err != nil {
229                 c.Fatal("Error making provider", err)
230         }
231
232         ap.(*azureInstanceSet).manageNics()
233         ap.Stop()
234 }
235
236 func (*AzureInstanceSetSuite) TestManageBlobs(c *check.C) {
237         ap, _, _, err := GetInstanceSet()
238         if err != nil {
239                 c.Fatal("Error making provider", err)
240         }
241
242         ap.(*azureInstanceSet).manageBlobs()
243         ap.Stop()
244 }
245
246 func (*AzureInstanceSetSuite) TestDestroyInstances(c *check.C) {
247         ap, _, _, err := GetInstanceSet()
248         if err != nil {
249                 c.Fatal("Error making provider", err)
250         }
251
252         l, err := ap.Instances(nil)
253         c.Assert(err, check.IsNil)
254
255         for _, i := range filterInstances(c, l) {
256                 c.Check(i.Destroy(), check.IsNil)
257         }
258 }
259
260 func (*AzureInstanceSetSuite) TestDeleteFake(c *check.C) {
261         ap, _, _, err := GetInstanceSet()
262         if err != nil {
263                 c.Fatal("Error making provider", err)
264         }
265
266         _, err = ap.(*azureInstanceSet).netClient.delete(context.Background(), "fakefakefake", "fakefakefake")
267
268         de, ok := err.(autorest.DetailedError)
269         if ok {
270                 rq := de.Original.(*azure.RequestError)
271
272                 log.Printf("%v %q %q", rq.Response.StatusCode, rq.ServiceError.Code, rq.ServiceError.Message)
273         }
274 }
275
276 func (*AzureInstanceSetSuite) TestWrapError(c *check.C) {
277         retryError := autorest.DetailedError{
278                 Original: &azure.RequestError{
279                         DetailedError: autorest.DetailedError{
280                                 Response: &http.Response{
281                                         StatusCode: 429,
282                                         Header:     map[string][]string{"Retry-After": {"123"}},
283                                 },
284                         },
285                         ServiceError: &azure.ServiceError{},
286                 },
287         }
288         wrapped := wrapAzureError(retryError)
289         _, ok := wrapped.(cloud.RateLimitError)
290         c.Check(ok, check.Equals, true)
291
292         quotaError := autorest.DetailedError{
293                 Original: &azure.RequestError{
294                         DetailedError: autorest.DetailedError{
295                                 Response: &http.Response{
296                                         StatusCode: 503,
297                                 },
298                         },
299                         ServiceError: &azure.ServiceError{
300                                 Message: "No more quota",
301                         },
302                 },
303         }
304         wrapped = wrapAzureError(quotaError)
305         _, ok = wrapped.(cloud.QuotaError)
306         c.Check(ok, check.Equals, true)
307 }
308
309 func (*AzureInstanceSetSuite) TestSetTags(c *check.C) {
310         ap, _, _, err := GetInstanceSet()
311         if err != nil {
312                 c.Fatal("Error making provider", err)
313         }
314
315         l, err := ap.Instances(nil)
316         c.Assert(err, check.IsNil)
317         l = filterInstances(c, l)
318         if len(l) > 0 {
319                 err = l[0].SetTags(map[string]string{"foo": "bar"})
320                 if err != nil {
321                         c.Fatal("Error setting tags", err)
322                 }
323         }
324
325         l, err = ap.Instances(nil)
326         c.Assert(err, check.IsNil)
327         l = filterInstances(c, l)
328
329         if len(l) > 0 {
330                 tg := l[0].Tags()
331                 log.Printf("tags are %v", tg)
332         }
333 }
334
335 func (*AzureInstanceSetSuite) TestSSH(c *check.C) {
336         ap, _, _, err := GetInstanceSet()
337         if err != nil {
338                 c.Fatal("Error making provider", err)
339         }
340         l, err := ap.Instances(nil)
341         c.Assert(err, check.IsNil)
342         l = filterInstances(c, l)
343
344         if len(l) > 0 {
345                 sshclient, err := SetupSSHClient(c, l[0])
346                 c.Assert(err, check.IsNil)
347                 defer sshclient.Conn.Close()
348
349                 sess, err := sshclient.NewSession()
350                 c.Assert(err, check.IsNil)
351                 defer sess.Close()
352                 _, err = sess.Output("find /var/run/test-file -maxdepth 0 -user root -perm 0600")
353                 c.Assert(err, check.IsNil)
354
355                 sess, err = sshclient.NewSession()
356                 c.Assert(err, check.IsNil)
357                 defer sess.Close()
358                 out, err := sess.Output("sudo cat /var/run/test-file")
359                 c.Assert(err, check.IsNil)
360                 c.Check(string(out), check.Equals, "test-file-data")
361         }
362 }
363
364 func SetupSSHClient(c *check.C, inst cloud.Instance) (*ssh.Client, error) {
365         addr := inst.Address() + ":2222"
366         if addr == "" {
367                 return nil, errors.New("instance has no address")
368         }
369
370         f, err := os.Open("azconfig_sshkey")
371         c.Assert(err, check.IsNil)
372
373         keybytes, err := ioutil.ReadAll(f)
374         c.Assert(err, check.IsNil)
375
376         priv, err := ssh.ParsePrivateKey(keybytes)
377         c.Assert(err, check.IsNil)
378
379         var receivedKey ssh.PublicKey
380         client, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
381                 User: "crunch",
382                 Auth: []ssh.AuthMethod{
383                         ssh.PublicKeys(priv),
384                 },
385                 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
386                         receivedKey = key
387                         return nil
388                 },
389                 Timeout: time.Minute,
390         })
391
392         if err != nil {
393                 return nil, err
394         } else if receivedKey == nil {
395                 return nil, errors.New("BUG: key was never provided to HostKeyCallback")
396         }
397
398         err = inst.VerifyHostKey(receivedKey, client)
399         c.Assert(err, check.IsNil)
400
401         return client, nil
402 }
403
404 func filterInstances(c *check.C, instances []cloud.Instance) []cloud.Instance {
405         var r []cloud.Instance
406         for _, i := range instances {
407                 if !strings.HasPrefix(i.String(), testNamePrefix) {
408                         c.Logf("ignoring instance %s", i)
409                         continue
410                 }
411                 r = append(r, i)
412         }
413         return r
414 }