21189: Improve wording in config comments and logs.
[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         vmParameters compute.VirtualMachine
74 }
75
76 func (stub *VirtualMachinesClientStub) createOrUpdate(ctx context.Context,
77         resourceGroupName string,
78         VMName string,
79         parameters compute.VirtualMachine) (result compute.VirtualMachine, err error) {
80         parameters.ID = &VMName
81         parameters.Name = &VMName
82         stub.vmParameters = parameters
83         return parameters, nil
84 }
85
86 func (*VirtualMachinesClientStub) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
87         return nil, nil
88 }
89
90 func (*VirtualMachinesClientStub) listComplete(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultIterator, err error) {
91         return compute.VirtualMachineListResultIterator{}, nil
92 }
93
94 type InterfacesClientStub struct{}
95
96 func (*InterfacesClientStub) createOrUpdate(ctx context.Context,
97         resourceGroupName string,
98         nicName string,
99         parameters network.Interface) (result network.Interface, err error) {
100         parameters.ID = to.StringPtr(nicName)
101         (*parameters.IPConfigurations)[0].PrivateIPAddress = to.StringPtr("192.168.5.5")
102         return parameters, nil
103 }
104
105 func (*InterfacesClientStub) delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
106         return nil, nil
107 }
108
109 func (*InterfacesClientStub) listComplete(ctx context.Context, resourceGroupName string) (result network.InterfaceListResultIterator, err error) {
110         return network.InterfaceListResultIterator{}, nil
111 }
112
113 type BlobContainerStub struct{}
114
115 func (*BlobContainerStub) GetBlobReference(name string) *storage.Blob {
116         return nil
117 }
118
119 func (*BlobContainerStub) ListBlobs(params storage.ListBlobsParameters) (storage.BlobListResponse, error) {
120         return storage.BlobListResponse{}, nil
121 }
122
123 type testConfig struct {
124         ImageIDForTestSuite string
125         DriverParameters    json.RawMessage
126 }
127
128 var live = flag.String("live-azure-cfg", "", "Test with real azure API, provide config file")
129
130 func GetInstanceSet() (*azureInstanceSet, cloud.ImageID, arvados.Cluster, error) {
131         cluster := arvados.Cluster{
132                 InstanceTypes: arvados.InstanceTypeMap(map[string]arvados.InstanceType{
133                         "tiny": {
134                                 Name:         "tiny",
135                                 ProviderType: "Standard_D1_v2",
136                                 VCPUs:        1,
137                                 RAM:          4000000000,
138                                 Scratch:      10000000000,
139                                 Price:        .02,
140                                 Preemptible:  false,
141                         },
142                         "tinyp": {
143                                 Name:         "tiny",
144                                 ProviderType: "Standard_D1_v2",
145                                 VCPUs:        1,
146                                 RAM:          4000000000,
147                                 Scratch:      10000000000,
148                                 Price:        .002,
149                                 Preemptible:  true,
150                         },
151                 })}
152         if *live != "" {
153                 var exampleCfg testConfig
154                 err := config.LoadFile(&exampleCfg, *live)
155                 if err != nil {
156                         return nil, cloud.ImageID(""), cluster, err
157                 }
158
159                 ap, err := newAzureInstanceSet(exampleCfg.DriverParameters, "test123", nil, logrus.StandardLogger(), nil)
160                 return ap.(*azureInstanceSet), cloud.ImageID(exampleCfg.ImageIDForTestSuite), cluster, err
161         }
162         ap := azureInstanceSet{
163                 azconfig: azureInstanceSetConfig{
164                         BlobContainer: "vhds",
165                 },
166                 dispatcherID: "test123",
167                 namePrefix:   testNamePrefix,
168                 logger:       logrus.StandardLogger(),
169                 deleteNIC:    make(chan string),
170                 deleteBlob:   make(chan storage.Blob),
171                 deleteDisk:   make(chan compute.Disk),
172         }
173         ap.ctx, ap.stopFunc = context.WithCancel(context.Background())
174         ap.vmClient = &VirtualMachinesClientStub{}
175         ap.netClient = &InterfacesClientStub{}
176         ap.blobcont = &BlobContainerStub{}
177         return &ap, cloud.ImageID("blob"), cluster, nil
178 }
179
180 func (*AzureInstanceSetSuite) TestCreate(c *check.C) {
181         ap, img, cluster, err := GetInstanceSet()
182         if err != nil {
183                 c.Fatal("Error making provider", err)
184         }
185
186         pk, _ := test.LoadTestKey(c, "../../dispatchcloud/test/sshkey_dispatch")
187         c.Assert(err, check.IsNil)
188
189         inst, err := ap.Create(cluster.InstanceTypes["tiny"],
190                 img, map[string]string{
191                         "TestTagName": "test tag value",
192                 }, "umask 0600; echo -n test-file-data >/var/run/test-file", pk)
193
194         c.Assert(err, check.IsNil)
195
196         tags := inst.Tags()
197         c.Check(tags["TestTagName"], check.Equals, "test tag value")
198         c.Logf("inst.String()=%v Address()=%v Tags()=%v", inst.String(), inst.Address(), tags)
199         if *live == "" {
200                 c.Check(ap.vmClient.(*VirtualMachinesClientStub).vmParameters.VirtualMachineProperties.OsProfile.LinuxConfiguration.SSH, check.NotNil)
201         }
202
203         instPreemptable, err := ap.Create(cluster.InstanceTypes["tinyp"],
204                 img, map[string]string{
205                         "TestTagName": "test tag value",
206                 }, "umask 0600; echo -n test-file-data >/var/run/test-file", nil)
207
208         c.Assert(err, check.IsNil)
209
210         tags = instPreemptable.Tags()
211         c.Check(tags["TestTagName"], check.Equals, "test tag value")
212         c.Logf("instPreemptable.String()=%v Address()=%v Tags()=%v", instPreemptable.String(), instPreemptable.Address(), tags)
213         if *live == "" {
214                 // Should not have set SSH option, because publickey
215                 // arg was nil
216                 c.Check(ap.vmClient.(*VirtualMachinesClientStub).vmParameters.VirtualMachineProperties.OsProfile.LinuxConfiguration.SSH, check.IsNil)
217         }
218 }
219
220 func (*AzureInstanceSetSuite) TestListInstances(c *check.C) {
221         ap, _, _, err := GetInstanceSet()
222         if err != nil {
223                 c.Fatal("Error making provider", err)
224         }
225
226         l, err := ap.Instances(nil)
227
228         c.Assert(err, check.IsNil)
229
230         for _, i := range l {
231                 tg := i.Tags()
232                 log.Printf("%v %v %v", i.String(), i.Address(), tg)
233         }
234 }
235
236 func (*AzureInstanceSetSuite) TestManageNics(c *check.C) {
237         ap, _, _, err := GetInstanceSet()
238         if err != nil {
239                 c.Fatal("Error making provider", err)
240         }
241
242         ap.manageNics()
243         ap.Stop()
244 }
245
246 func (*AzureInstanceSetSuite) TestManageBlobs(c *check.C) {
247         ap, _, _, err := GetInstanceSet()
248         if err != nil {
249                 c.Fatal("Error making provider", err)
250         }
251
252         ap.manageBlobs()
253         ap.Stop()
254 }
255
256 func (*AzureInstanceSetSuite) TestDestroyInstances(c *check.C) {
257         ap, _, _, err := GetInstanceSet()
258         if err != nil {
259                 c.Fatal("Error making provider", err)
260         }
261
262         l, err := ap.Instances(nil)
263         c.Assert(err, check.IsNil)
264
265         for _, i := range filterInstances(c, l) {
266                 c.Check(i.Destroy(), check.IsNil)
267         }
268 }
269
270 func (*AzureInstanceSetSuite) TestDeleteFake(c *check.C) {
271         ap, _, _, err := GetInstanceSet()
272         if err != nil {
273                 c.Fatal("Error making provider", err)
274         }
275
276         _, err = ap.netClient.delete(context.Background(), "fakefakefake", "fakefakefake")
277
278         de, ok := err.(autorest.DetailedError)
279         if ok {
280                 rq := de.Original.(*azure.RequestError)
281
282                 log.Printf("%v %q %q", rq.Response.StatusCode, rq.ServiceError.Code, rq.ServiceError.Message)
283         }
284 }
285
286 func (*AzureInstanceSetSuite) TestWrapError(c *check.C) {
287         retryError := autorest.DetailedError{
288                 Original: &azure.RequestError{
289                         DetailedError: autorest.DetailedError{
290                                 Response: &http.Response{
291                                         StatusCode: 429,
292                                         Header:     map[string][]string{"Retry-After": {"123"}},
293                                 },
294                         },
295                         ServiceError: &azure.ServiceError{},
296                 },
297         }
298         wrapped := wrapAzureError(retryError)
299         _, ok := wrapped.(cloud.RateLimitError)
300         c.Check(ok, check.Equals, true)
301
302         quotaError := autorest.DetailedError{
303                 Original: &azure.RequestError{
304                         DetailedError: autorest.DetailedError{
305                                 Response: &http.Response{
306                                         StatusCode: 503,
307                                 },
308                         },
309                         ServiceError: &azure.ServiceError{
310                                 Message: "No more quota",
311                         },
312                 },
313         }
314         wrapped = wrapAzureError(quotaError)
315         _, ok = wrapped.(cloud.QuotaError)
316         c.Check(ok, check.Equals, true)
317 }
318
319 func (*AzureInstanceSetSuite) TestSetTags(c *check.C) {
320         ap, _, _, err := GetInstanceSet()
321         if err != nil {
322                 c.Fatal("Error making provider", err)
323         }
324
325         l, err := ap.Instances(nil)
326         c.Assert(err, check.IsNil)
327         l = filterInstances(c, l)
328         if len(l) > 0 {
329                 err = l[0].SetTags(map[string]string{"foo": "bar"})
330                 if err != nil {
331                         c.Fatal("Error setting tags", err)
332                 }
333         }
334
335         l, err = ap.Instances(nil)
336         c.Assert(err, check.IsNil)
337         l = filterInstances(c, l)
338
339         if len(l) > 0 {
340                 tg := l[0].Tags()
341                 log.Printf("tags are %v", tg)
342         }
343 }
344
345 func (*AzureInstanceSetSuite) TestSSH(c *check.C) {
346         ap, _, _, err := GetInstanceSet()
347         if err != nil {
348                 c.Fatal("Error making provider", err)
349         }
350         l, err := ap.Instances(nil)
351         c.Assert(err, check.IsNil)
352         l = filterInstances(c, l)
353
354         if len(l) > 0 {
355                 sshclient, err := SetupSSHClient(c, l[0])
356                 c.Assert(err, check.IsNil)
357                 defer sshclient.Conn.Close()
358
359                 sess, err := sshclient.NewSession()
360                 c.Assert(err, check.IsNil)
361                 defer sess.Close()
362                 _, err = sess.Output("find /var/run/test-file -maxdepth 0 -user root -perm 0600")
363                 c.Assert(err, check.IsNil)
364
365                 sess, err = sshclient.NewSession()
366                 c.Assert(err, check.IsNil)
367                 defer sess.Close()
368                 out, err := sess.Output("sudo cat /var/run/test-file")
369                 c.Assert(err, check.IsNil)
370                 c.Check(string(out), check.Equals, "test-file-data")
371         }
372 }
373
374 func SetupSSHClient(c *check.C, inst cloud.Instance) (*ssh.Client, error) {
375         addr := inst.Address() + ":2222"
376         if addr == "" {
377                 return nil, errors.New("instance has no address")
378         }
379
380         f, err := os.Open("azconfig_sshkey")
381         c.Assert(err, check.IsNil)
382
383         keybytes, err := ioutil.ReadAll(f)
384         c.Assert(err, check.IsNil)
385
386         priv, err := ssh.ParsePrivateKey(keybytes)
387         c.Assert(err, check.IsNil)
388
389         var receivedKey ssh.PublicKey
390         client, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
391                 User: "crunch",
392                 Auth: []ssh.AuthMethod{
393                         ssh.PublicKeys(priv),
394                 },
395                 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
396                         receivedKey = key
397                         return nil
398                 },
399                 Timeout: time.Minute,
400         })
401
402         if err != nil {
403                 return nil, err
404         } else if receivedKey == nil {
405                 return nil, errors.New("BUG: key was never provided to HostKeyCallback")
406         }
407
408         err = inst.VerifyHostKey(receivedKey, client)
409         c.Assert(err, check.IsNil)
410
411         return client, nil
412 }
413
414 func filterInstances(c *check.C, instances []cloud.Instance) []cloud.Instance {
415         var r []cloud.Instance
416         for _, i := range instances {
417                 if !strings.HasPrefix(i.String(), testNamePrefix) {
418                         c.Logf("ignoring instance %s", i)
419                         continue
420                 }
421                 r = append(r, i)
422         }
423         return r
424 }