lib/cmd
lib/controller
lib/crunchstat
+lib/cloud
lib/dispatchcloud
lib/dispatchcloud/container
lib/dispatchcloud/scheduler
lib/cmd
lib/controller
lib/crunchstat
+ lib/cloud
lib/dispatchcloud
lib/dispatchcloud/container
lib/dispatchcloud/scheduler
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package cloud
+
+import (
+ "context"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "git.curoverse.com/arvados.git/sdk/go/arvados"
+ "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-06-01/compute"
+ "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-06-01/network"
+ storageacct "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2018-02-01/storage"
+ "github.com/Azure/azure-sdk-for-go/storage"
+ "github.com/Azure/go-autorest/autorest"
+ "github.com/Azure/go-autorest/autorest/azure"
+ "github.com/Azure/go-autorest/autorest/azure/auth"
+ "github.com/Azure/go-autorest/autorest/to"
+ "github.com/jmcvetta/randutil"
+ "github.com/mitchellh/mapstructure"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/crypto/ssh"
+)
+
+type AzureInstanceSetConfig struct {
+ SubscriptionID string `mapstructure:"subscription_id"`
+ ClientID string `mapstructure:"key"`
+ ClientSecret string `mapstructure:"secret"`
+ TenantID string `mapstructure:"tenant_id"`
+ CloudEnv string `mapstructure:"cloud_environment"`
+ ResourceGroup string `mapstructure:"resource_group"`
+ Location string `mapstructure:"region"`
+ Network string `mapstructure:"network"`
+ Subnet string `mapstructure:"subnet"`
+ StorageAccount string `mapstructure:"storage_account"`
+ BlobContainer string `mapstructure:"blob_container"`
+ Image string `mapstructure:"image"`
+ DeleteDanglingResourcesAfter float64 `mapstructure:"delete_dangling_resources_after"`
+}
+
+type VirtualMachinesClientWrapper interface {
+ CreateOrUpdate(ctx context.Context,
+ resourceGroupName string,
+ VMName string,
+ parameters compute.VirtualMachine) (result compute.VirtualMachine, err error)
+ Delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error)
+ ListComplete(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultIterator, err error)
+}
+
+type VirtualMachinesClientImpl struct {
+ inner compute.VirtualMachinesClient
+}
+
+func (cl *VirtualMachinesClientImpl) CreateOrUpdate(ctx context.Context,
+ resourceGroupName string,
+ VMName string,
+ parameters compute.VirtualMachine) (result compute.VirtualMachine, err error) {
+
+ future, err := cl.inner.CreateOrUpdate(ctx, resourceGroupName, VMName, parameters)
+ if err != nil {
+ return compute.VirtualMachine{}, WrapAzureError(err)
+ }
+ future.WaitForCompletionRef(ctx, cl.inner.Client)
+ r, err := future.Result(cl.inner)
+ return r, WrapAzureError(err)
+}
+
+func (cl *VirtualMachinesClientImpl) Delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
+ future, err := cl.inner.Delete(ctx, resourceGroupName, VMName)
+ if err != nil {
+ return nil, WrapAzureError(err)
+ }
+ err = future.WaitForCompletionRef(ctx, cl.inner.Client)
+ return future.Response(), WrapAzureError(err)
+}
+
+func (cl *VirtualMachinesClientImpl) ListComplete(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultIterator, err error) {
+ r, err := cl.inner.ListComplete(ctx, resourceGroupName)
+ return r, WrapAzureError(err)
+}
+
+type InterfacesClientWrapper interface {
+ CreateOrUpdate(ctx context.Context,
+ resourceGroupName string,
+ networkInterfaceName string,
+ parameters network.Interface) (result network.Interface, err error)
+ Delete(ctx context.Context, resourceGroupName string, networkInterfaceName string) (result *http.Response, err error)
+ ListComplete(ctx context.Context, resourceGroupName string) (result network.InterfaceListResultIterator, err error)
+}
+
+type InterfacesClientImpl struct {
+ inner network.InterfacesClient
+}
+
+func (cl *InterfacesClientImpl) Delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
+ future, err := cl.inner.Delete(ctx, resourceGroupName, VMName)
+ if err != nil {
+ return nil, WrapAzureError(err)
+ }
+ err = future.WaitForCompletionRef(ctx, cl.inner.Client)
+ return future.Response(), WrapAzureError(err)
+}
+
+func (cl *InterfacesClientImpl) CreateOrUpdate(ctx context.Context,
+ resourceGroupName string,
+ networkInterfaceName string,
+ parameters network.Interface) (result network.Interface, err error) {
+
+ future, err := cl.inner.CreateOrUpdate(ctx, resourceGroupName, networkInterfaceName, parameters)
+ if err != nil {
+ return network.Interface{}, WrapAzureError(err)
+ }
+ future.WaitForCompletionRef(ctx, cl.inner.Client)
+ r, err := future.Result(cl.inner)
+ return r, WrapAzureError(err)
+}
+
+func (cl *InterfacesClientImpl) ListComplete(ctx context.Context, resourceGroupName string) (result network.InterfaceListResultIterator, err error) {
+ r, err := cl.inner.ListComplete(ctx, resourceGroupName)
+ return r, WrapAzureError(err)
+}
+
+var quotaRe = regexp.MustCompile(`(?i:exceed|quota|limit)`)
+
+type AzureRateLimitError struct {
+ azure.RequestError
+ earliestRetry time.Time
+}
+
+func (ar *AzureRateLimitError) EarliestRetry() time.Time {
+ return ar.earliestRetry
+}
+
+type AzureQuotaError struct {
+ azure.RequestError
+}
+
+func (ar *AzureQuotaError) IsQuotaError() bool {
+ return true
+}
+
+func WrapAzureError(err error) error {
+ de, ok := err.(autorest.DetailedError)
+ if !ok {
+ return err
+ }
+ rq, ok := de.Original.(*azure.RequestError)
+ if !ok {
+ return err
+ }
+ if rq.Response == nil {
+ return err
+ }
+ if rq.Response.StatusCode == 429 || len(rq.Response.Header["Retry-After"]) >= 1 {
+ // API throttling
+ ra := rq.Response.Header["Retry-After"][0]
+ earliestRetry, parseErr := http.ParseTime(ra)
+ if parseErr != nil {
+ // Could not parse as a timestamp, must be number of seconds
+ dur, parseErr := strconv.ParseInt(ra, 10, 64)
+ if parseErr == nil {
+ earliestRetry = time.Now().Add(time.Duration(dur) * time.Second)
+ } else {
+ // Couldn't make sense of retry-after,
+ // so set retry to 20 seconds
+ earliestRetry = time.Now().Add(20 * time.Second)
+ }
+ }
+ return &AzureRateLimitError{*rq, earliestRetry}
+ }
+ if rq.ServiceError == nil {
+ return err
+ }
+ if quotaRe.FindString(rq.ServiceError.Code) != "" || quotaRe.FindString(rq.ServiceError.Message) != "" {
+ return &AzureQuotaError{*rq}
+ }
+ return err
+}
+
+type AzureInstanceSet struct {
+ azconfig AzureInstanceSetConfig
+ vmClient VirtualMachinesClientWrapper
+ netClient InterfacesClientWrapper
+ storageAcctClient storageacct.AccountsClient
+ azureEnv azure.Environment
+ interfaces map[string]network.Interface
+ dispatcherID string
+ namePrefix string
+ ctx context.Context
+ stopFunc context.CancelFunc
+ stopWg sync.WaitGroup
+ deleteNIC chan string
+ deleteBlob chan storage.Blob
+ logger logrus.FieldLogger
+}
+
+func NewAzureInstanceSet(config map[string]interface{}, dispatcherID InstanceSetID, logger logrus.FieldLogger) (prv InstanceSet, err error) {
+ azcfg := AzureInstanceSetConfig{}
+ if err = mapstructure.Decode(config, &azcfg); err != nil {
+ return nil, err
+ }
+ ap := AzureInstanceSet{logger: logger}
+ err = ap.setup(azcfg, string(dispatcherID))
+ if err != nil {
+ return nil, err
+ }
+ return &ap, nil
+}
+
+func (az *AzureInstanceSet) setup(azcfg AzureInstanceSetConfig, dispatcherID string) (err error) {
+ az.azconfig = azcfg
+ vmClient := compute.NewVirtualMachinesClient(az.azconfig.SubscriptionID)
+ netClient := network.NewInterfacesClient(az.azconfig.SubscriptionID)
+ storageAcctClient := storageacct.NewAccountsClient(az.azconfig.SubscriptionID)
+
+ az.azureEnv, err = azure.EnvironmentFromName(az.azconfig.CloudEnv)
+ if err != nil {
+ return err
+ }
+
+ authorizer, err := auth.ClientCredentialsConfig{
+ ClientID: az.azconfig.ClientID,
+ ClientSecret: az.azconfig.ClientSecret,
+ TenantID: az.azconfig.TenantID,
+ Resource: az.azureEnv.ResourceManagerEndpoint,
+ AADEndpoint: az.azureEnv.ActiveDirectoryEndpoint,
+ }.Authorizer()
+ if err != nil {
+ return err
+ }
+
+ vmClient.Authorizer = authorizer
+ netClient.Authorizer = authorizer
+ storageAcctClient.Authorizer = authorizer
+
+ az.vmClient = &VirtualMachinesClientImpl{vmClient}
+ az.netClient = &InterfacesClientImpl{netClient}
+ az.storageAcctClient = storageAcctClient
+
+ az.dispatcherID = dispatcherID
+ az.namePrefix = fmt.Sprintf("compute-%s-", az.dispatcherID)
+
+ az.ctx, az.stopFunc = context.WithCancel(context.Background())
+ go func() {
+ az.stopWg.Add(1)
+ defer az.stopWg.Done()
+
+ tk := time.NewTicker(5 * time.Minute)
+ for {
+ select {
+ case <-az.ctx.Done():
+ tk.Stop()
+ return
+ case <-tk.C:
+ az.ManageBlobs()
+ }
+ }
+ }()
+
+ az.deleteNIC = make(chan string)
+ az.deleteBlob = make(chan storage.Blob)
+
+ for i := 0; i < 4; i += 1 {
+ go func() {
+ for {
+ nicname, ok := <-az.deleteNIC
+ if !ok {
+ return
+ }
+ _, delerr := az.netClient.Delete(context.Background(), az.azconfig.ResourceGroup, nicname)
+ if delerr != nil {
+ az.logger.WithError(delerr).Warnf("Error deleting %v", nicname)
+ } else {
+ az.logger.Printf("Deleted NIC %v", nicname)
+ }
+ }
+ }()
+ go func() {
+ for {
+ blob, ok := <-az.deleteBlob
+ if !ok {
+ return
+ }
+ err := blob.Delete(nil)
+ if err != nil {
+ az.logger.WithError(err).Warnf("Error deleting %v", blob.Name)
+ } else {
+ az.logger.Printf("Deleted blob %v", blob.Name)
+ }
+ }
+ }()
+ }
+
+ return nil
+}
+
+func (az *AzureInstanceSet) Create(
+ instanceType arvados.InstanceType,
+ imageId ImageID,
+ newTags InstanceTags,
+ publicKey ssh.PublicKey) (Instance, error) {
+
+ az.stopWg.Add(1)
+ defer az.stopWg.Done()
+
+ if len(newTags["node-token"]) == 0 {
+ return nil, fmt.Errorf("Must provide tag 'node-token'")
+ }
+
+ name, err := randutil.String(15, "abcdefghijklmnopqrstuvwxyz0123456789")
+ if err != nil {
+ return nil, err
+ }
+
+ name = az.namePrefix + name
+
+ timestamp := time.Now().Format(time.RFC3339Nano)
+
+ tags := make(map[string]*string)
+ tags["created-at"] = ×tamp
+ for k, v := range newTags {
+ newstr := v
+ tags["dispatch-"+k] = &newstr
+ }
+
+ tags["dispatch-instance-type"] = &instanceType.Name
+
+ nicParameters := network.Interface{
+ Location: &az.azconfig.Location,
+ Tags: tags,
+ InterfacePropertiesFormat: &network.InterfacePropertiesFormat{
+ IPConfigurations: &[]network.InterfaceIPConfiguration{
+ network.InterfaceIPConfiguration{
+ Name: to.StringPtr("ip1"),
+ InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{
+ Subnet: &network.Subnet{
+ ID: to.StringPtr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers"+
+ "/Microsoft.Network/virtualnetworks/%s/subnets/%s",
+ az.azconfig.SubscriptionID,
+ az.azconfig.ResourceGroup,
+ az.azconfig.Network,
+ az.azconfig.Subnet)),
+ },
+ PrivateIPAllocationMethod: network.Dynamic,
+ },
+ },
+ },
+ },
+ }
+ nic, err := az.netClient.CreateOrUpdate(az.ctx, az.azconfig.ResourceGroup, name+"-nic", nicParameters)
+ if err != nil {
+ return nil, WrapAzureError(err)
+ }
+
+ instance_vhd := fmt.Sprintf("https://%s.blob.%s/%s/%s-os.vhd",
+ az.azconfig.StorageAccount,
+ az.azureEnv.StorageEndpointSuffix,
+ az.azconfig.BlobContainer,
+ name)
+
+ customData := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`#!/bin/sh
+echo '%s-%s' > /home/crunch/node-token`, name, newTags["node-token"])))
+
+ vmParameters := compute.VirtualMachine{
+ Location: &az.azconfig.Location,
+ Tags: tags,
+ VirtualMachineProperties: &compute.VirtualMachineProperties{
+ HardwareProfile: &compute.HardwareProfile{
+ VMSize: compute.VirtualMachineSizeTypes(instanceType.ProviderType),
+ },
+ StorageProfile: &compute.StorageProfile{
+ OsDisk: &compute.OSDisk{
+ OsType: compute.Linux,
+ Name: to.StringPtr(name + "-os"),
+ CreateOption: compute.FromImage,
+ Image: &compute.VirtualHardDisk{
+ URI: to.StringPtr(string(imageId)),
+ },
+ Vhd: &compute.VirtualHardDisk{
+ URI: &instance_vhd,
+ },
+ },
+ },
+ NetworkProfile: &compute.NetworkProfile{
+ NetworkInterfaces: &[]compute.NetworkInterfaceReference{
+ compute.NetworkInterfaceReference{
+ ID: nic.ID,
+ NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{
+ Primary: to.BoolPtr(true),
+ },
+ },
+ },
+ },
+ OsProfile: &compute.OSProfile{
+ ComputerName: &name,
+ AdminUsername: to.StringPtr("crunch"),
+ LinuxConfiguration: &compute.LinuxConfiguration{
+ DisablePasswordAuthentication: to.BoolPtr(true),
+ SSH: &compute.SSHConfiguration{
+ PublicKeys: &[]compute.SSHPublicKey{
+ compute.SSHPublicKey{
+ Path: to.StringPtr("/home/crunch/.ssh/authorized_keys"),
+ KeyData: to.StringPtr(string(ssh.MarshalAuthorizedKey(publicKey))),
+ },
+ },
+ },
+ },
+ CustomData: &customData,
+ },
+ },
+ }
+
+ vm, err := az.vmClient.CreateOrUpdate(az.ctx, az.azconfig.ResourceGroup, name, vmParameters)
+ if err != nil {
+ return nil, WrapAzureError(err)
+ }
+
+ return &AzureInstance{
+ provider: az,
+ nic: nic,
+ vm: vm,
+ }, nil
+}
+
+func (az *AzureInstanceSet) Instances(InstanceTags) ([]Instance, error) {
+ az.stopWg.Add(1)
+ defer az.stopWg.Done()
+
+ interfaces, err := az.ManageNics()
+ if err != nil {
+ return nil, err
+ }
+
+ result, err := az.vmClient.ListComplete(az.ctx, az.azconfig.ResourceGroup)
+ if err != nil {
+ return nil, WrapAzureError(err)
+ }
+
+ instances := make([]Instance, 0)
+
+ for ; result.NotDone(); err = result.Next() {
+ if err != nil {
+ return nil, WrapAzureError(err)
+ }
+ 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]})
+ }
+ }
+ return instances, nil
+}
+
+// ManageNics returns a list of Azure network interface resources.
+// Also performs garbage collection of NICs which have "namePrefix", are
+// not associated with a virtual machine and have a "create-at" time
+// more than DeleteDanglingResourcesAfter (to prevent racing and
+// deleting newly created NICs) in the past are deleted.
+func (az *AzureInstanceSet) ManageNics() (map[string]network.Interface, error) {
+ az.stopWg.Add(1)
+ defer az.stopWg.Done()
+
+ result, err := az.netClient.ListComplete(az.ctx, az.azconfig.ResourceGroup)
+ if err != nil {
+ return nil, WrapAzureError(err)
+ }
+
+ interfaces := make(map[string]network.Interface)
+
+ timestamp := time.Now()
+ for ; result.NotDone(); err = result.Next() {
+ if err != nil {
+ az.logger.WithError(err).Warnf("Error listing nics")
+ return interfaces, nil
+ }
+ if strings.HasPrefix(*result.Value().Name, az.namePrefix) {
+ if result.Value().VirtualMachine != nil {
+ interfaces[*result.Value().ID] = result.Value()
+ } else {
+ if result.Value().Tags["created-at"] != nil {
+ created_at, err := time.Parse(time.RFC3339Nano, *result.Value().Tags["created-at"])
+ if err == nil {
+ if timestamp.Sub(created_at).Seconds() > az.azconfig.DeleteDanglingResourcesAfter {
+ az.logger.Printf("Will delete %v because it is older than %v s", *result.Value().Name, az.azconfig.DeleteDanglingResourcesAfter)
+ az.deleteNIC <- *result.Value().Name
+ }
+ }
+ }
+ }
+ }
+ }
+ return interfaces, nil
+}
+
+// ManageBlobs garbage collects blobs (VM disk images) in the
+// configured storage account container. It will delete blobs which
+// have "namePrefix", are "available" (which means they are not
+// leased to a VM) and haven't been modified for
+// DeleteDanglingResourcesAfter seconds.
+func (az *AzureInstanceSet) ManageBlobs() {
+ result, err := az.storageAcctClient.ListKeys(az.ctx, az.azconfig.ResourceGroup, az.azconfig.StorageAccount)
+ if err != nil {
+ az.logger.WithError(err).Warn("Couldn't get account keys")
+ return
+ }
+
+ key1 := *(*result.Keys)[0].Value
+ client, err := storage.NewBasicClientOnSovereignCloud(az.azconfig.StorageAccount, key1, az.azureEnv)
+ if err != nil {
+ az.logger.WithError(err).Warn("Couldn't make client")
+ return
+ }
+
+ blobsvc := client.GetBlobService()
+ blobcont := blobsvc.GetContainerReference(az.azconfig.BlobContainer)
+
+ page := storage.ListBlobsParameters{Prefix: az.namePrefix}
+ timestamp := time.Now()
+
+ for {
+ response, err := blobcont.ListBlobs(page)
+ if err != nil {
+ az.logger.WithError(err).Warn("Error listing blobs")
+ return
+ }
+ for _, b := range response.Blobs {
+ age := timestamp.Sub(time.Time(b.Properties.LastModified))
+ if b.Properties.BlobType == storage.BlobTypePage &&
+ b.Properties.LeaseState == "available" &&
+ b.Properties.LeaseStatus == "unlocked" &&
+ age.Seconds() > az.azconfig.DeleteDanglingResourcesAfter {
+
+ az.logger.Printf("Blob %v is unlocked and not modified for %v seconds, will delete", b.Name, age.Seconds())
+ az.deleteBlob <- b
+ }
+ }
+ if response.NextMarker != "" {
+ page.Marker = response.NextMarker
+ } else {
+ break
+ }
+ }
+}
+
+func (az *AzureInstanceSet) Stop() {
+ az.stopFunc()
+ az.stopWg.Wait()
+ close(az.deleteNIC)
+ close(az.deleteBlob)
+}
+
+type AzureInstance struct {
+ provider *AzureInstanceSet
+ nic network.Interface
+ vm compute.VirtualMachine
+}
+
+func (ai *AzureInstance) ID() InstanceID {
+ return InstanceID(*ai.vm.ID)
+}
+
+func (ai *AzureInstance) String() string {
+ return *ai.vm.Name
+}
+
+func (ai *AzureInstance) ProviderType() string {
+ return string(ai.vm.VirtualMachineProperties.HardwareProfile.VMSize)
+}
+
+func (ai *AzureInstance) SetTags(newTags InstanceTags) error {
+ ai.provider.stopWg.Add(1)
+ defer ai.provider.stopWg.Done()
+
+ tags := make(map[string]*string)
+
+ for k, v := range ai.vm.Tags {
+ if !strings.HasPrefix(k, "dispatch-") {
+ tags[k] = v
+ }
+ }
+ for k, v := range newTags {
+ newstr := v
+ tags["dispatch-"+k] = &newstr
+ }
+
+ vmParameters := compute.VirtualMachine{
+ Location: &ai.provider.azconfig.Location,
+ Tags: tags,
+ }
+ vm, err := ai.provider.vmClient.CreateOrUpdate(ai.provider.ctx, ai.provider.azconfig.ResourceGroup, *ai.vm.Name, vmParameters)
+ if err != nil {
+ return WrapAzureError(err)
+ }
+ ai.vm = vm
+
+ return nil
+}
+
+func (ai *AzureInstance) Tags() InstanceTags {
+ tags := make(map[string]string)
+
+ for k, v := range ai.vm.Tags {
+ if strings.HasPrefix(k, "dispatch-") {
+ tags[k[9:]] = *v
+ }
+ }
+
+ return tags
+}
+
+func (ai *AzureInstance) Destroy() error {
+ ai.provider.stopWg.Add(1)
+ defer ai.provider.stopWg.Done()
+
+ _, err := ai.provider.vmClient.Delete(ai.provider.ctx, ai.provider.azconfig.ResourceGroup, *ai.vm.Name)
+ return WrapAzureError(err)
+}
+
+func (ai *AzureInstance) Address() string {
+ return *(*ai.nic.IPConfigurations)[0].PrivateIPAddress
+}
+
+func (ai *AzureInstance) VerifyHostKey(receivedKey ssh.PublicKey, client *ssh.Client) error {
+ ai.provider.stopWg.Add(1)
+ defer ai.provider.stopWg.Done()
+
+ remoteFingerprint := ssh.FingerprintSHA256(receivedKey)
+
+ tags := ai.Tags()
+
+ tg := tags["ssh-pubkey-fingerprint"]
+ if tg != "" {
+ if remoteFingerprint == tg {
+ return nil
+ } else {
+ return fmt.Errorf("Key fingerprint did not match, expected %q got %q", tg, remoteFingerprint)
+ }
+ }
+
+ nodetokenTag := tags["node-token"]
+ if nodetokenTag == "" {
+ return fmt.Errorf("Missing node token tag")
+ }
+
+ sess, err := client.NewSession()
+ if err != nil {
+ return err
+ }
+
+ nodetokenbytes, err := sess.Output("cat /home/crunch/node-token")
+ if err != nil {
+ return err
+ }
+
+ nodetoken := strings.TrimSpace(string(nodetokenbytes))
+
+ expectedToken := fmt.Sprintf("%s-%s", *ai.vm.Name, nodetokenTag)
+
+ if strings.TrimSpace(nodetoken) != expectedToken {
+ return fmt.Errorf("Node token did not match, expected %q got %q", expectedToken, nodetoken)
+ }
+
+ sess, err = client.NewSession()
+ if err != nil {
+ return err
+ }
+
+ keyfingerprintbytes, err := sess.Output("ssh-keygen -E sha256 -l -f /etc/ssh/ssh_host_rsa_key.pub")
+ if err != nil {
+ return err
+ }
+
+ sp := strings.Split(string(keyfingerprintbytes), " ")
+
+ if remoteFingerprint != sp[1] {
+ return fmt.Errorf("Key fingerprint did not match, expected %q got %q", sp[1], remoteFingerprint)
+ }
+
+ tags["ssh-pubkey-fingerprint"] = sp[1]
+ delete(tags, "node-token")
+ ai.SetTags(tags)
+ return nil
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+//
+//
+// How to manually run individual tests against the real cloud
+//
+// $ go test -v git.curoverse.com/arvados.git/lib/cloud -live-azure-cfg azconfig.yml -check.f=TestListInstances
+//
+// Example azconfig.yml:
+//
+// subscription_id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+// key: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+// region: centralus
+// cloud_environment: AzurePublicCloud
+// secret: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+// tenant_id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+// resource_group: zzzzz
+// network: zzzzz
+// subnet: zzzzz-subnet-private
+// storage_account: example
+// blob_container: vhds
+// image: "https://example.blob.core.windows.net/system/Microsoft.Compute/Images/images/zzzzz-compute-osDisk.XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.vhd"
+// delete_dangling_resources_after: 20
+// authorized_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLQS1ExT2+WjA0d/hntEAyAtgeN1W2ik2QX8c2zO6HjlPHWXL92r07W0WMuDib40Pcevpi1BXeBWXA9ZB5KKMJB+ukaAu22KklnQuUmNvk6ZXnPKSkGxuCYvPQb08WhHf3p1VxiKfP3iauedBDM4x9/bkJohlBBQiFXzNUcQ+a6rKiMzmJN2gbL8ncyUzc+XQ5q4JndTwTGtOlzDiGOc9O4z5Dd76wtAVJneOuuNpwfFRVHThpJM6VThpCZOnl8APaceWXKeuwOuCae3COZMz++xQfxOfZ9Z8aIwo+TlQhsRaNfZ4Vjrop6ej8dtfZtgUFKfbXEOYaHrGrWGotFDTD example@example"
+
+package cloud
+
+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"
+ "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-06-01/compute"
+ "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-06-01/network"
+ "github.com/Azure/azure-sdk-for-go/storage"
+ "github.com/Azure/go-autorest/autorest"
+ "github.com/Azure/go-autorest/autorest/azure"
+ "github.com/Azure/go-autorest/autorest/to"
+ "github.com/jmcvetta/randutil"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/crypto/ssh"
+ check "gopkg.in/check.v1"
+)
+
+type AzureInstanceSetSuite struct{}
+
+var _ = check.Suite(&AzureInstanceSetSuite{})
+
+type VirtualMachinesClientStub struct{}
+
+var testKey []byte = []byte(`ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDLQS1ExT2+WjA0d/hntEAyAtgeN1W2ik2QX8c2zO6HjlPHWXL92r07W0WMuDib40Pcevpi1BXeBWXA9ZB5KKMJB+ukaAu22KklnQuUmNvk6ZXnPKSkGxuCYvPQb08WhHf3p1VxiKfP3iauedBDM4x9/bkJohlBBQiFXzNUcQ+a6rKiMzmJN2gbL8ncyUzc+XQ5q4JndTwTGtOlzDiGOc9O4z5Dd76wtAVJneOuuNpwfFRVHThpJM6VThpCZOnl8APaceWXKeuwOuCae3COZMz++xQfxOfZ9Z8aIwo+TlQhsRaNfZ4Vjrop6ej8dtfZtgUFKfbXEOYaHrGrWGotFDTD example@example`)
+
+func (*VirtualMachinesClientStub) CreateOrUpdate(ctx context.Context,
+ resourceGroupName string,
+ VMName string,
+ parameters compute.VirtualMachine) (result compute.VirtualMachine, err error) {
+ parameters.ID = &VMName
+ parameters.Name = &VMName
+ return parameters, nil
+}
+
+func (*VirtualMachinesClientStub) Delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
+ return nil, nil
+}
+
+func (*VirtualMachinesClientStub) ListComplete(ctx context.Context, resourceGroupName string) (result compute.VirtualMachineListResultIterator, err error) {
+ return compute.VirtualMachineListResultIterator{}, nil
+}
+
+type InterfacesClientStub struct{}
+
+func (*InterfacesClientStub) CreateOrUpdate(ctx context.Context,
+ resourceGroupName string,
+ nicName string,
+ parameters network.Interface) (result network.Interface, err error) {
+ parameters.ID = to.StringPtr(nicName)
+ (*parameters.IPConfigurations)[0].PrivateIPAddress = to.StringPtr("192.168.5.5")
+ return parameters, nil
+}
+
+func (*InterfacesClientStub) Delete(ctx context.Context, resourceGroupName string, VMName string) (result *http.Response, err error) {
+ return nil, nil
+}
+
+func (*InterfacesClientStub) ListComplete(ctx context.Context, resourceGroupName string) (result network.InterfaceListResultIterator, err error) {
+ return network.InterfaceListResultIterator{}, nil
+}
+
+var live = flag.String("live-azure-cfg", "", "Test with real azure API, provide config file")
+
+func GetInstanceSet() (InstanceSet, ImageID, arvados.Cluster, error) {
+ cluster := arvados.Cluster{
+ InstanceTypes: arvados.InstanceTypeMap(map[string]arvados.InstanceType{
+ "tiny": arvados.InstanceType{
+ Name: "tiny",
+ ProviderType: "Standard_D1_v2",
+ VCPUs: 1,
+ RAM: 4000000000,
+ Scratch: 10000000000,
+ Price: .02,
+ Preemptible: false,
+ },
+ })}
+ if *live != "" {
+ cfg := make(map[string]interface{})
+ err := config.LoadFile(&cfg, *live)
+ if err != nil {
+ return nil, ImageID(""), cluster, err
+ }
+ ap, err := NewAzureInstanceSet(cfg, "test123", logrus.StandardLogger())
+ return ap, ImageID(cfg["image"].(string)), cluster, err
+ } else {
+ ap := AzureInstanceSet{
+ azconfig: AzureInstanceSetConfig{
+ BlobContainer: "vhds",
+ },
+ dispatcherID: "test123",
+ namePrefix: "compute-test123-",
+ logger: logrus.StandardLogger(),
+ deleteNIC: make(chan string),
+ deleteBlob: make(chan storage.Blob),
+ }
+ ap.ctx, ap.stopFunc = context.WithCancel(context.Background())
+ ap.vmClient = &VirtualMachinesClientStub{}
+ ap.netClient = &InterfacesClientStub{}
+ return &ap, ImageID("blob"), cluster, nil
+ }
+}
+
+func (*AzureInstanceSetSuite) TestCreate(c *check.C) {
+ ap, img, cluster, err := GetInstanceSet()
+ if err != nil {
+ c.Fatal("Error making provider", err)
+ }
+
+ pk, _, _, _, err := ssh.ParseAuthorizedKey(testKey)
+ c.Assert(err, check.IsNil)
+
+ nodetoken, err := randutil.String(40, "abcdefghijklmnopqrstuvwxyz0123456789")
+ c.Assert(err, check.IsNil)
+
+ inst, err := ap.Create(cluster.InstanceTypes["tiny"],
+ img, map[string]string{
+ "node-token": nodetoken},
+ pk)
+
+ c.Assert(err, check.IsNil)
+
+ tg := inst.Tags()
+ log.Printf("Result %v %v %v", inst.String(), inst.Address(), tg)
+
+}
+
+func (*AzureInstanceSetSuite) TestListInstances(c *check.C) {
+ ap, _, _, err := GetInstanceSet()
+ if err != nil {
+ c.Fatal("Error making provider", err)
+ }
+
+ l, err := ap.Instances(nil)
+
+ c.Assert(err, check.IsNil)
+
+ for _, i := range l {
+ tg := i.Tags()
+ log.Printf("%v %v %v", i.String(), i.Address(), tg)
+ }
+}
+
+func (*AzureInstanceSetSuite) TestManageNics(c *check.C) {
+ ap, _, _, err := GetInstanceSet()
+ if err != nil {
+ c.Fatal("Error making provider", err)
+ }
+
+ ap.(*AzureInstanceSet).ManageNics()
+ ap.Stop()
+}
+
+func (*AzureInstanceSetSuite) TestManageBlobs(c *check.C) {
+ ap, _, _, err := GetInstanceSet()
+ if err != nil {
+ c.Fatal("Error making provider", err)
+ }
+
+ ap.(*AzureInstanceSet).ManageBlobs()
+ ap.Stop()
+}
+
+func (*AzureInstanceSetSuite) TestDestroyInstances(c *check.C) {
+ ap, _, _, err := GetInstanceSet()
+ if err != nil {
+ c.Fatal("Error making provider", err)
+ }
+
+ l, err := ap.Instances(nil)
+ c.Assert(err, check.IsNil)
+
+ for _, i := range l {
+ c.Check(i.Destroy(), check.IsNil)
+ }
+}
+
+func (*AzureInstanceSetSuite) TestDeleteFake(c *check.C) {
+ ap, _, _, err := GetInstanceSet()
+ if err != nil {
+ c.Fatal("Error making provider", err)
+ }
+
+ _, err = ap.(*AzureInstanceSet).netClient.Delete(context.Background(), "fakefakefake", "fakefakefake")
+
+ de, ok := err.(autorest.DetailedError)
+ if ok {
+ rq := de.Original.(*azure.RequestError)
+
+ log.Printf("%v %q %q", rq.Response.StatusCode, rq.ServiceError.Code, rq.ServiceError.Message)
+ }
+}
+
+func (*AzureInstanceSetSuite) TestWrapError(c *check.C) {
+ retryError := autorest.DetailedError{
+ Original: &azure.RequestError{
+ DetailedError: autorest.DetailedError{
+ Response: &http.Response{
+ StatusCode: 429,
+ Header: map[string][]string{"Retry-After": []string{"123"}},
+ },
+ },
+ ServiceError: &azure.ServiceError{},
+ },
+ }
+ wrapped := WrapAzureError(retryError)
+ _, ok := wrapped.(RateLimitError)
+ c.Check(ok, check.Equals, true)
+
+ quotaError := autorest.DetailedError{
+ Original: &azure.RequestError{
+ DetailedError: autorest.DetailedError{
+ Response: &http.Response{
+ StatusCode: 503,
+ },
+ },
+ ServiceError: &azure.ServiceError{
+ Message: "No more quota",
+ },
+ },
+ }
+ wrapped = WrapAzureError(quotaError)
+ _, ok = wrapped.(QuotaError)
+ c.Check(ok, check.Equals, true)
+}
+
+func (*AzureInstanceSetSuite) TestSetTags(c *check.C) {
+ ap, _, _, err := GetInstanceSet()
+ if err != nil {
+ c.Fatal("Error making provider", err)
+ }
+ l, err := ap.Instances(nil)
+ c.Assert(err, check.IsNil)
+
+ if len(l) > 0 {
+ err = l[0].SetTags(map[string]string{"foo": "bar"})
+ if err != nil {
+ c.Fatal("Error setting tags", err)
+ }
+ }
+ l, err = ap.Instances(nil)
+ c.Assert(err, check.IsNil)
+
+ if len(l) > 0 {
+ tg := l[0].Tags()
+ log.Printf("tags are %v", tg)
+ }
+}
+
+func (*AzureInstanceSetSuite) TestSSH(c *check.C) {
+ ap, _, _, err := GetInstanceSet()
+ if err != nil {
+ c.Fatal("Error making provider", err)
+ }
+ l, err := ap.Instances(nil)
+ c.Assert(err, check.IsNil)
+
+ if len(l) > 0 {
+
+ sshclient, err := SetupSSHClient(c, l[0])
+ c.Assert(err, check.IsNil)
+
+ sess, err := sshclient.NewSession()
+ c.Assert(err, check.IsNil)
+
+ out, err := sess.Output("cat /home/crunch/node-token")
+ c.Assert(err, check.IsNil)
+
+ log.Printf("%v", string(out))
+
+ sshclient.Conn.Close()
+ }
+}
+
+func SetupSSHClient(c *check.C, inst Instance) (*ssh.Client, error) {
+ addr := inst.Address() + ":2222"
+ 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")
+ }
+
+ err = inst.VerifyHostKey(receivedKey, client)
+ c.Assert(err, check.IsNil)
+
+ return client, nil
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package cloud
+
+import (
+ "testing"
+
+ check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+ check.TestingT(t)
+}
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
+ "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
//
// var _ = registerCloudDriver("example", &exampleDriver{})
type Driver interface {
- InstanceSet(config map[string]interface{}, id InstanceSetID) (InstanceSet, error)
+ InstanceSet(config map[string]interface{}, id InstanceSetID, logger logrus.FieldLogger) (InstanceSet, error)
}
// DriverFunc makes a Driver using the provided function as its
// InstanceSet method. This is similar to http.HandlerFunc.
-func DriverFunc(fn func(config map[string]interface{}, id InstanceSetID) (InstanceSet, error)) Driver {
+func DriverFunc(fn func(config map[string]interface{}, id InstanceSetID, logger logrus.FieldLogger) (InstanceSet, error)) Driver {
return driverFunc(fn)
}
-type driverFunc func(config map[string]interface{}, id InstanceSetID) (InstanceSet, error)
+type driverFunc func(config map[string]interface{}, id InstanceSetID, logger logrus.FieldLogger) (InstanceSet, error)
-func (df driverFunc) InstanceSet(config map[string]interface{}, id InstanceSetID) (InstanceSet, error) {
- return df(config, id)
+func (df driverFunc) InstanceSet(config map[string]interface{}, id InstanceSetID, logger logrus.FieldLogger) (InstanceSet, error) {
+ return df(config, id, logger)
}
"git.curoverse.com/arvados.git/sdk/go/arvadostest"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
"git.curoverse.com/arvados.git/sdk/go/keepclient"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
check "gopkg.in/check.v1"
)
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
check "gopkg.in/check.v1"
)
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
"github.com/prometheus/client_golang/prometheus"
+ "github.com/sirupsen/logrus"
)
type typeChooser func(*arvados.Container) (arvados.InstanceType, error)
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/auth"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
- "github.com/Sirupsen/logrus"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
disp.sshKey = key
}
- instanceSet, err := newInstanceSet(disp.Cluster, disp.InstanceSetID)
+ instanceSet, err := newInstanceSet(disp.Cluster, disp.InstanceSetID, disp.logger)
if err != nil {
disp.logger.Fatalf("error initializing driver: %s", err)
}
"git.curoverse.com/arvados.git/lib/dispatchcloud/test"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
check "gopkg.in/check.v1"
)
"git.curoverse.com/arvados.git/lib/cloud"
"git.curoverse.com/arvados.git/sdk/go/arvados"
+ "github.com/sirupsen/logrus"
)
-var drivers = map[string]cloud.Driver{}
+var drivers = map[string]cloud.Driver{
+ "azure": cloud.DriverFunc(cloud.NewAzureInstanceSet),
+}
-func newInstanceSet(cluster *arvados.Cluster, setID cloud.InstanceSetID) (cloud.InstanceSet, error) {
+func newInstanceSet(cluster *arvados.Cluster, setID cloud.InstanceSetID, logger logrus.FieldLogger) (cloud.InstanceSet, error) {
driver, ok := drivers[cluster.CloudVMs.Driver]
if !ok {
return nil, fmt.Errorf("unsupported cloud driver %q", cluster.CloudVMs.Driver)
}
- return driver.InstanceSet(cluster.CloudVMs.DriverParameters, setID)
+ return driver.InstanceSet(cluster.CloudVMs.DriverParameters, setID, logger)
}
"git.curoverse.com/arvados.git/lib/cloud"
"git.curoverse.com/arvados.git/lib/dispatchcloud/container"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
func (sch *Scheduler) runQueue() {
"git.curoverse.com/arvados.git/lib/dispatchcloud/test"
"git.curoverse.com/arvados.git/lib/dispatchcloud/worker"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
check "gopkg.in/check.v1"
)
"sync"
"time"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
// A Scheduler maps queued containers onto unallocated workers in
"git.curoverse.com/arvados.git/lib/dispatchcloud/container"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
// sync resolves discrepancies between the queue and the pool:
"git.curoverse.com/arvados.git/lib/cloud"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
"github.com/mitchellh/mapstructure"
+ "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
}
// InstanceSet returns a new *StubInstanceSet.
-func (sd *StubDriver) InstanceSet(params map[string]interface{}, id cloud.InstanceSetID) (cloud.InstanceSet, error) {
+func (sd *StubDriver) InstanceSet(params map[string]interface{}, id cloud.InstanceSetID,
+ logger logrus.FieldLogger) (cloud.InstanceSet, error) {
+
sis := StubInstanceSet{
driver: sd,
servers: map[cloud.InstanceID]*StubVM{},
"git.curoverse.com/arvados.git/lib/cloud"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
"github.com/prometheus/client_golang/prometheus"
+ "github.com/sirupsen/logrus"
)
const (
"git.curoverse.com/arvados.git/lib/cloud"
"git.curoverse.com/arvados.git/lib/dispatchcloud/test"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
check "gopkg.in/check.v1"
)
"git.curoverse.com/arvados.git/lib/cloud"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
// State indicates whether a worker is available to do work, and (if
"git.curoverse.com/arvados.git/lib/cmd"
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
- "github.com/Sirupsen/logrus"
"github.com/coreos/go-systemd/daemon"
+ "github.com/sirupsen/logrus"
)
type Handler interface {
import (
"context"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
var (
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
const (
"time"
"git.curoverse.com/arvados.git/sdk/go/stats"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
type contextKey struct {
"testing"
"time"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
check "gopkg.in/check.v1"
)
"git.curoverse.com/arvados.git/sdk/go/auth"
"git.curoverse.com/arvados.git/sdk/go/stats"
- "github.com/Sirupsen/logrus"
"github.com/gogo/protobuf/jsonpb"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/sirupsen/logrus"
)
type Handler interface {
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
"git.curoverse.com/arvados.git/sdk/go/dispatch"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
var version = "dev"
"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
"git.curoverse.com/arvados.git/sdk/go/arvadostest"
"git.curoverse.com/arvados.git/sdk/go/dispatch"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
. "gopkg.in/check.v1"
)
"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
"git.curoverse.com/arvados.git/sdk/go/config"
"git.curoverse.com/arvados.git/sdk/go/dispatch"
- "github.com/Sirupsen/logrus"
"github.com/coreos/go-systemd/daemon"
+ "github.com/sirupsen/logrus"
)
type logger interface {
"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
"git.curoverse.com/arvados.git/sdk/go/arvadostest"
"git.curoverse.com/arvados.git/sdk/go/dispatch"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
. "gopkg.in/check.v1"
)
import (
"time"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
. "gopkg.in/check.v1"
)
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/health"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
- log "github.com/Sirupsen/logrus"
+ log "github.com/sirupsen/logrus"
)
var version = "dev"
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/keepclient"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
// Balancer compares the contents of keepstore servers with the
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
check "gopkg.in/check.v1"
)
"git.curoverse.com/arvados.git/sdk/go/arvadosclient"
"git.curoverse.com/arvados.git/sdk/go/arvadostest"
"git.curoverse.com/arvados.git/sdk/go/keepclient"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
check "gopkg.in/check.v1"
)
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/config"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
var debugf = func(string, ...interface{}) {}
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/auth"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
var version = "dev"
"git.curoverse.com/arvados.git/sdk/go/health"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
"git.curoverse.com/arvados.git/sdk/go/keepclient"
- log "github.com/Sirupsen/logrus"
+ log "github.com/sirupsen/logrus"
"golang.org/x/net/webdav"
)
"git.curoverse.com/arvados.git/sdk/go/arvados"
"git.curoverse.com/arvados.git/sdk/go/config"
- log "github.com/Sirupsen/logrus"
"github.com/coreos/go-systemd/daemon"
+ log "github.com/sirupsen/logrus"
)
var (
"git.curoverse.com/arvados.git/sdk/go/health"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
"git.curoverse.com/arvados.git/sdk/go/keepclient"
- log "github.com/Sirupsen/logrus"
"github.com/coreos/go-systemd/daemon"
"github.com/ghodss/yaml"
"github.com/gorilla/mux"
+ log "github.com/sirupsen/logrus"
)
var version = "dev"
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
type Config struct {
package main
import (
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
func init() {
"git.curoverse.com/arvados.git/sdk/go/ctxlog"
"git.curoverse.com/arvados.git/sdk/go/health"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
"golang.org/x/net/websocket"
)
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
- "github.com/Sirupsen/logrus"
+ "github.com/sirupsen/logrus"
)
var (
"revision": "888b4804f2653cd35ebcc95f046079e63b5b2799",
"revisionTime": "2017-07-27T13:52:37Z"
},
+ {
+ "checksumSHA1": "KF4DsRUpZ+h+qRQ/umRAQZfVvw0=",
+ "path": "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-06-01/compute",
+ "revision": "4e8cbbfb1aeab140cd0fa97fd16b64ee18c3ca6a",
+ "revisionTime": "2018-07-27T22:05:59Z"
+ },
+ {
+ "checksumSHA1": "IZNzp1cYx+xYHd4gzosKpG6Jr/k=",
+ "path": "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-06-01/network",
+ "revision": "4e8cbbfb1aeab140cd0fa97fd16b64ee18c3ca6a",
+ "revisionTime": "2018-07-27T22:05:59Z"
+ },
+ {
+ "checksumSHA1": "W4c2uTDJlwhfryWg9esshmJANo0=",
+ "path": "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2018-02-01/storage",
+ "revision": "4e8cbbfb1aeab140cd0fa97fd16b64ee18c3ca6a",
+ "revisionTime": "2018-07-27T22:05:59Z"
+ },
{
"checksumSHA1": "xHZe/h/tyrqmS9qiR03bLfRv5FI=",
"path": "github.com/Azure/azure-sdk-for-go/storage",
"revisionTime": "2018-02-14T01:17:07Z"
},
{
- "checksumSHA1": "LQWU/2M2E4L/hVzT9BVW1SkLrpA=",
+ "checksumSHA1": "1Y2+bSzYrdPHQqRjR1OrBMHAvxY=",
"path": "github.com/Azure/go-autorest/autorest",
- "revision": "a91c94d19d5efcb398b3aab64b8766e724aa7442",
- "revisionTime": "2017-11-30T17:00:06Z"
+ "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
+ "revisionTime": "2018-08-09T20:19:59Z"
},
{
- "checksumSHA1": "nBQ7cdhoeYUur6G6HG97uueoDmE=",
+ "checksumSHA1": "GxL0HHpZDj2milPhR3SPV6MWLPc=",
"path": "github.com/Azure/go-autorest/autorest/adal",
- "revision": "a91c94d19d5efcb398b3aab64b8766e724aa7442",
- "revisionTime": "2017-11-30T17:00:06Z"
+ "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
+ "revisionTime": "2018-08-09T20:19:59Z"
},
{
- "checksumSHA1": "zXyLmDVpkYkIsL0yinNLoW82IZc=",
+ "checksumSHA1": "ZNgwJOdHZmm4k/HJIbT1L5giO6M=",
"path": "github.com/Azure/go-autorest/autorest/azure",
- "revision": "a91c94d19d5efcb398b3aab64b8766e724aa7442",
- "revisionTime": "2017-11-30T17:00:06Z"
+ "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
+ "revisionTime": "2018-08-09T20:19:59Z"
+ },
+ {
+ "checksumSHA1": "6i7kwcXGTn55WqfubQs21swgr34=",
+ "path": "github.com/Azure/go-autorest/autorest/azure/auth",
+ "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
+ "revisionTime": "2018-08-09T20:19:59Z"
},
{
"checksumSHA1": "9nXCi9qQsYjxCeajJKWttxgEt0I=",
"path": "github.com/Azure/go-autorest/autorest/date",
- "revision": "a91c94d19d5efcb398b3aab64b8766e724aa7442",
- "revisionTime": "2017-11-30T17:00:06Z"
+ "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
+ "revisionTime": "2018-08-09T20:19:59Z"
+ },
+ {
+ "checksumSHA1": "SbBb2GcJNm5GjuPKGL2777QywR4=",
+ "path": "github.com/Azure/go-autorest/autorest/to",
+ "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
+ "revisionTime": "2018-08-09T20:19:59Z"
+ },
+ {
+ "checksumSHA1": "HjdLfAF3oA2In8F3FKh/Y+BPyXk=",
+ "path": "github.com/Azure/go-autorest/autorest/validation",
+ "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
+ "revisionTime": "2018-08-09T20:19:59Z"
+ },
+ {
+ "checksumSHA1": "b2lrPJRxf+MEfmMafN40wepi5WM=",
+ "path": "github.com/Azure/go-autorest/logger",
+ "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
+ "revisionTime": "2018-08-09T20:19:59Z"
+ },
+ {
+ "checksumSHA1": "UtAIMAsMWLBJ6yO1qZ0soFnb0sI=",
+ "path": "github.com/Azure/go-autorest/version",
+ "revision": "39013ecb48eaf6ced3f4e3e1d95515140ce6b3cf",
+ "revisionTime": "2018-08-09T20:19:59Z"
},
{
"checksumSHA1": "o/3cn04KAiwC7NqNVvmfVTD+hgA=",
"revision": "78439966b38d69bf38227fbf57ac8a6fee70f69a",
"revisionTime": "2017-08-04T20:09:54Z"
},
- {
- "checksumSHA1": "CWLxwFSj7MNed2MzAOSm0Cg9p+o=",
- "path": "github.com/Sirupsen/logrus",
- "revision": "d682213848ed68c0a260ca37d6dd5ace8423f5ba",
- "revisionTime": "2017-12-05T20:32:29Z"
- },
{
"checksumSHA1": "spyv5/YFBjYyZLZa1U2LBfDR8PM=",
"path": "github.com/beorn7/perks/quantile",
"revision": "dbeaa9332f19a944acb5736b4456cfcc02140e29",
"revisionTime": "2017-10-19T21:57:19Z"
},
+ {
+ "checksumSHA1": "7EjxkAUND/QY/sN+2fNKJ52v1Rc=",
+ "path": "github.com/dimchansky/utfbom",
+ "revision": "5448fe645cb1964ba70ac8f9f2ffe975e61a536c",
+ "revisionTime": "2018-07-13T13:37:17Z"
+ },
{
"checksumSHA1": "Gj+xR1VgFKKmFXYOJMnAczC3Znk=",
"path": "github.com/docker/distribution/digestset",
"revision": "1744e2970ca51c86172c8190fadad617561ed6e7",
"revisionTime": "2017-11-10T11:01:46Z"
},
+ {
+ "checksumSHA1": "ySaT8G3I3y4MmnoXOYAAX0rC+p8=",
+ "path": "github.com/sirupsen/logrus",
+ "revision": "d682213848ed68c0a260ca37d6dd5ace8423f5ba",
+ "revisionTime": "2017-12-05T20:32:29Z"
+ },
{
"checksumSHA1": "8QeSG127zQqbA+YfkO1WkKx/iUI=",
"path": "github.com/src-d/gcfg",
"revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8",
"revisionTime": "2017-11-25T19:00:56Z"
},
+ {
+ "checksumSHA1": "PJY7uCr3UnX4/Mf/RoWnbieSZ8o=",
+ "path": "golang.org/x/crypto/pkcs12",
+ "revision": "614d502a4dac94afa3a6ce146bd1736da82514c6",
+ "revisionTime": "2018-07-28T08:01:47Z"
+ },
+ {
+ "checksumSHA1": "p0GC51McIdA7JygoP223twJ1s0E=",
+ "path": "golang.org/x/crypto/pkcs12/internal/rc2",
+ "revision": "614d502a4dac94afa3a6ce146bd1736da82514c6",
+ "revisionTime": "2018-07-28T08:01:47Z"
+ },
{
"checksumSHA1": "NHjGg73p5iGZ+7tflJ4cVABNmKE=",
"path": "golang.org/x/crypto/ssh",