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