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