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