14844: Azure fixes
[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.curoverse.com/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         "testing"
43         "time"
44
45         "git.curoverse.com/arvados.git/lib/cloud"
46         "git.curoverse.com/arvados.git/sdk/go/arvados"
47         "git.curoverse.com/arvados.git/sdk/go/config"
48         "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-06-01/compute"
49         "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-06-01/network"
50         "github.com/Azure/azure-sdk-for-go/storage"
51         "github.com/Azure/go-autorest/autorest"
52         "github.com/Azure/go-autorest/autorest/azure"
53         "github.com/Azure/go-autorest/autorest/to"
54         "github.com/sirupsen/logrus"
55         "golang.org/x/crypto/ssh"
56         check "gopkg.in/check.v1"
57 )
58
59 // Gocheck boilerplate
60 func Test(t *testing.T) {
61         check.TestingT(t)
62 }
63
64 type AzureInstanceSetSuite struct{}
65
66 var _ = check.Suite(&AzureInstanceSetSuite{})
67
68 type VirtualMachinesClientStub struct{}
69
70 var testKey = []byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLQS1ExT2+WjA0d/hntEAyAtgeN1W2ik2QX8c2zO6HjlPHWXL92r07W0WMuDib40Pcevpi1BXeBWXA9ZB5KKMJB+ukaAu22KklnQuUmNvk6ZXnPKSkGxuCYvPQb08WhHf3p1VxiKfP3iauedBDM4x9/bkJohlBBQiFXzNUcQ+a6rKiMzmJN2gbL8ncyUzc+XQ5q4JndTwTGtOlzDiGOc9O4z5Dd76wtAVJneOuuNpwfFRVHThpJM6VThpCZOnl8APaceWXKeuwOuCae3COZMz++xQfxOfZ9Z8aIwo+TlQhsRaNfZ4Vjrop6ej8dtfZtgUFKfbXEOYaHrGrWGotFDTD example@example`)
71
72 func (*VirtualMachinesClientStub) createOrUpdate(ctx context.Context,
73         resourceGroupName string,
74         VMName string,
75         parameters compute.VirtualMachine) (result compute.VirtualMachine, err error) {
76         parameters.ID = &VMName
77         parameters.Name = &VMName
78         return parameters, nil
79 }
80
81 func (*VirtualMachinesClientStub) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
82         return nil, nil
83 }
84
85 func (*VirtualMachinesClientStub) listComplete(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultIterator, err error) {
86         return compute.VirtualMachineListResultIterator{}, nil
87 }
88
89 type InterfacesClientStub struct{}
90
91 func (*InterfacesClientStub) createOrUpdate(ctx context.Context,
92         resourceGroupName string,
93         nicName string,
94         parameters network.Interface) (result network.Interface, err error) {
95         parameters.ID = to.StringPtr(nicName)
96         (*parameters.IPConfigurations)[0].PrivateIPAddress = to.StringPtr("192.168.5.5")
97         return parameters, nil
98 }
99
100 func (*InterfacesClientStub) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
101         return nil, nil
102 }
103
104 func (*InterfacesClientStub) listComplete(ctx context.Context, resourceGroupName string) (result network.InterfaceListResultIterator, err error) {
105         return network.InterfaceListResultIterator{}, nil
106 }
107
108 type BlobContainerStub struct{}
109
110 func (*BlobContainerStub) GetBlobReference(name string) *storage.Blob {
111         return nil
112 }
113
114 func (*BlobContainerStub) ListBlobs(params storage.ListBlobsParameters) (storage.BlobListResponse, error) {
115         return storage.BlobListResponse{}, nil
116 }
117
118 type testConfig struct {
119         ImageIDForTestSuite string
120         DriverParameters    json.RawMessage
121 }
122
123 var live = flag.String("live-azure-cfg", "", "Test with real azure API, provide config file")
124
125 func GetInstanceSet() (cloud.InstanceSet, cloud.ImageID, arvados.Cluster, error) {
126         cluster := arvados.Cluster{
127                 InstanceTypes: arvados.InstanceTypeMap(map[string]arvados.InstanceType{
128                         "tiny": arvados.InstanceType{
129                                 Name:         "tiny",
130                                 ProviderType: "Standard_D1_v2",
131                                 VCPUs:        1,
132                                 RAM:          4000000000,
133                                 Scratch:      10000000000,
134                                 Price:        .02,
135                                 Preemptible:  false,
136                         },
137                 })}
138         if *live != "" {
139                 var exampleCfg testConfig
140                 err := config.LoadFile(&exampleCfg, *live)
141                 if err != nil {
142                         return nil, cloud.ImageID(""), cluster, err
143                 }
144
145                 ap, err := newAzureInstanceSet(exampleCfg.DriverParameters, "test123", logrus.StandardLogger())
146                 return ap, cloud.ImageID(exampleCfg.ImageIDForTestSuite), cluster, err
147         }
148         ap := azureInstanceSet{
149                 azconfig: azureInstanceSetConfig{
150                         BlobContainer: "vhds",
151                 },
152                 dispatcherID: "test123",
153                 namePrefix:   "compute-test123-",
154                 logger:       logrus.StandardLogger(),
155                 deleteNIC:    make(chan string),
156                 deleteBlob:   make(chan storage.Blob),
157         }
158         ap.ctx, ap.stopFunc = context.WithCancel(context.Background())
159         ap.vmClient = &VirtualMachinesClientStub{}
160         ap.netClient = &InterfacesClientStub{}
161         ap.blobcont = &BlobContainerStub{}
162         return &ap, cloud.ImageID("blob"), cluster, nil
163 }
164
165 func (*AzureInstanceSetSuite) TestCreate(c *check.C) {
166         ap, img, cluster, err := GetInstanceSet()
167         if err != nil {
168                 c.Fatal("Error making provider", err)
169         }
170
171         pk, _, _, _, err := ssh.ParseAuthorizedKey(testKey)
172         c.Assert(err, check.IsNil)
173
174         inst, err := ap.Create(cluster.InstanceTypes["tiny"],
175                 img, map[string]string{
176                         "TestTagName": "test tag value",
177                 }, "umask 0600; echo -n test-file-data >/var/run/test-file", pk)
178
179         c.Assert(err, check.IsNil)
180
181         tags := inst.Tags()
182         c.Check(tags["TestTagName"], check.Equals, "test tag value")
183         c.Logf("inst.String()=%v Address()=%v Tags()=%v", inst.String(), inst.Address(), tags)
184
185 }
186
187 func (*AzureInstanceSetSuite) TestListInstances(c *check.C) {
188         ap, _, _, err := GetInstanceSet()
189         if err != nil {
190                 c.Fatal("Error making provider", err)
191         }
192
193         l, err := ap.Instances(nil)
194
195         c.Assert(err, check.IsNil)
196
197         for _, i := range l {
198                 tg := i.Tags()
199                 log.Printf("%v %v %v", i.String(), i.Address(), tg)
200         }
201 }
202
203 func (*AzureInstanceSetSuite) TestManageNics(c *check.C) {
204         ap, _, _, err := GetInstanceSet()
205         if err != nil {
206                 c.Fatal("Error making provider", err)
207         }
208
209         ap.(*azureInstanceSet).manageNics()
210         ap.Stop()
211 }
212
213 func (*AzureInstanceSetSuite) TestManageBlobs(c *check.C) {
214         ap, _, _, err := GetInstanceSet()
215         if err != nil {
216                 c.Fatal("Error making provider", err)
217         }
218
219         ap.(*azureInstanceSet).manageBlobs()
220         ap.Stop()
221 }
222
223 func (*AzureInstanceSetSuite) TestDestroyInstances(c *check.C) {
224         ap, _, _, err := GetInstanceSet()
225         if err != nil {
226                 c.Fatal("Error making provider", err)
227         }
228
229         l, err := ap.Instances(nil)
230         c.Assert(err, check.IsNil)
231
232         for _, i := range l {
233                 c.Check(i.Destroy(), check.IsNil)
234         }
235 }
236
237 func (*AzureInstanceSetSuite) TestDeleteFake(c *check.C) {
238         ap, _, _, err := GetInstanceSet()
239         if err != nil {
240                 c.Fatal("Error making provider", err)
241         }
242
243         _, err = ap.(*azureInstanceSet).netClient.delete(context.Background(), "fakefakefake", "fakefakefake")
244
245         de, ok := err.(autorest.DetailedError)
246         if ok {
247                 rq := de.Original.(*azure.RequestError)
248
249                 log.Printf("%v %q %q", rq.Response.StatusCode, rq.ServiceError.Code, rq.ServiceError.Message)
250         }
251 }
252
253 func (*AzureInstanceSetSuite) TestWrapError(c *check.C) {
254         retryError := autorest.DetailedError{
255                 Original: &azure.RequestError{
256                         DetailedError: autorest.DetailedError{
257                                 Response: &http.Response{
258                                         StatusCode: 429,
259                                         Header:     map[string][]string{"Retry-After": []string{"123"}},
260                                 },
261                         },
262                         ServiceError: &azure.ServiceError{},
263                 },
264         }
265         wrapped := wrapAzureError(retryError)
266         _, ok := wrapped.(cloud.RateLimitError)
267         c.Check(ok, check.Equals, true)
268
269         quotaError := autorest.DetailedError{
270                 Original: &azure.RequestError{
271                         DetailedError: autorest.DetailedError{
272                                 Response: &http.Response{
273                                         StatusCode: 503,
274                                 },
275                         },
276                         ServiceError: &azure.ServiceError{
277                                 Message: "No more quota",
278                         },
279                 },
280         }
281         wrapped = wrapAzureError(quotaError)
282         _, ok = wrapped.(cloud.QuotaError)
283         c.Check(ok, check.Equals, true)
284 }
285
286 func (*AzureInstanceSetSuite) TestSetTags(c *check.C) {
287         ap, _, _, err := GetInstanceSet()
288         if err != nil {
289                 c.Fatal("Error making provider", err)
290         }
291         l, err := ap.Instances(nil)
292         c.Assert(err, check.IsNil)
293
294         if len(l) > 0 {
295                 err = l[0].SetTags(map[string]string{"foo": "bar"})
296                 if err != nil {
297                         c.Fatal("Error setting tags", err)
298                 }
299         }
300         l, err = ap.Instances(nil)
301         c.Assert(err, check.IsNil)
302
303         if len(l) > 0 {
304                 tg := l[0].Tags()
305                 log.Printf("tags are %v", tg)
306         }
307 }
308
309 func (*AzureInstanceSetSuite) TestSSH(c *check.C) {
310         ap, _, _, err := GetInstanceSet()
311         if err != nil {
312                 c.Fatal("Error making provider", err)
313         }
314         l, err := ap.Instances(nil)
315         c.Assert(err, check.IsNil)
316
317         if len(l) > 0 {
318                 sshclient, err := SetupSSHClient(c, l[0])
319                 c.Assert(err, check.IsNil)
320                 defer sshclient.Conn.Close()
321
322                 sess, err := sshclient.NewSession()
323                 c.Assert(err, check.IsNil)
324                 defer sess.Close()
325                 _, err = sess.Output("find /var/run/test-file -maxdepth 0 -user root -perm 0600")
326                 c.Assert(err, check.IsNil)
327
328                 sess, err = sshclient.NewSession()
329                 c.Assert(err, check.IsNil)
330                 defer sess.Close()
331                 out, err := sess.Output("sudo cat /var/run/test-file")
332                 c.Assert(err, check.IsNil)
333                 c.Check(string(out), check.Equals, "test-file-data")
334         }
335 }
336
337 func SetupSSHClient(c *check.C, inst cloud.Instance) (*ssh.Client, error) {
338         addr := inst.Address() + ":2222"
339         if addr == "" {
340                 return nil, errors.New("instance has no address")
341         }
342
343         f, err := os.Open("azconfig_sshkey")
344         c.Assert(err, check.IsNil)
345
346         keybytes, err := ioutil.ReadAll(f)
347         c.Assert(err, check.IsNil)
348
349         priv, err := ssh.ParsePrivateKey(keybytes)
350         c.Assert(err, check.IsNil)
351
352         var receivedKey ssh.PublicKey
353         client, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
354                 User: "crunch",
355                 Auth: []ssh.AuthMethod{
356                         ssh.PublicKeys(priv),
357                 },
358                 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
359                         receivedKey = key
360                         return nil
361                 },
362                 Timeout: time.Minute,
363         })
364
365         if err != nil {
366                 return nil, err
367         } else if receivedKey == nil {
368                 return nil, errors.New("BUG: key was never provided to HostKeyCallback")
369         }
370
371         err = inst.VerifyHostKey(receivedKey, client)
372         c.Assert(err, check.IsNil)
373
374         return client, nil
375 }