13964: SSH access WIP
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Fri, 31 Aug 2018 15:42:01 +0000 (11:42 -0400)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Wed, 9 Jan 2019 21:28:16 +0000 (16:28 -0500)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

lib/dispatchcloud/azure.go
lib/dispatchcloud/azure_test.go
lib/dispatchcloud/provider.go

index 21e8d28d8317cd76b0b3ebe6bbce9ad50aab64b5..da2e3eb8fbf923c23c3c98119f4943c4916939a7 100644 (file)
@@ -25,6 +25,7 @@ import (
        "github.com/Azure/go-autorest/autorest/azure/auth"
        "github.com/Azure/go-autorest/autorest/to"
        "github.com/jmcvetta/randutil"
+       "golang.org/x/crypto/ssh"
 )
 
 type AzureProviderConfig struct {
@@ -40,7 +41,6 @@ type AzureProviderConfig struct {
        StorageAccount               string  `json:"storage_account"`
        BlobContainer                string  `json:"blob_container"`
        Image                        string  `json:"image"`
-       AuthorizedKey                string  `json:"authorized_key"`
        DeleteDanglingResourcesAfter float64 `json:"delete_dangling_resources_after"`
 }
 
@@ -186,7 +186,6 @@ func WrapAzureError(err error) error {
 
 type AzureProvider struct {
        azconfig          AzureProviderConfig
-       arvconfig         arvados.Cluster
        vmClient          VirtualMachinesClientWrapper
        netClient         InterfacesClientWrapper
        storageAcctClient storageacct.AccountsClient
@@ -196,18 +195,17 @@ type AzureProvider struct {
        namePrefix        string
 }
 
-func NewAzureProvider(azcfg AzureProviderConfig, arvcfg arvados.Cluster, dispatcherID string) (prv Provider, err error) {
+func NewAzureProvider(azcfg AzureProviderConfig, dispatcherID string) (prv InstanceProvider, err error) {
        ap := AzureProvider{}
-       err = ap.setup(azcfg, arvcfg, dispatcherID)
+       err = ap.setup(azcfg, dispatcherID)
        if err != nil {
                return nil, err
        }
        return &ap, nil
 }
 
-func (az *AzureProvider) setup(azcfg AzureProviderConfig, arvcfg arvados.Cluster, dispatcherID string) (err error) {
+func (az *AzureProvider) setup(azcfg AzureProviderConfig, dispatcherID string) (err error) {
        az.azconfig = azcfg
-       az.arvconfig = arvcfg
        vmClient := compute.NewVirtualMachinesClient(az.azconfig.SubscriptionID)
        netClient := network.NewInterfacesClient(az.azconfig.SubscriptionID)
        storageAcctClient := storageacct.NewAccountsClient(az.azconfig.SubscriptionID)
@@ -245,7 +243,8 @@ func (az *AzureProvider) setup(azcfg AzureProviderConfig, arvcfg arvados.Cluster
 func (az *AzureProvider) Create(ctx context.Context,
        instanceType arvados.InstanceType,
        imageId ImageID,
-       newTags InstanceTags) (Instance, error) {
+       newTags InstanceTags,
+       publicKey ssh.PublicKey) (Instance, error) {
 
        name, err := randutil.String(15, "abcdefghijklmnopqrstuvwxyz0123456789")
        if err != nil {
@@ -300,8 +299,6 @@ func (az *AzureProvider) Create(ctx context.Context,
 
        log.Printf("URI instance vhd %v", instance_vhd)
 
-       tags["arvados-instance-type"] = &instanceType.Name
-
        vmParameters := compute.VirtualMachine{
                Location: &az.azconfig.Location,
                Tags:     tags,
@@ -334,14 +331,14 @@ func (az *AzureProvider) Create(ctx context.Context,
                        },
                        OsProfile: &compute.OSProfile{
                                ComputerName:  &name,
-                               AdminUsername: to.StringPtr("arvados"),
+                               AdminUsername: to.StringPtr("crunch"),
                                LinuxConfiguration: &compute.LinuxConfiguration{
                                        DisablePasswordAuthentication: to.BoolPtr(true),
                                        SSH: &compute.SSHConfiguration{
                                                PublicKeys: &[]compute.SSHPublicKey{
                                                        compute.SSHPublicKey{
-                                                               Path:    to.StringPtr("/home/arvados/.ssh/authorized_keys"),
-                                                               KeyData: to.StringPtr(az.azconfig.AuthorizedKey),
+                                                               Path:    to.StringPtr("/home/crunch/.ssh/authorized_keys"),
+                                                               KeyData: to.StringPtr(string(ssh.MarshalAuthorizedKey(publicKey))),
                                                        },
                                                },
                                        },
@@ -357,10 +354,9 @@ func (az *AzureProvider) Create(ctx context.Context,
        }
 
        return &AzureInstance{
-               instanceType: instanceType,
-               provider:     az,
-               nic:          nic,
-               vm:           vm,
+               provider: az,
+               nic:      nic,
+               vm:       vm,
        }, nil
 }
 
@@ -381,13 +377,11 @@ func (az *AzureProvider) Instances(ctx context.Context) ([]Instance, error) {
                if err != nil {
                        return nil, WrapAzureError(err)
                }
-               if strings.HasPrefix(*result.Value().Name, az.namePrefix) &&
-                       result.Value().Tags["arvados-instance-type"] != nil {
+               if strings.HasPrefix(*result.Value().Name, az.namePrefix) {
                        instances = append(instances, &AzureInstance{
-                               provider:     az,
-                               vm:           result.Value(),
-                               nic:          interfaces[*(*result.Value().NetworkProfile.NetworkInterfaces)[0].ID],
-                               instanceType: az.arvconfig.InstanceTypes[(*result.Value().Tags["arvados-instance-type"])]})
+                               provider: az,
+                               vm:       result.Value(),
+                               nic:      interfaces[*(*result.Value().NetworkProfile.NetworkInterfaces)[0].ID]})
                }
        }
        return instances, nil
@@ -522,19 +516,21 @@ func (az *AzureProvider) ManageBlobs(ctx context.Context) {
        }
 }
 
+func (az *AzureProvider) Stop() {
+}
+
 type AzureInstance struct {
-       instanceType arvados.InstanceType
-       provider     *AzureProvider
-       nic          network.Interface
-       vm           compute.VirtualMachine
+       provider *AzureProvider
+       nic      network.Interface
+       vm       compute.VirtualMachine
 }
 
-func (ai *AzureInstance) String() string {
-       return *ai.vm.Name
+func (ai *AzureInstance) ID() InstanceID {
+       return InstanceID(*ai.vm.ID)
 }
 
-func (ai *AzureInstance) InstanceType() arvados.InstanceType {
-       return ai.instanceType
+func (ai *AzureInstance) String() string {
+       return *ai.vm.Name
 }
 
 func (ai *AzureInstance) SetTags(ctx context.Context, newTags InstanceTags) error {
@@ -562,7 +558,7 @@ func (ai *AzureInstance) SetTags(ctx context.Context, newTags InstanceTags) erro
        return nil
 }
 
-func (ai *AzureInstance) GetTags(ctx context.Context) (InstanceTags, error) {
+func (ai *AzureInstance) Tags(ctx context.Context) (InstanceTags, error) {
        tags := make(map[string]string)
 
        for k, v := range ai.vm.Tags {
index a5a173bee5004c0f6f118825936b90eacbdf713b..c23360b32c24de37791bf4f273488dd2563b5591 100644 (file)
@@ -6,9 +6,14 @@ package dispatchcloud
 
 import (
        "context"
+       "errors"
        "flag"
+       "io/ioutil"
        "log"
+       "net"
        "net/http"
+       "os"
+       "time"
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/config"
@@ -17,6 +22,7 @@ import (
        "github.com/Azure/go-autorest/autorest"
        "github.com/Azure/go-autorest/autorest/azure"
        "github.com/Azure/go-autorest/autorest/to"
+       "golang.org/x/crypto/ssh"
        check "gopkg.in/check.v1"
 )
 
@@ -64,7 +70,7 @@ func (*InterfacesClientStub) ListComplete(ctx context.Context, resourceGroupName
 
 var live = flag.String("live-azure-cfg", "", "Test with real azure API, provide config file")
 
-func GetProvider() (Provider, ImageID, arvados.Cluster, error) {
+func GetProvider() (InstanceProvider, ImageID, arvados.Cluster, error) {
        cluster := arvados.Cluster{
                InstanceTypes: arvados.InstanceTypeMap(map[string]arvados.InstanceType{
                        "tiny": arvados.InstanceType{
@@ -83,14 +89,13 @@ func GetProvider() (Provider, ImageID, arvados.Cluster, error) {
                if err != nil {
                        return nil, ImageID(""), cluster, err
                }
-               ap, err := NewAzureProvider(cfg, cluster, "test123")
+               ap, err := NewAzureProvider(cfg, "test123")
                return ap, ImageID(cfg.Image), cluster, err
        } else {
                ap := AzureProvider{
                        azconfig: AzureProviderConfig{
                                BlobContainer: "vhds",
                        },
-                       arvconfig:    cluster,
                        dispatcherID: "test123",
                        namePrefix:   "compute-test123-",
                }
@@ -106,13 +111,24 @@ func (*AzureProviderSuite) TestCreate(c *check.C) {
                c.Fatal("Error making provider", err)
        }
 
+       f, err := os.Open("azconfig_sshkey.pub")
+       c.Assert(err, check.IsNil)
+
+       keybytes, err := ioutil.ReadAll(f)
+       c.Assert(err, check.IsNil)
+
+       pk, _, _, _, err := ssh.ParseAuthorizedKey(keybytes)
+       c.Assert(err, check.IsNil)
+
        inst, err := ap.Create(context.Background(),
                cluster.InstanceTypes["tiny"],
-               img, map[string]string{"tag1": "bleep"})
+               img, map[string]string{"tag1": "bleep"},
+               pk)
 
        c.Assert(err, check.IsNil)
 
        log.Printf("Result %v %v", inst.String(), inst.Address())
+
 }
 
 func (*AzureProviderSuite) TestListInstances(c *check.C) {
@@ -126,8 +142,8 @@ func (*AzureProviderSuite) TestListInstances(c *check.C) {
        c.Assert(err, check.IsNil)
 
        for _, i := range l {
-               tg, _ := i.GetTags(context.Background())
-               log.Printf("%v %v %v %v", i.String(), i.Address(), i.InstanceType(), tg)
+               tg, _ := i.Tags(context.Background())
+               log.Printf("%v %v %v", i.String(), i.Address(), tg)
        }
 }
 
@@ -230,7 +246,75 @@ func (*AzureProviderSuite) TestSetTags(c *check.C) {
        c.Assert(err, check.IsNil)
 
        if len(l) > 0 {
-               tg, _ := l[0].GetTags(context.Background())
+               tg, _ := l[0].Tags(context.Background())
                log.Printf("tags are %v", tg)
        }
 }
+
+func (*AzureProviderSuite) TestSSH(c *check.C) {
+       ap, _, _, err := GetProvider()
+       if err != nil {
+               c.Fatal("Error making provider", err)
+       }
+       l, err := ap.Instances(context.Background())
+       c.Assert(err, check.IsNil)
+
+       if len(l) > 0 {
+
+               sshclient, err := SetupSSHClient(c, l[0].Address()+":2222")
+               c.Assert(err, check.IsNil)
+
+               sess, err := sshclient.NewSession()
+               c.Assert(err, check.IsNil)
+
+               out, err := sess.Output("ls /")
+               c.Assert(err, check.IsNil)
+
+               log.Printf("%v", out)
+
+               sshclient.Conn.Close()
+       }
+}
+
+func SetupSSHClient(c *check.C, addr string) (*ssh.Client, error) {
+       if addr == "" {
+               return nil, errors.New("instance has no address")
+       }
+
+       f, err := os.Open("azconfig_sshkey")
+       c.Assert(err, check.IsNil)
+
+       keybytes, err := ioutil.ReadAll(f)
+       c.Assert(err, check.IsNil)
+
+       priv, err := ssh.ParsePrivateKey(keybytes)
+       c.Assert(err, check.IsNil)
+
+       var receivedKey ssh.PublicKey
+       client, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
+               User: "crunch",
+               Auth: []ssh.AuthMethod{
+                       ssh.PublicKeys(priv),
+               },
+               HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
+                       receivedKey = key
+                       return nil
+               },
+               Timeout: time.Minute,
+       })
+
+       if err != nil {
+               return nil, err
+       } else if receivedKey == nil {
+               return nil, errors.New("BUG: key was never provided to HostKeyCallback")
+       }
+
+       /*if wkr.publicKey == nil || !bytes.Equal(wkr.publicKey.Marshal(), receivedKey.Marshal()) {
+               err = wkr.instance.VerifyPublicKey(receivedKey, client)
+               if err != nil {
+                       return nil, err
+               }
+               wkr.publicKey = receivedKey
+       }*/
+       return client, nil
+}
index 3322575dcdb04339d2426798bfdfdf94b7491617..ed5eb8fe2e318ff9d12656df71c1d5d90e1e771d 100644 (file)
@@ -9,6 +9,7 @@ import (
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "golang.org/x/crypto/ssh"
 )
 
 // A RateLimitError should be returned by a Provider when the cloud
@@ -38,21 +39,44 @@ type ImageID string
 
 // instance is implemented by the provider-specific instance types.
 type Instance interface {
+       // ID returns the provider's instance ID. It must be stable
+       // for the life of the instance.
+       ID() InstanceID
+
        // String typically returns the cloud-provided instance ID.
        String() string
-       // Configured Arvados instance type
-       InstanceType() arvados.InstanceType
+
        // Get tags
-       GetTags(context.Context) (InstanceTags, error)
+       Tags(context.Context) (InstanceTags, error)
+
        // Replace tags with the given tags
        SetTags(context.Context, InstanceTags) error
+
        // Shut down the node
        Destroy(context.Context) error
+
        // SSH server hostname or IP address, or empty string if unknown pending creation.
        Address() string
 }
 
-type Provider interface {
-       Create(context.Context, arvados.InstanceType, ImageID, InstanceTags) (Instance, error)
+type InstanceProvider interface {
+       // Create a new instance. If supported by the driver, add the
+       // provided public key to /root/.ssh/authorized_keys.
+       //
+       // The returned error should implement RateLimitError and
+       // QuotaError where applicable.
+       Create(context.Context, arvados.InstanceType, ImageID, InstanceTags, ssh.PublicKey) (Instance, error)
+
+       // Return all instances, including ones that are booting or
+       // shutting down.
+       //
+       // An instance returned by successive calls to Instances() may
+       // -- but does not need to -- be represented by the same
+       // Instance object each time. Thus, the caller is responsible
+       // for de-duplicating the returned instances by comparing the
+       // InstanceIDs returned by the instances' ID() methods.
        Instances(context.Context) ([]Instance, error)
+
+       // Stop any background tasks and release other resources.
+       Stop()
 }