Merge branch '14745-azure-cloud-driver-fixups'
[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
29 package azure
30
31 import (
32         "context"
33         "encoding/json"
34         "errors"
35         "flag"
36         "io/ioutil"
37         "log"
38         "net"
39         "net/http"
40         "os"
41         "testing"
42         "time"
43
44         "git.curoverse.com/arvados.git/lib/cloud"
45         "git.curoverse.com/arvados.git/sdk/go/arvados"
46         "git.curoverse.com/arvados.git/sdk/go/config"
47         "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-06-01/compute"
48         "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-06-01/network"
49         "github.com/Azure/azure-sdk-for-go/storage"
50         "github.com/Azure/go-autorest/autorest"
51         "github.com/Azure/go-autorest/autorest/azure"
52         "github.com/Azure/go-autorest/autorest/to"
53         "github.com/jmcvetta/randutil"
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 testConfig struct {
109         ImageIDForTestSuite string
110         DriverParameters    json.RawMessage
111 }
112
113 var live = flag.String("live-azure-cfg", "", "Test with real azure API, provide config file")
114
115 func GetInstanceSet() (cloud.InstanceSet, cloud.ImageID, arvados.Cluster, error) {
116         cluster := arvados.Cluster{
117                 InstanceTypes: arvados.InstanceTypeMap(map[string]arvados.InstanceType{
118                         "tiny": arvados.InstanceType{
119                                 Name:         "tiny",
120                                 ProviderType: "Standard_D1_v2",
121                                 VCPUs:        1,
122                                 RAM:          4000000000,
123                                 Scratch:      10000000000,
124                                 Price:        .02,
125                                 Preemptible:  false,
126                         },
127                 })}
128         if *live != "" {
129                 var exampleCfg testConfig
130                 err := config.LoadFile(&exampleCfg, *live)
131                 if err != nil {
132                         return nil, cloud.ImageID(""), cluster, err
133                 }
134
135                 ap, err := newAzureInstanceSet(exampleCfg.DriverParameters, "test123", logrus.StandardLogger())
136                 return ap, cloud.ImageID(exampleCfg.ImageIDForTestSuite), cluster, err
137         }
138         ap := azureInstanceSet{
139                 azconfig: azureInstanceSetConfig{
140                         BlobContainer: "vhds",
141                 },
142                 dispatcherID: "test123",
143                 namePrefix:   "compute-test123-",
144                 logger:       logrus.StandardLogger(),
145                 deleteNIC:    make(chan string),
146                 deleteBlob:   make(chan storage.Blob),
147         }
148         ap.ctx, ap.stopFunc = context.WithCancel(context.Background())
149         ap.vmClient = &VirtualMachinesClientStub{}
150         ap.netClient = &InterfacesClientStub{}
151         return &ap, cloud.ImageID("blob"), cluster, nil
152 }
153
154 func (*AzureInstanceSetSuite) TestCreate(c *check.C) {
155         ap, img, cluster, err := GetInstanceSet()
156         if err != nil {
157                 c.Fatal("Error making provider", err)
158         }
159
160         pk, _, _, _, err := ssh.ParseAuthorizedKey(testKey)
161         c.Assert(err, check.IsNil)
162
163         nodetoken, err := randutil.String(40, "abcdefghijklmnopqrstuvwxyz0123456789")
164         c.Assert(err, check.IsNil)
165
166         inst, err := ap.Create(cluster.InstanceTypes["tiny"],
167                 img, map[string]string{
168                         "node-token": nodetoken},
169                 pk)
170
171         c.Assert(err, check.IsNil)
172
173         tg := inst.Tags()
174         log.Printf("Result %v %v %v", inst.String(), inst.Address(), tg)
175
176 }
177
178 func (*AzureInstanceSetSuite) TestListInstances(c *check.C) {
179         ap, _, _, err := GetInstanceSet()
180         if err != nil {
181                 c.Fatal("Error making provider", err)
182         }
183
184         l, err := ap.Instances(nil)
185
186         c.Assert(err, check.IsNil)
187
188         for _, i := range l {
189                 tg := i.Tags()
190                 log.Printf("%v %v %v", i.String(), i.Address(), tg)
191         }
192 }
193
194 func (*AzureInstanceSetSuite) TestManageNics(c *check.C) {
195         ap, _, _, err := GetInstanceSet()
196         if err != nil {
197                 c.Fatal("Error making provider", err)
198         }
199
200         ap.(*azureInstanceSet).manageNics()
201         ap.Stop()
202 }
203
204 func (*AzureInstanceSetSuite) TestManageBlobs(c *check.C) {
205         ap, _, _, err := GetInstanceSet()
206         if err != nil {
207                 c.Fatal("Error making provider", err)
208         }
209
210         ap.(*azureInstanceSet).manageBlobs()
211         ap.Stop()
212 }
213
214 func (*AzureInstanceSetSuite) TestDestroyInstances(c *check.C) {
215         ap, _, _, err := GetInstanceSet()
216         if err != nil {
217                 c.Fatal("Error making provider", err)
218         }
219
220         l, err := ap.Instances(nil)
221         c.Assert(err, check.IsNil)
222
223         for _, i := range l {
224                 c.Check(i.Destroy(), check.IsNil)
225         }
226 }
227
228 func (*AzureInstanceSetSuite) TestDeleteFake(c *check.C) {
229         ap, _, _, err := GetInstanceSet()
230         if err != nil {
231                 c.Fatal("Error making provider", err)
232         }
233
234         _, err = ap.(*azureInstanceSet).netClient.delete(context.Background(), "fakefakefake", "fakefakefake")
235
236         de, ok := err.(autorest.DetailedError)
237         if ok {
238                 rq := de.Original.(*azure.RequestError)
239
240                 log.Printf("%v %q %q", rq.Response.StatusCode, rq.ServiceError.Code, rq.ServiceError.Message)
241         }
242 }
243
244 func (*AzureInstanceSetSuite) TestWrapError(c *check.C) {
245         retryError := autorest.DetailedError{
246                 Original: &azure.RequestError{
247                         DetailedError: autorest.DetailedError{
248                                 Response: &http.Response{
249                                         StatusCode: 429,
250                                         Header:     map[string][]string{"Retry-After": []string{"123"}},
251                                 },
252                         },
253                         ServiceError: &azure.ServiceError{},
254                 },
255         }
256         wrapped := wrapAzureError(retryError)
257         _, ok := wrapped.(cloud.RateLimitError)
258         c.Check(ok, check.Equals, true)
259
260         quotaError := autorest.DetailedError{
261                 Original: &azure.RequestError{
262                         DetailedError: autorest.DetailedError{
263                                 Response: &http.Response{
264                                         StatusCode: 503,
265                                 },
266                         },
267                         ServiceError: &azure.ServiceError{
268                                 Message: "No more quota",
269                         },
270                 },
271         }
272         wrapped = wrapAzureError(quotaError)
273         _, ok = wrapped.(cloud.QuotaError)
274         c.Check(ok, check.Equals, true)
275 }
276
277 func (*AzureInstanceSetSuite) TestSetTags(c *check.C) {
278         ap, _, _, err := GetInstanceSet()
279         if err != nil {
280                 c.Fatal("Error making provider", err)
281         }
282         l, err := ap.Instances(nil)
283         c.Assert(err, check.IsNil)
284
285         if len(l) > 0 {
286                 err = l[0].SetTags(map[string]string{"foo": "bar"})
287                 if err != nil {
288                         c.Fatal("Error setting tags", err)
289                 }
290         }
291         l, err = ap.Instances(nil)
292         c.Assert(err, check.IsNil)
293
294         if len(l) > 0 {
295                 tg := l[0].Tags()
296                 log.Printf("tags are %v", tg)
297         }
298 }
299
300 func (*AzureInstanceSetSuite) TestSSH(c *check.C) {
301         ap, _, _, err := GetInstanceSet()
302         if err != nil {
303                 c.Fatal("Error making provider", err)
304         }
305         l, err := ap.Instances(nil)
306         c.Assert(err, check.IsNil)
307
308         if len(l) > 0 {
309
310                 sshclient, err := SetupSSHClient(c, l[0])
311                 c.Assert(err, check.IsNil)
312
313                 sess, err := sshclient.NewSession()
314                 c.Assert(err, check.IsNil)
315
316                 out, err := sess.Output("cat /home/crunch/node-token")
317                 c.Assert(err, check.IsNil)
318
319                 log.Printf("%v", string(out))
320
321                 sshclient.Conn.Close()
322         }
323 }
324
325 func SetupSSHClient(c *check.C, inst cloud.Instance) (*ssh.Client, error) {
326         addr := inst.Address() + ":2222"
327         if addr == "" {
328                 return nil, errors.New("instance has no address")
329         }
330
331         f, err := os.Open("azconfig_sshkey")
332         c.Assert(err, check.IsNil)
333
334         keybytes, err := ioutil.ReadAll(f)
335         c.Assert(err, check.IsNil)
336
337         priv, err := ssh.ParsePrivateKey(keybytes)
338         c.Assert(err, check.IsNil)
339
340         var receivedKey ssh.PublicKey
341         client, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
342                 User: "crunch",
343                 Auth: []ssh.AuthMethod{
344                         ssh.PublicKeys(priv),
345                 },
346                 HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
347                         receivedKey = key
348                         return nil
349                 },
350                 Timeout: time.Minute,
351         })
352
353         if err != nil {
354                 return nil, err
355         } else if receivedKey == nil {
356                 return nil, errors.New("BUG: key was never provided to HostKeyCallback")
357         }
358
359         err = inst.VerifyHostKey(receivedKey, client)
360         c.Assert(err, check.IsNil)
361
362         return client, nil
363 }