From 3a240993cdb719066d97c84ad4a2e28ed8bf6525 Mon Sep 17 00:00:00 2001 From: Andrey Pavlov Date: Mon, 23 Jan 2017 15:37:20 +0300 Subject: [PATCH 1/2] Support Azure for libstorage This patchset adds support of Azure Cloud. There are several operations implemented in patchset: create, attach, detach, remove (all base operations for volumes). --- Makefile | 1 + drivers/storage/azure/azure.go | 103 ++ .../storage/azure/executor/azure_executor.go | 143 +++ .../storage/azure/storage/azure_storage.go | 954 ++++++++++++++++++ drivers/storage/azure/tests/README.md | 17 + drivers/storage/azure/tests/azure_test.go | 447 ++++++++ drivers/storage/azure/utils/utils.go | 81 ++ drivers/storage/azure/utils/utils_test.go | 28 + drivers/storage/azure/utils/utils_unix.go | 19 + glide.lock | 79 +- glide.yaml | 9 + imports/executors/imports_executor.go | 1 + imports/executors/imports_executor_azure.go | 8 + imports/remote/imports_remote.go | 1 + imports/remote/imports_remote_azure.go | 8 + 15 files changed, 1880 insertions(+), 19 deletions(-) create mode 100644 drivers/storage/azure/azure.go create mode 100644 drivers/storage/azure/executor/azure_executor.go create mode 100644 drivers/storage/azure/storage/azure_storage.go create mode 100644 drivers/storage/azure/tests/README.md create mode 100644 drivers/storage/azure/tests/azure_test.go create mode 100644 drivers/storage/azure/utils/utils.go create mode 100644 drivers/storage/azure/utils/utils_test.go create mode 100644 drivers/storage/azure/utils/utils_unix.go create mode 100644 imports/executors/imports_executor_azure.go create mode 100644 imports/remote/imports_remote_azure.go diff --git a/Makefile b/Makefile index 5b8f2b42..49414377 100644 --- a/Makefile +++ b/Makefile @@ -857,6 +857,7 @@ $1: BUILD_TAGS="$$(BUILD_TAGS)" GOOS=$2 GOARCH=amd64 $$(MAKE) $$@ $1-clean: rm -f $1 + rm -f $(EXECUTORS_GENERATED) GO_PHONY += $1-clean GO_CLEAN += $1-clean endif diff --git a/drivers/storage/azure/azure.go b/drivers/storage/azure/azure.go new file mode 100644 index 00000000..1c6321ec --- /dev/null +++ b/drivers/storage/azure/azure.go @@ -0,0 +1,103 @@ +// +build !libstorage_storage_driver libstorage_storage_driver_azure + +package azure + +import ( + gofigCore "github.com/akutz/gofig" + gofig "github.com/akutz/gofig/types" +) + +const ( + // Name is the provider's name. + Name = "azure" + + // TagDelimiter separates tags from volume or snapshot names + TagDelimiter = "/" + + // DefaultUseHTTPS - Use https prefix by default + // or not for Azure URI's + DefaultUseHTTPS = true + + // TenantIDKey is a Directory ID from Azure + TenantIDKey = "tenantID" + // ClientIDKey is an Application ID from Azure + ClientIDKey = "clientID" + // ClientSecretKey is a secret of the application + ClientSecretKey = "clientSecret" + // CertPathKey is a path to application certificate in case of + // authorization via certificate + CertPathKey = "certPath" + + // StorageAccountKey is a name of storage account + StorageAccountKey = "storageAccount" + // StorageAccessKey is an access key of storage account + StorageAccessKey = "storageAccessKey" + // TODO: add option to pass StorageURI + + // SubscriptionIDKey is an ID of subscription + SubscriptionIDKey = "subscriptionID" + // ResourceGroupKey is a name of resource group + ResourceGroupKey = "resourceGroup" + // ContainerKey is a name of container in the storage account + // ('vhds' by default) + ContainerKey = "container" + // UseHTTPSKey is a flag about use https or not for making Azure URI's + UseHTTPSKey = "useHTTPS" + // TagKey is a tag key + TagKey = "tag" +) + +const ( + // ConfigAzure is a config key + ConfigAzure = Name + + // ConfigAzureSubscriptionIDKey is a config key + ConfigAzureSubscriptionIDKey = ConfigAzure + "." + SubscriptionIDKey + + // ConfigAzureResourceGroupKey is a config key + ConfigAzureResourceGroupKey = ConfigAzure + "." + ResourceGroupKey + + // ConfigAzureTenantIDKey is a config key + ConfigAzureTenantIDKey = ConfigAzure + "." + TenantIDKey + + // ConfigAzureStorageAccountKey is a config key + ConfigAzureStorageAccountKey = ConfigAzure + "." + StorageAccountKey + + // ConfigAzureStorageAccessKeyKey is a config key + ConfigAzureStorageAccessKeyKey = ConfigAzure + "." + StorageAccessKey + + // ConfigAzureContainerKey is a config key + ConfigAzureContainerKey = ConfigAzure + "." + ContainerKey + + // ConfigAzureClientIDKey is a config key + ConfigAzureClientIDKey = ConfigAzure + "." + ClientIDKey + + // ConfigAzureClientSecretKey is a config key + ConfigAzureClientSecretKey = ConfigAzure + "." + ClientSecretKey + + // ConfigAzureCertPathKey is a config key + ConfigAzureCertPathKey = ConfigAzure + "." + CertPathKey + + // ConfigAzureUseHTTPSKey is a config key + ConfigAzureUseHTTPSKey = ConfigAzure + "." + UseHTTPSKey + + // ConfigAzureTagKey is a config key + ConfigAzureTagKey = ConfigAzure + "." + TagKey +) + +func init() { + r := gofigCore.NewRegistration("Azure") + r.Key(gofig.String, "", "", "", ConfigAzureSubscriptionIDKey) + r.Key(gofig.String, "", "", "", ConfigAzureResourceGroupKey) + r.Key(gofig.String, "", "", "", ConfigAzureTenantIDKey) + r.Key(gofig.String, "", "", "", ConfigAzureStorageAccountKey) + r.Key(gofig.String, "", "", "", ConfigAzureContainerKey) + r.Key(gofig.String, "", "", "", ConfigAzureClientIDKey) + r.Key(gofig.String, "", "", "", ConfigAzureClientSecretKey) + r.Key(gofig.String, "", "", "", ConfigAzureCertPathKey) + r.Key(gofig.Bool, "", DefaultUseHTTPS, "", ConfigAzureUseHTTPSKey) + r.Key(gofig.String, "", "", + "Tag prefix for Azure naming", ConfigAzureTagKey) + + gofigCore.Register(r) +} diff --git a/drivers/storage/azure/executor/azure_executor.go b/drivers/storage/azure/executor/azure_executor.go new file mode 100644 index 00000000..b73c12b0 --- /dev/null +++ b/drivers/storage/azure/executor/azure_executor.go @@ -0,0 +1,143 @@ +// +build !libstorage_storage_executor libstorage_storage_executor_azure + +package executor + +import ( + "bufio" + "fmt" + "os" + "path" + "regexp" + "strings" + + gofig "github.com/akutz/gofig/types" + "github.com/akutz/goof" + + "github.com/codedellemc/libstorage/api/registry" + "github.com/codedellemc/libstorage/api/types" + "github.com/codedellemc/libstorage/drivers/storage/azure" + "github.com/codedellemc/libstorage/drivers/storage/azure/utils" +) + +// driver is the storage executor for the azure storage driver. +type driver struct { + config gofig.Config +} + +func init() { + registry.RegisterStorageExecutor(azure.Name, newDriver) +} + +func newDriver() types.StorageExecutor { + return &driver{} +} + +func (d *driver) Init(ctx types.Context, config gofig.Config) error { + ctx.Info("azure_executor: Init") + d.config = config + return nil +} + +func (d *driver) Name() string { + return azure.Name +} + +// Supported returns a flag indicating whether or not the platform +// implementing the executor is valid for the host on which the executor +// resides. +func (d *driver) Supported( + ctx types.Context, + opts types.Store) (bool, error) { + return utils.IsAzureInstance(ctx) +} + +// InstanceID returns the instance ID from the current instance from metadata +func (d *driver) InstanceID( + ctx types.Context, + opts types.Store) (*types.InstanceID, error) { + return utils.InstanceID(ctx) +} + +var errNoAvaiDevice = goof.New("no available device") +var nextDevRe = regexp.MustCompile("^/dev/" + + utils.NextDeviceInfo.Prefix + + "(" + utils.NextDeviceInfo.Pattern + ")") +var availLetters = []string{ + "c", "d", "f", "g", "h", "i", "j", "k", "l", "m", "n", + "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} + +// NextDevice returns the next available device. +func (d *driver) NextDevice( + ctx types.Context, + opts types.Store) (string, error) { + // All possible device paths on Linux instances are /dev/sd[c-z] + + // Find which letters are used for local devices + localDeviceNames := make(map[string]bool) + + localDevices, err := d.LocalDevices( + ctx, &types.LocalDevicesOpts{Opts: opts}) + if err != nil { + return "", goof.WithError("error getting local devices", err) + } + localDeviceMapping := localDevices.DeviceMap + + for localDevice := range localDeviceMapping { + res := nextDevRe.FindStringSubmatch(localDevice) + if len(res) > 0 { + localDeviceNames[res[1]] = true + } + } + + // Find next available letter for device path + for _, letter := range availLetters { + if localDeviceNames[letter] { + continue + } + return fmt.Sprintf( + "/dev/%s%s", utils.NextDeviceInfo.Prefix, letter), nil + } + return "", errNoAvaiDevice +} + +const procPartitions = "/proc/partitions" + +var devRX = regexp.MustCompile(`^sd[a-z]$`) + +// Retrieve device paths currently attached and/or mounted +func (d *driver) LocalDevices( + ctx types.Context, + opts *types.LocalDevicesOpts) (*types.LocalDevices, error) { + + f, err := os.Open(procPartitions) + if err != nil { + return nil, goof.WithError( + "error reading "+procPartitions, err) + } + defer f.Close() + + devMap := map[string]string{} + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) != 4 { + continue + } + devName := fields[3] + if !devRX.MatchString(devName) { + continue + } + devPath := path.Join("/dev/", devName) + devMap[devPath] = devPath + } + + ld := &types.LocalDevices{Driver: d.Name()} + if len(devMap) > 0 { + ld.DeviceMap = devMap + } + + ctx.WithField("devicemap", ld.DeviceMap).Debug("local devices") + + return ld, nil +} diff --git a/drivers/storage/azure/storage/azure_storage.go b/drivers/storage/azure/storage/azure_storage.go new file mode 100644 index 00000000..3a34e729 --- /dev/null +++ b/drivers/storage/azure/storage/azure_storage.go @@ -0,0 +1,954 @@ +// +build !libstorage_storage_driver libstorage_storage_driver_azure + +package storage + +import ( + "bytes" + "crypto/md5" + "crypto/rsa" + "crypto/x509" + "encoding/binary" + "fmt" + "hash" + "io/ioutil" + "strings" + "sync" + "time" + + gofig "github.com/akutz/gofig/types" + "github.com/akutz/goof" + "github.com/rubiojr/go-vhd/vhd" + + armCompute "github.com/Azure/azure-sdk-for-go/arm/compute" + blobStorage "github.com/Azure/azure-sdk-for-go/storage" + autorest "github.com/Azure/go-autorest/autorest/azure" + + "golang.org/x/crypto/pkcs12" + + "github.com/codedellemc/libstorage/api/context" + "github.com/codedellemc/libstorage/api/registry" + "github.com/codedellemc/libstorage/api/types" + "github.com/codedellemc/libstorage/drivers/storage/azure" + "github.com/codedellemc/libstorage/drivers/storage/azure/utils" +) + +const ( + // Name of Blob service in URL + blobServiceName = "blob" + + // Required by Azure blob name suffix + vhdExtension = ".vhd" + + // Size 1GB + size1GB int64 = 1024 * 1024 * 1024 + + // Default new disk size + defaultNewDiskSizeGB int32 = 128 +) + +type driver struct { + name string + config gofig.Config + subscriptionID string + resourceGroup string + tenantID string + storageAccount string + storageAccessKey string + container string + clientID string + clientSecret string + certPath string + useHTTPS bool +} + +func init() { + registry.RegisterStorageDriver(azure.Name, newDriver) +} + +func newDriver() types.StorageDriver { + return &driver{name: azure.Name} +} + +func (d *driver) Name() string { + return d.name +} + +// Init initializes the driver. +func (d *driver) Init(context types.Context, config gofig.Config) error { + d.config = config + d.tenantID = d.getTenantID() + d.clientID = d.getClientID() + if d.tenantID == "" || d.clientID == "" { + context.Error( + "tenantID or clientID are not set. Login will fail.") + } + d.clientSecret = d.getClientSecret() + d.certPath = d.getCertPath() + if d.clientSecret == "" && d.certPath == "" { + context.Error( + "clientSecret or certPath must be set for login.") + } + + d.storageAccount = d.getStorageAccount() + d.storageAccessKey = d.getStorageAccessKey() + if d.storageAccount == "" || d.storageAccessKey == "" { + context.Error( + "storageAccount and storageAccessKey " + + "are needed for correct work.") + } + d.container = d.getContainer() + + d.subscriptionID = d.getSubscriptionID() + if d.subscriptionID == "" { + context.Error("subscriptionID must be set for correct work.") + } + d.resourceGroup = d.getResourceGroup() + if d.resourceGroup == "" { + context.Warning("resourceGroup is not set." + + " Some operations will fail.") + } + + d.useHTTPS = d.getUseHTTPS() + + context.Info("storage driver initialized") + + return nil +} + +const cacheKeyC = "cacheKey" + +type azureSession struct { + vmClient *armCompute.VirtualMachinesClient + blobClient *blobStorage.BlobStorageClient +} + +var ( + sessions = map[string]*azureSession{} + sessionsL = &sync.Mutex{} +) + +func writeHkeyB(h hash.Hash, ps []byte) { + if ps == nil { + return + } + h.Write(ps) +} + +func writeHkey(h hash.Hash, ps *string) { + writeHkeyB(h, []byte(*ps)) +} + +var ( + errLoginMsg = "Failed to login to Azure" + errAuthFailed = goof.New(errLoginMsg) + invalideRsaPrivateKey = goof.New( + "PKCS#12 certificate must contain an RSA private key") +) + +func decodePkcs12( + pkcs []byte, + password string) (*x509.Certificate, *rsa.PrivateKey, error) { + + privateKey, certificate, err := pkcs12.Decode(pkcs, password) + if err != nil { + return nil, nil, err + } + + rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey) + if !isRsaKey { + return nil, nil, invalideRsaPrivateKey + } + + return certificate, rsaPrivateKey, nil +} + +func mustSession(ctx types.Context) *azureSession { + return context.MustSession(ctx).(*azureSession) +} + +func (d *driver) Login(ctx types.Context) (interface{}, error) { + sessionsL.Lock() + defer sessionsL.Unlock() + + ctx.Debug("login to azure storage driver") + var ( + hkey = md5.New() + ckey string + certData []byte + spt *autorest.ServicePrincipalToken + err error + ) + + if d.tenantID == "" { + return nil, goof.New("Empty tenantID") + } + + writeHkey(hkey, &d.subscriptionID) + writeHkey(hkey, &d.tenantID) + writeHkey(hkey, &d.storageAccount) + writeHkey(hkey, &d.storageAccessKey) + if d.clientID != "" && d.clientSecret != "" { + ctx.Debug("login to azure using clientID and clientSecret") + writeHkey(hkey, &d.clientID) + writeHkey(hkey, &d.clientSecret) + } else if d.certPath != "" { + ctx.Debug("login to azure using clientCert") + certData, err = ioutil.ReadFile(d.certPath) + if err != nil { + return nil, goof.WithError( + "Failed to read provided certificate file", + err) + } + writeHkeyB(hkey, certData) + } else { + ctx.Error("No login information provided") + return nil, errAuthFailed + } + ckey = fmt.Sprintf("%x", hkey.Sum(nil)) + + if session, ok := sessions[ckey]; ok { + ctx.WithField(cacheKeyC, ckey).Debug( + "using cached azure client") + return session, nil + } + + oauthConfig, err := autorest.PublicCloud.OAuthConfigForTenant( + d.tenantID) + if err != nil { + return nil, goof.WithError( + "Failed to create OAuthConfig for tenant", err) + } + + if d.clientID != "" && d.clientSecret != "" { + spt, err = autorest.NewServicePrincipalToken( + *oauthConfig, d.clientID, d.clientSecret, + autorest.PublicCloud.ResourceManagerEndpoint) + if err != nil { + return nil, goof.WithError( + "Failed to create Service Principal Token"+ + " with client ID and secret", err) + } + } else { + certificate, rsaPrivateKey, err := decodePkcs12(certData, "") + if err != nil { + return nil, goof.WithError( + "Failed to decode certificate data", err) + } + + spt, err = autorest.NewServicePrincipalTokenFromCertificate( + *oauthConfig, d.clientID, certificate, + rsaPrivateKey, + autorest.PublicCloud.ResourceManagerEndpoint) + if err != nil { + return nil, goof.WithError( + "Failed to create Service Principal Token"+ + " with certificate ", err) + } + } + + newVMC := armCompute.NewVirtualMachinesClient(d.subscriptionID) + newVMC.Authorizer = spt + newVMC.PollingDelay = 5 * time.Second + bc, err := blobStorage.NewBasicClient( + d.storageAccount, + d.storageAccessKey) + if err != nil { + return nil, goof.WithError( + "Failed to create BlobStorage client", err) + } + newBC := bc.GetBlobService() + session := azureSession{ + blobClient: &newBC, + vmClient: &newVMC, + } + sessions[ckey] = &session + + ctx.WithField(cacheKeyC, ckey).Info( + "login to azure storage driver created and cached") + + return &session, nil +} + +// NextDeviceInfo returns the information about the driver's next +// available device workflow. +func (d *driver) NextDeviceInfo( + ctx types.Context) (*types.NextDeviceInfo, error) { + return utils.NextDeviceInfo, nil +} + +// Type returns the type of storage the driver provides. +func (d *driver) Type(ctx types.Context) (types.StorageType, error) { + //Example: Block storage + return types.Block, nil +} + +// InstanceInspect returns an instance. +func (d *driver) InstanceInspect( + ctx types.Context, + opts types.Store) (*types.Instance, error) { + + iid := context.MustInstanceID(ctx) + return &types.Instance{ + Name: iid.ID, + InstanceID: iid, + ProviderName: iid.Driver, + }, nil +} + +// Volumes returns all volumes or a filtered list of volumes. +func (d *driver) Volumes( + ctx types.Context, + opts *types.VolumesOpts) ([]*types.Volume, error) { + + list, err := mustSession(ctx).blobClient.ListBlobs(d.container, + blobStorage.ListBlobsParameters{Include: "metadata"}) + if err != nil { + return nil, goof.WithError("error listing blobs", err) + } + // Convert retrieved volumes to libStorage types.Volume + vols, convErr := d.toTypesVolume(ctx, &list.Blobs, opts.Attachments) + if convErr != nil { + return nil, goof.WithError( + "error converting to types.Volume", convErr) + } + return vols, nil +} + +// VolumeInspect inspects a single volume. +func (d *driver) VolumeInspect( + ctx types.Context, + volumeID string, + opts *types.VolumeInspectOpts) (*types.Volume, error) { + + return d.getVolume(ctx, volumeID, opts.Attachments) +} + +// VolumeCreate creates a new volume. +func (d *driver) VolumeCreate(ctx types.Context, volumeName string, + opts *types.VolumeCreateOpts) (*types.Volume, error) { + + if opts.Encrypted != nil && *opts.Encrypted { + return nil, types.ErrNotImplemented + } + + id, ok := context.InstanceID(ctx) + if !ok || id == nil { + return nil, goof.New( + "Can't create volume outside of Azure instance") + } + vmName := id.ID + + if !strings.HasSuffix(volumeName, vhdExtension) { + ctx.Warning("Disk name doesn't end with '.vhd'." + + " This extension will be added automatically.") + volumeName = volumeName + vhdExtension + } + + size := int64(defaultNewDiskSizeGB) + if opts.Size != nil && *opts.Size != 0 { + size = *opts.Size + } + size *= size1GB + + fields := map[string]interface{}{ + "provider": d.Name(), + "vmName": vmName, + "volumeName": volumeName, + "size_in_bytes": size, + } + + _, err := d.getVM(ctx, vmName) + if err != nil { + return nil, goof.WithFieldsE(fields, + "VM could not be obtained.", err) + } + + blobClient := mustSession(ctx).blobClient + err = d.createDiskBlob(volumeName, size, blobClient) + if err != nil { + return nil, goof.WithFieldsE(fields, + "failed to create volume for VM", err) + } + + var volume *types.Volume + volume, err = d.getVolume(ctx, volumeName, types.VolAttNone) + if err != nil { + d.deleteDiskBlob(volumeName, blobClient) + return nil, goof.WithFieldsE(fields, + "failed to get just created/attached volume", err) + } + + return volume, nil +} + +// VolumeCreateFromSnapshot creates a new volume from an existing snapshot. +func (d *driver) VolumeCreateFromSnapshot( + ctx types.Context, + snapshotID, volumeName string, + opts *types.VolumeCreateOpts) (*types.Volume, error) { + // TODO Snapshots are not implemented yet + return nil, types.ErrNotImplemented +} + +// VolumeCopy copies an existing volume. +func (d *driver) VolumeCopy( + ctx types.Context, + volumeID, volumeName string, + opts types.Store) (*types.Volume, error) { + // TODO Snapshots are not implemented yet + return nil, types.ErrNotImplemented +} + +// VolumeSnapshot snapshots a volume. +func (d *driver) VolumeSnapshot( + ctx types.Context, + volumeID, snapshotName string, + opts types.Store) (*types.Snapshot, error) { + // TODO Snapshots are not implemented yet + return nil, types.ErrNotImplemented +} + +// VolumeRemove removes a volume. +func (d *driver) VolumeRemove( + ctx types.Context, + volumeID string, + opts *types.VolumeRemoveOpts) error { + + //TODO check if volume is attached? if so fail + + _, err := mustSession(ctx).blobClient.DeleteBlobIfExists( + d.container, volumeID, nil) + if err != nil { + fields := map[string]interface{}{ + "provider": d.Name(), + "volumeID": volumeID, + } + return goof.WithFieldsE(fields, "error removing volume", err) + } + return nil +} + +var ( + errMissingNextDevice = goof.New("missing next device") + errVolAlreadyAttached = goof.New("volume already attached to a host") +) + +// VolumeAttach attaches a volume and provides a token clients can use +// to validate that device has appeared locally. +func (d *driver) VolumeAttach( + ctx types.Context, + volumeID string, + opts *types.VolumeAttachOpts) (*types.Volume, string, error) { + + id, ok := context.InstanceID(ctx) + if !ok || id == nil { + return nil, "", goof.New( + "Can't attach volume outside of Azure instance") + } + vmName := id.ID + + fields := map[string]interface{}{ + "provider": d.Name(), + "vmName": vmName, + "volumeID": volumeID, + "nextDevice": *opts.NextDevice, + } + + volume, err := d.getVolume(ctx, volumeID, + types.VolumeAttachmentsRequested) + if err != nil { + return nil, "", goof.WithFieldsE(fields, + "failed to get volume for attach", err) + } + // Check if volume is already attached + // TODO: maybe add the check that new instance is the same as current + if len(volume.Attachments) > 0 { + // Detach already attached volume if forced + if !opts.Force { + return nil, "", errVolAlreadyAttached + } + _, err = d.VolumeDetach( + ctx, + volumeID, + &types.VolumeDetachOpts{ + Force: opts.Force, + Opts: opts.Opts, + }) + if err != nil { + return nil, "", goof.WithFieldsE(fields, + "failed to detach volume first", err) + } + } + + if opts.NextDevice == nil { + return nil, "", errMissingNextDevice + } + + vm, err := d.getVM(ctx, vmName) + if err != nil { + return nil, "", goof.WithFieldsE(fields, + "VM could not be obtained.", err) + } + + err = d.attachDisk(ctx, volumeID, volume.Size*size1GB, vm) + if err != nil { + return nil, "", goof.WithFieldsE(fields, + "failed to attach volume.", err) + } + + volume, err = d.getVolume(ctx, volumeID, + types.VolumeAttachmentsRequested) + if err != nil { + return nil, "", goof.WithFieldsE(fields, + "failed to get just created/attached volume", err) + } + if len(volume.Attachments) == 0 { + return nil, "", goof.WithFieldsE(fields, + "volume is not attached to VM", err) + } + + return volume, *opts.NextDevice, nil +} + +var errVolAlreadyDetached = goof.New("volume already detached") + +// VolumeDetach detaches a volume. +func (d *driver) VolumeDetach( + ctx types.Context, + volumeID string, + opts *types.VolumeDetachOpts) (*types.Volume, error) { + + id, ok := context.InstanceID(ctx) + if !ok || id == nil { + return nil, goof.New( + "Can't detach volume outside of Azure instance") + } + vmName := id.ID + + fields := map[string]interface{}{ + "provider": d.Name(), + "vmName": vmName, + "volumeID": volumeID, + } + + volume, err := d.getVolume(ctx, volumeID, + types.VolumeAttachmentsRequested) + if err != nil { + return nil, goof.WithFieldsE(fields, + "failed to get volume", err) + } + if len(volume.Attachments) == 0 { + return nil, errVolAlreadyDetached + } + + vm, err := d.getVM(ctx, vmName) + if err != nil { + return nil, goof.WithFieldsE(fields, + "failed to detach volume to VM", err) + } + + disks := *vm.StorageProfile.DataDisks + for i, disk := range disks { + // Disk is paged blob in Azure. So blob name is case sensitive. + if disk.Name != nil && *disk.Name == volumeID { + // found the disk + disks = append(disks[:i], disks[i+1:]...) + break + } + } + newVM := armCompute.VirtualMachine{ + Location: vm.Location, + VirtualMachineProperties: &armCompute.VirtualMachineProperties{ + StorageProfile: &armCompute.StorageProfile{ + DataDisks: &disks, + }, + }, + } + + _, err = mustSession(ctx).vmClient.CreateOrUpdate(d.resourceGroup, + vmName, newVM, nil) + if err != nil { + return nil, goof.WithFieldsE(fields, + "failed to detach volume", err) + } + + volume, err = d.getVolume(ctx, volumeID, + types.VolumeAttachmentsRequested) + if err != nil { + return nil, goof.WithFieldsE(fields, + "failed to get volume", err) + } + if len(volume.Attachments) != 0 { + return nil, goof.WithFieldsE(fields, + "volume is not detached", err) + } + return volume, nil +} + +// Snapshots returns all volumes or a filtered list of snapshots. +func (d *driver) Snapshots( + ctx types.Context, + opts types.Store) ([]*types.Snapshot, error) { + // TODO Snapshots are not implemented yet + return nil, types.ErrNotImplemented +} + +// SnapshotInspect inspects a single snapshot. +func (d *driver) SnapshotInspect( + ctx types.Context, + snapshotID string, + opts types.Store) (*types.Snapshot, error) { + // TODO Snapshots are not implemented yet + return nil, types.ErrNotImplemented +} + +// SnapshotCopy copies an existing snapshot. +func (d *driver) SnapshotCopy( + ctx types.Context, + snapshotID, snapshotName, destinationID string, + opts types.Store) (*types.Snapshot, error) { + // TODO Snapshots are not implemented yet + return nil, types.ErrNotImplemented +} + +// SnapshotRemove removes a snapshot. +func (d *driver) SnapshotRemove( + ctx types.Context, + snapshotID string, + opts types.Store) error { + // TODO Snapshots are not implemented yet + return types.ErrNotImplemented +} + +// Get volume or snapshot name without config tag +func (d *driver) getPrintableName(name string) string { + return strings.TrimPrefix(name, d.tag()+azure.TagDelimiter) +} + +// Prefix volume or snapshot name with config tag +func (d *driver) getFullName(name string) string { + if d.tag() != "" { + return d.tag() + azure.TagDelimiter + name + } + return name +} + +// Retrieve config arguments +func (d *driver) getSubscriptionID() string { + return d.config.GetString(azure.ConfigAzureSubscriptionIDKey) +} + +func (d *driver) getResourceGroup() string { + return d.config.GetString(azure.ConfigAzureResourceGroupKey) +} + +func (d *driver) getTenantID() string { + return d.config.GetString(azure.ConfigAzureTenantIDKey) +} + +func (d *driver) getStorageAccount() string { + return d.config.GetString(azure.ConfigAzureStorageAccountKey) +} + +func (d *driver) getStorageAccessKey() string { + return d.config.GetString(azure.ConfigAzureStorageAccessKeyKey) +} + +func (d *driver) getContainer() string { + result := d.config.GetString(azure.ConfigAzureContainerKey) + if result == "" { + result = "vhds" + } + return result +} + +func (d *driver) getClientID() string { + return d.config.GetString(azure.ConfigAzureClientIDKey) +} + +func (d *driver) getClientSecret() string { + return d.config.GetString(azure.ConfigAzureClientSecretKey) +} + +func (d *driver) getCertPath() string { + return d.config.GetString(azure.ConfigAzureCertPathKey) +} + +func (d *driver) getUseHTTPS() bool { + return d.config.GetBool(azure.ConfigAzureUseHTTPSKey) +} + +func (d *driver) tag() string { + return d.config.GetString(azure.ConfigAzureTagKey) +} + +// TODO rexrayTag +/*func (d *driver) rexrayTag() string { + return d.config.GetString("azure.rexrayTag") +}*/ + +// var errGetLocDevs = goof.New("error getting local devices from context") + +func (d *driver) toTypesVolume( + ctx types.Context, + blobs *[]blobStorage.Blob, + attachments types.VolumeAttachmentsTypes) ([]*types.Volume, error) { + + /* TODO: + var ( + ld *types.LocalDevices + ldOK bool + ) + + if attachments.Devices() { + // Get local devices map from context + if ld, ldOK = context.LocalDevices(ctx); !ldOK { + return nil, errGetLocDevs + } + } + */ + + var volumesSD []*types.Volume + for _, blob := range *blobs { + volumeSD, err := d.toTypeVolume(ctx, &blob, attachments) + if err != nil { + ctx.WithError(err).Warning("Failed to convert volume") + } else if volumeSD != nil { + volumesSD = append(volumesSD, volumeSD) + } + } + return volumesSD, nil +} + +func (d *driver) toTypeVolume( + ctx types.Context, + blob *blobStorage.Blob, + attachments types.VolumeAttachmentsTypes) (*types.Volume, error) { + + // Metadata can have these fileds: + // microsoftazurecompute_resourcegroupname:trex + // microsoftazurecompute_vmname:ttt + // microsoftazurecompute_disktype:DataDisk (or OSDisk) + // microsoftazurecompute_diskid:7d9df1c9-7b4f-41d4-a2e3-6870dfa573ba + // microsoftazurecompute_diskname:ttt-20161221-130722 + // microsoftazurecompute_disksizeingb:50 + + btype := blob.Metadata["microsoftazurecompute_disktype"] + if btype == "" && !strings.HasSuffix(blob.Name, vhdExtension) { + return nil, nil + } + attachState := types.VolumeAvailable + bstate := "detached" + if blob.Metadata["microsoftazurecompute_vmname"] != "" { + bstate = "attached" + attachState = types.VolumeAttached + } + var attachmentsSD []*types.VolumeAttachment + if attachments.Requested() && attachState == types.VolumeAttached { + attachedIID := types.InstanceID{ + ID: blob.Metadata["microsoftazurecompute_vmname"], + Driver: d.name, + } + if attachments.Mine() { + id, ok := context.InstanceID(ctx) + if !ok || id == nil { + return nil, goof.New("Can't get isntance" + + " ID to filter volume") + } + + if id.ID == attachedIID.ID { + attachmentsSD = append( + attachmentsSD, + &types.VolumeAttachment{ + InstanceID: &attachedIID, + VolumeID: blob.Name, + }) + } + } else { + attachmentsSD = append( + attachmentsSD, + &types.VolumeAttachment{ + InstanceID: &attachedIID, + VolumeID: blob.Name, + }) + } + } + + volumeSD := &types.Volume{ + Name: blob.Name, + ID: blob.Name, + Status: bstate, + Type: btype, + Size: blob.Properties.ContentLength / size1GB, + AttachmentState: attachState, + Attachments: attachmentsSD, + // TODO: + //AvailabilityZone: *volume.AvailabilityZone, + //Encrypted: *volume.Encrypted, + } + + // Some volume types have no IOPS, so we get nil in volume.Iops + //if volume.Iops != nil { + // volumeSD.IOPS = *volume.Iops + //} + + return volumeSD, nil +} + +func (d *driver) diskURI(name string) string { + scheme := "http" + if d.useHTTPS { + scheme = "https" + } + host := fmt.Sprintf("%s://%s.%s.%s", scheme, d.storageAccount, + blobServiceName, autorest.PublicCloud.StorageEndpointSuffix) + uri := fmt.Sprintf("%s/%s/%s", host, d.container, name) + return uri +} + +func (d *driver) getVM(ctx types.Context, name string) ( + *armCompute.VirtualMachine, error) { + + vm, err := mustSession(ctx).vmClient.Get(d.resourceGroup, name, "") + if err != nil { + fields := map[string]interface{}{ + "provider": d.Name(), + "vmName": name, + } + return nil, goof.WithFieldsE( + fields, "failed to get virtual machine", err) + } + + return &vm, nil +} + +func (d *driver) getVolume( + ctx types.Context, + volumeID string, + attachments types.VolumeAttachmentsTypes) (*types.Volume, error) { + + list, err := mustSession(ctx).blobClient.ListBlobs(d.container, + blobStorage.ListBlobsParameters{ + Prefix: volumeID, + Include: "metadata"}) + if err != nil { + return nil, goof.WithError("error listing blobs", err) + } + if len(list.Blobs) == 0 { + return nil, goof.New("error to get volume") + } + // Convert retrieved volumes to libStorage types.Volume + return d.toTypeVolume(ctx, &list.Blobs[0], attachments) +} + +func (d *driver) createDiskBlob( + name string, + size int64, + blobClient *blobStorage.BlobStorageClient) error { + + // create new blob + vhdSize := size + vhd.VHD_HEADER_SIZE + err := blobClient.PutPageBlob(d.container, name, vhdSize, nil) + if err != nil { + return goof.WithError("PageBlob could not be created.", err) + } + + // add VHD signature + h := vhd.CreateFixedHeader(uint64(size), &vhd.VHDOptions{}) + b := new(bytes.Buffer) + err = binary.Write(b, binary.BigEndian, h) + if err != nil { + d.deleteDiskBlob(name, blobClient) + return goof.WithError("Vhd header could not be created.", err) + } + header := b.Bytes() + err = blobClient.PutPage(d.container, name, size, vhdSize-1, + blobStorage.PageWriteTypeUpdate, + header[:vhd.VHD_HEADER_SIZE], nil) + if err != nil { + d.deleteDiskBlob(name, blobClient) + return goof.WithError( + "Vhd header could not be updated in the blob.", err) + } + + return nil +} + +func (d *driver) deleteDiskBlob( + blobName string, + blobClient *blobStorage.BlobStorageClient) error { + + return blobClient.DeleteBlob(d.container, blobName, nil) +} + +func (d *driver) getNextDiskLun( + vm *armCompute.VirtualMachine) (int32, error) { + + // 64 is a max number of LUNs per VM + used := make([]bool, 64) + disks := *vm.StorageProfile.DataDisks + for _, disk := range disks { + if disk.Lun != nil { + used[*disk.Lun] = true + } + } + for k, v := range used { + if !v { + return int32(k), nil + } + } + return -1, goof.New("Free Lun could not be found.") +} + +func (d *driver) attachDisk( + ctx types.Context, + volumeName string, + size int64, + vm *armCompute.VirtualMachine) error { + + lun, err := d.getNextDiskLun(vm) + if err != nil { + return goof.WithError( + "Could not find find an empty Lun to attach disk to.", + err) + } + + uri := d.diskURI(volumeName) + disks := *vm.StorageProfile.DataDisks + sizeGB := int32(size / size1GB) + disks = append(disks, + armCompute.DataDisk{ + Name: &volumeName, + Vhd: &armCompute.VirtualHardDisk{URI: &uri}, + Lun: &lun, + CreateOption: armCompute.Attach, + DiskSizeGB: &sizeGB, + // TODO: + // Caching: cachingMode, + }) + newVM := armCompute.VirtualMachine{ + Location: vm.Location, + VirtualMachineProperties: &armCompute.VirtualMachineProperties{ + StorageProfile: &armCompute.StorageProfile{ + DataDisks: &disks, + }, + }, + } + + _, err = mustSession(ctx).vmClient.CreateOrUpdate(d.resourceGroup, + *vm.Name, newVM, nil) + if err != nil { + detail := err.Error() + if strings.Contains(detail, + "Code=\"AcquireDiskLeaseFailed\"") { + + // if lease cannot be acquired, immediately detach + // the disk and return the original error + ctx.Info("failed to acquire disk lease, try detach") + _, _ = d.VolumeDetach(ctx, volumeName, nil) + } + return goof.WithError("failed to attach volume to VM", err) + } + + return nil +} diff --git a/drivers/storage/azure/tests/README.md b/drivers/storage/azure/tests/README.md new file mode 100644 index 00000000..60517f0e --- /dev/null +++ b/drivers/storage/azure/tests/README.md @@ -0,0 +1,17 @@ +Functional tests for Azure driver. + +It requires to be run either inside of Azure instance or instance ID should be defined via environment variable 'AZURE_INSTANCE_ID' that points to existing running Azure instance. + +In order to run test the following environment variables should be defined (they should be filled with your data): + AZURE_INSTANCE_ID= # it is required if run test outside Azure instance + AZURE_SUBSCRIPTION_ID= # your subscription ID + AZURE_RESOURCE_GROUP= # your resource group name + AZURE_TENANT_ID= # your tenant ID + AZURE_CLIENT_ID= # id of your client (application) + AZURE_CLIENT_SECRET= # your client(application) secret key + AZURE_CONTAINER= # name of container for disk blob objects, e.g. 'vhds' + AZURE_STORAGE_ACCOUNT= # your storage account name + AZURE_STORAGE_ACCESS_KEY= # your storage account access key + +The driver and tests do not create container, instance, etc, all entities should be created before to run tests / use of libstorage. + diff --git a/drivers/storage/azure/tests/azure_test.go b/drivers/storage/azure/tests/azure_test.go new file mode 100644 index 00000000..c2f8d51d --- /dev/null +++ b/drivers/storage/azure/tests/azure_test.go @@ -0,0 +1,447 @@ +// +build !libstorage_storage_driver libstorage_storage_driver_azure + +package azure + +import ( + "os" + "strconv" + "strings" + "testing" + + log "github.com/Sirupsen/logrus" + gofig "github.com/akutz/gofig/types" + + "github.com/stretchr/testify/assert" + + "github.com/codedellemc/libstorage/api/context" + "github.com/codedellemc/libstorage/api/registry" + "github.com/codedellemc/libstorage/api/server" + apitests "github.com/codedellemc/libstorage/api/tests" + "github.com/codedellemc/libstorage/api/types" + "github.com/codedellemc/libstorage/api/utils" + "github.com/codedellemc/libstorage/drivers/storage/azure" + azureUtils "github.com/codedellemc/libstorage/drivers/storage/azure/utils" +) + +// Put contents of sample config.yml here +var ( + configYAMLazure = []byte(` + azure: + resourceGroup: "trex" + subscriptionID: "c971aa51-5850-460a-b300-3265d4af154b" + tenantID: "ebbc4596-9828-453c-b95e-b8cb122f45bd" + clientID: "5d7fbebc-2e7b-487d-bf6e-04e4bee8e8cc" + clientSecret: "fill_your_secret" + certPath: "" + container: "vhds" + storageAccount: "trexdisks256" + storageAccessKey: "fill_your_seceret" +`) +) + +var volumeName string +var volumeName2 string + +type CleanupIface interface { + cleanup(key string) +} + +type CleanupObjectContextT struct { + objects map[string]CleanupIface +} + +var cleanupObjectContext CleanupObjectContextT + +func (ctx *CleanupObjectContextT) remove(key string) { + delete(ctx.objects, key) +} + +func (ctx *CleanupObjectContextT) cleanup() { + for key, value := range ctx.objects { + value.cleanup(key) + delete(ctx.objects, key) + } +} + +type CleanupVolume struct { + vol *types.Volume + client types.Client +} + +func (ctx *CleanupObjectContextT) add(key string, vol *types.Volume, client types.Client) { + cobj := &CleanupVolume{vol: vol, client: client} + ctx.objects[key] = cobj +} + +func (cvol *CleanupVolume) cleanup(key string) { + cvol.client.API().VolumeDetach(nil, azure.Name, cvol.vol.Name, &types.VolumeDetachRequest{Force: true}) + cvol.client.API().VolumeRemove(nil, azure.Name, cvol.vol.Name) +} + +// Check environment vars to see whether or not to run this test +func skipTests() bool { + travis, _ := strconv.ParseBool(os.Getenv("TRAVIS")) + noTestAZURE, _ := strconv.ParseBool(os.Getenv("TEST_SKIP_AZURE")) + return travis || noTestAZURE +} + +// Set volume names to first part of UUID before the - +func init() { + uuid, _ := types.NewUUID() + uuids := strings.Split(uuid.String(), "-") + volumeName = "test-vol-" + uuids[0] + ".vhd" + uuid, _ = types.NewUUID() + uuids = strings.Split(uuid.String(), "-") + volumeName2 = "test-vol-" + uuids[0] + ".vhd" + cleanupObjectContext = CleanupObjectContextT{ + objects: make(map[string]CleanupIface), + } +} + +func TestMain(m *testing.M) { + server.CloseOnAbort() + ec := m.Run() + os.Exit(ec) +} + +/////////////////////////////////////////////////////////////////////// +///////// PUBLIC TESTS ///////// +/////////////////////////////////////////////////////////////////////// +func TestConfig(t *testing.T) { + if skipTests() { + t.SkipNow() + } + tf := func(config gofig.Config, client types.Client, t *testing.T) { + assert.NotEqual(t, config.GetString("azure.clientID"), "") + assert.Equal(t, config.GetString("azure.clientID"), + "5d7fbebc-2e7b-487d-bf6e-04e4bee8e8cc") + } + apitests.Run(t, azure.Name, configYAMLazure, tf) + cleanupObjectContext.cleanup() +} + +// Check if InstanceID metadata is properly returned by executor +// and InstanceID.ID is filled out by InstanceInspect +func TestInstanceID(t *testing.T) { + if skipTests() { + t.SkipNow() + } + + // create storage driver + sd, err := registry.NewStorageDriver(azure.Name) + if err != nil { + t.Fatal(err) + } + + // initialize storage driver + ctx := context.Background() + if err := sd.Init(ctx, registry.NewConfig()); err != nil { + t.Fatal(err) + } + // Get Instance ID metadata from executor + iid, err := azureUtils.InstanceID(ctx) + assert.NoError(t, err) + if err != nil { + t.Fatal(err) + } + + // Fill in Instance ID's ID field with InstanceInspect + ctx = ctx.WithValue(context.InstanceIDKey, iid) + i, err := sd.InstanceInspect(ctx, utils.NewStore()) + if err != nil { + t.Fatal(err) + } + + iid = i.InstanceID + + // test resulting InstanceID + apitests.Run( + t, azure.Name, configYAMLazure, + (&apitests.InstanceIDTest{ + Driver: azure.Name, + Expected: iid, + }).Test) + cleanupObjectContext.cleanup() +} + +// Test if Services are configured and returned properly from the client +func TestServices(t *testing.T) { + if skipTests() { + t.SkipNow() + } + + tf := func(config gofig.Config, client types.Client, t *testing.T) { + reply, err := client.API().Services(nil) + assert.NoError(t, err) + assert.Equal(t, len(reply), 1) + + _, ok := reply[azure.Name] + assert.True(t, ok) + } + apitests.Run(t, azure.Name, configYAMLazure, tf) + cleanupObjectContext.cleanup() +} + +// Test volume functionality from storage driver +func TestVolumeCreateRemove(t *testing.T) { + if skipTests() { + t.SkipNow() + } + + tf := func(config gofig.Config, client types.Client, t *testing.T) { + vol := volumeCreate(t, client, volumeName) + volumeRemove(t, client, vol.ID) + } + apitests.Run(t, azure.Name, configYAMLazure, tf) + cleanupObjectContext.cleanup() +} + +// Test volume functionality from storage driver +func TestVolumes(t *testing.T) { + if skipTests() { + t.SkipNow() + } + + tf := func(config gofig.Config, client types.Client, t *testing.T) { + _ = volumeCreate(t, client, volumeName) + _ = volumeCreate(t, client, volumeName2) + + vol1 := volumeByName(t, client, volumeName) + vol2 := volumeByName(t, client, volumeName2) + + volumeRemove(t, client, vol1.ID) + volumeRemove(t, client, vol2.ID) + } + apitests.Run(t, azure.Name, configYAMLazure, tf) + cleanupObjectContext.cleanup() +} + +// Test volume functionality from storage driver +func TestVolumeAttachDetach(t *testing.T) { + if skipTests() { + t.SkipNow() + } + var vol *types.Volume + tf := func(config gofig.Config, client types.Client, t *testing.T) { + vol = volumeCreate(t, client, volumeName) + _ = volumeAttach(t, client, vol.ID) + _ = volumeInspectAttached(t, client, vol.ID) + _ = volumeInspectAttachedToMyInstance(t, client, vol.ID) + } + tf2 := func(config gofig.Config, client types.Client, t *testing.T) { + _ = volumeInspectAttachedToMyInstanceWithForeignInstance(t, client, vol.ID) + } + tf3 := func(config gofig.Config, client types.Client, t *testing.T) { + _ = volumeDetach(t, client, vol.ID) + _ = volumeInspectDetached(t, client, vol.ID) + volumeRemove(t, client, vol.ID) + } + apitests.Run(t, azure.Name, configYAMLazure, tf) + apitests.RunWithClientType(t, types.ControllerClient, azure.Name, configYAMLazure, tf2) + apitests.Run(t, azure.Name, configYAMLazure, tf3) + cleanupObjectContext.cleanup() +} + +/////////////////////////////////////////////////////////////////////// +///////// PRIVATE TESTS FOR VOLUME FUNCTIONALITY ///////// +/////////////////////////////////////////////////////////////////////// +// Test volume creation specifying size and volume name +func volumeCreate( + t *testing.T, client types.Client, volumeName string) *types.Volume { + log.WithField("volumeName", volumeName).Info("creating volume") + // Prepare request for storage driver call to create volume + size := int64(1) + + opts := map[string]interface{}{ + "priority": 2, + "owner": "root@example.com", + } + volumeCreateRequest := &types.VolumeCreateRequest{ + Name: volumeName, + Size: &size, + Opts: opts, + } + // Send request and retrieve created libStorage types.Volume + vol, err := client.API().VolumeCreate(nil, azure.Name, volumeCreateRequest) + assert.NoError(t, err) + if err != nil { + t.FailNow() + t.Error("failed volumeCreate") + } + apitests.LogAsJSON(vol, t) + // Add obj to automated cleanup in case of errors + cleanupObjectContext.add(vol.ID, vol, client) + // Check volume options + assert.Equal(t, volumeName, vol.Name) + assert.Equal(t, size, vol.Size) + return vol +} + +// Test volume retrieval by volume name using Volumes, which retrieves all volumes +// from the storage driver without filtering, and filters the volumes externally. +func volumeByName( + t *testing.T, client types.Client, volumeName string) *types.Volume { + log.WithField("volumeName", volumeName).Info("get volume by azure.Name") + // Retrieve all volumes + vols, err := client.API().Volumes(nil, 0) + assert.NoError(t, err) + if err != nil { + t.FailNow() + } + // Filter volumes to those under the azure service, + // and find a volume matching inputted volume name + assert.Contains(t, vols, azure.Name) + for _, vol := range vols[azure.Name] { + if vol.Name == volumeName { + return vol + } + } + // No matching volumes found + t.FailNow() + t.Error("failed volumeByName") + return nil +} + +// Test volume removal by volume ID +func volumeRemove(t *testing.T, client types.Client, volumeID string) { + log.WithField("volumeID", volumeID).Info("removing volume") + err := client.API().VolumeRemove( + nil, azure.Name, volumeID) + assert.NoError(t, err) + if err != nil { + t.Error("failed volumeRemove") + t.FailNow() + } + cleanupObjectContext.remove(volumeID) +} + +// Test volume attachment by volume ID +func volumeAttach( + t *testing.T, client types.Client, volumeID string) *types.Volume { + log.WithField("volumeID", volumeID).Info("attaching volume") + // Get next device name from executor + nextDevice, err := client.Executor().NextDevice(context.Background().WithValue(context.ServiceKey, azure.Name), + utils.NewStore()) + assert.NoError(t, err) + if err != nil { + t.Error("error getting next device name from executor") + t.FailNow() + } + reply, token, err := client.API().VolumeAttach( + nil, azure.Name, volumeID, &types.VolumeAttachRequest{ + NextDeviceName: &nextDevice, + }) + assert.NoError(t, err) + if err != nil { + t.Error("failed volumeAttach") + t.FailNow() + } + apitests.LogAsJSON(reply, t) + assert.NotEqual(t, token, "") + return reply +} + +// Test volume retrieval by volume ID using VolumeInspect, which directly +// retrieves matching volumes from the storage driver. Contrast with +// volumeByID, which uses Volumes to retrieve all volumes from the storage +// driver without filtering, and filters the volumes externally. +func volumeInspect( + t *testing.T, client types.Client, volumeID string) *types.Volume { + log.WithField("volumeID", volumeID).Info("inspecting volume") + reply, err := client.API().VolumeInspect(nil, azure.Name, volumeID, 0) + assert.NoError(t, err) + if err != nil { + t.Error("failed volumeInspect") + t.FailNow() + } + apitests.LogAsJSON(reply, t) + return reply +} + +// Test if volume is attached, its Attachments field should be populated +func volumeInspectAttached( + t *testing.T, client types.Client, volumeID string) *types.Volume { + log.WithField("volumeID", volumeID).Info("inspecting volume") + reply, err := client.API().VolumeInspect( + nil, azure.Name, volumeID, + types.VolAttReq) + assert.NoError(t, err) + if err != nil { + t.Error("failed volumeInspectAttached") + t.FailNow() + } + apitests.LogAsJSON(reply, t) + assert.Len(t, reply.Attachments, 1) + return reply +} + +// Test if volume is attached to specified instance +func volumeInspectAttachedToMyInstance( + t *testing.T, client types.Client, volumeID string) *types.Volume { + log.WithField("volumeID", volumeID).Info("inspecting volume attached to my instance") + reply, err := client.API().VolumeInspect(nil, azure.Name, volumeID, types.VolAttReqForInstance) + assert.NoError(t, err) + if err != nil { + t.Error("failed volumeInspectAttached") + t.FailNow() + } + apitests.LogAsJSON(reply, t) + assert.Len(t, reply.Attachments, 1) + return reply +} + +// Test if volume is attached to my instance with foreign instance in filter +func volumeInspectAttachedToMyInstanceWithForeignInstance( + t *testing.T, client types.Client, volumeID string) *types.Volume { + ctx := context.Background() + iidm := types.InstanceIDMap{"azure": &types.InstanceID{ID: "none", Driver: "azure"}} + ctx = ctx.WithValue(context.AllInstanceIDsKey, iidm) + log.WithField("volumeID", volumeID).Info( + "inspecting volume attached to my instance with foreign instance in filter") + reply, err := client.API().VolumeInspect( + ctx, azure.Name, volumeID, + types.VolAttReqForInstance) + assert.NoError(t, err) + if err != nil { + t.Error("failed volumeInspectAttached") + t.FailNow() + } + apitests.LogAsJSON(reply, t) + assert.Len(t, reply.Attachments, 0) + return reply +} + +// Test if volume is detached, its Attachments field should not be populated +func volumeInspectDetached( + t *testing.T, client types.Client, volumeID string) *types.Volume { + log.WithField("volumeID", volumeID).Info("inspecting volume") + reply, err := client.API().VolumeInspect( + nil, azure.Name, volumeID, + types.VolAttNone) + assert.NoError(t, err) + + if err != nil { + t.Error("failed volumeInspectDetached") + t.FailNow() + } + apitests.LogAsJSON(reply, t) + assert.Len(t, reply.Attachments, 0) + apitests.LogAsJSON(reply, t) + return reply +} + +// Test detaching volume by volume ID +func volumeDetach( + t *testing.T, client types.Client, volumeID string) *types.Volume { + log.WithField("volumeID", volumeID).Info("detaching volume") + reply, err := client.API().VolumeDetach( + nil, azure.Name, volumeID, &types.VolumeDetachRequest{}) + assert.NoError(t, err) + if err != nil { + t.Error("failed volumeDetach") + t.FailNow() + } + apitests.LogAsJSON(reply, t) + assert.Len(t, reply.Attachments, 0) + return reply +} diff --git a/drivers/storage/azure/utils/utils.go b/drivers/storage/azure/utils/utils.go new file mode 100644 index 00000000..23404a67 --- /dev/null +++ b/drivers/storage/azure/utils/utils.go @@ -0,0 +1,81 @@ +// +build !libstorage_storage_driver libstorage_storage_driver_azure + +package utils + +import ( + "bufio" + "bytes" + "os" + + "github.com/akutz/goof" + + "github.com/codedellemc/libstorage/api/types" + "github.com/codedellemc/libstorage/drivers/storage/azure" +) + +func checkAzureMarkInFile(ctx types.Context) bool { + file := "/var/lib/dhcp/dhclient.eth0.leases" + pattern := []byte("unknown-245") + + f, err := os.Open(file) + if err != nil { + ctx.Debug("Specific file (" + file + ") could not be opened:") + ctx.Debug(err) + return false + } + defer f.Close() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if bytes.Contains(scanner.Bytes(), pattern) { + return true + } + } + if err := scanner.Err(); err != nil { + ctx.Debugf("Specific file %s could not be read: %s", file, err) + } + return false +} + +// IsAzureInstance returns a flag indicating whether the executing host +// is an Azure instance . +func IsAzureInstance(ctx types.Context) (bool, error) { + // http://blog.mszcool.com/index.php/2015/04/ + // detecting-if-a-virtual-machine-runs-in-microsoft-azure-linux- + // windows-to-protect-your-software-when-distributed-via-the- + // azure-marketplace/ + if id := os.Getenv("AZURE_INSTANCE_ID"); id != "" { + return true, nil + } + result := checkAzureMarkInFile(ctx) + return result, nil +} + +// InstanceID returns the instance ID for the local host. +func InstanceID(ctx types.Context) (*types.InstanceID, error) { + hostname := os.Getenv("AZURE_INSTANCE_ID") + if hostname == "" { + isAzure, err := IsAzureInstance(ctx) + if err != nil { + return nil, err + } + if !isAzure { + return nil, goof.New("Executing outside of Instance.") + } + + // UUID can be obtained as descried in + // https://azure.microsoft.com/en-us/blog/accessing-and-using- + // azure-vm-unique-id/ + // but this code will use hostname as ID + + hostname, err = os.Hostname() + if err != nil { + return nil, err + } + } else { + ctx.Info("Use InstanceID from env " + hostname) + } + return &types.InstanceID{ + ID: hostname, + Driver: azure.Name, + }, nil +} diff --git a/drivers/storage/azure/utils/utils_test.go b/drivers/storage/azure/utils/utils_test.go new file mode 100644 index 00000000..7f673f5b --- /dev/null +++ b/drivers/storage/azure/utils/utils_test.go @@ -0,0 +1,28 @@ +// +build !libstorage_storage_driver libstorage_storage_driver_azure + +package utils + +import ( + "os" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/codedellemc/libstorage/api/context" +) + +func skipTest(t *testing.T) { + if ok, _ := strconv.ParseBool(os.Getenv("AZURE_UTILS_TEST")); !ok { + t.Skip() + } +} + +func TestInstanceID(t *testing.T) { + skipTest(t) + iid, err := InstanceID(context.Background()) + if !assert.NoError(t, err) { + t.FailNow() + } + t.Logf("instanceID=%s", iid.String()) +} diff --git a/drivers/storage/azure/utils/utils_unix.go b/drivers/storage/azure/utils/utils_unix.go new file mode 100644 index 00000000..b96d1b0a --- /dev/null +++ b/drivers/storage/azure/utils/utils_unix.go @@ -0,0 +1,19 @@ +// +build !windows +// +build !libstorage_storage_driver libstorage_storage_driver_azure + +package utils + +import ( + "github.com/codedellemc/libstorage/api/types" +) + +// NextDeviceInfo is the NextDeviceInfo object for Azure. +// +// On Azure Linux instance /dev/sda is the boot volume, +// /dev/sdb is a temporary disk. +// Other letters can be used for data volumes. +var NextDeviceInfo = &types.NextDeviceInfo{ + Prefix: "sd", + Pattern: "[c-z]", + Ignore: false, +} diff --git a/glide.lock b/glide.lock index 4671509c..7dd69bc6 100644 --- a/glide.lock +++ b/glide.lock @@ -1,8 +1,8 @@ -hash: ba391adbccda43b43812c633d8423a72e769887a4561c0f250288b4c5fa6b8ac -updated: 2017-02-14T21:13:06.854781561-08:00 +hash: 0db888931b861820351b5b3ec5b8032d46c82cda970e0b17d5bc57707f521a41 +updated: 2017-02-16T09:04:50.463836449-07:00 imports: - name: cloud.google.com/go - version: e4de3dc4493f142c5833f3185e1182025a61f805 + version: b4ca3d4ba32e251f6fee7bda65c5727ccbf3faa9 subpackages: - compute/metadata - internal @@ -23,7 +23,7 @@ imports: - vboxwebsrv - virtualboxclient - name: github.com/asaskevich/govalidator - version: fdf19785fd3558d619ef81212f5edf1d6c2a5911 + version: 7b3beb6df3c42abd3509abfc3bcacc0fbfb7c877 - name: github.com/aws/aws-sdk-go version: 3f8f870ec9939e32b3372abf74d24e468bcd285d repo: https://github.com/aws/aws-sdk-go @@ -59,6 +59,19 @@ imports: - service/efs - service/s3 - service/sts +- name: github.com/Azure/azure-sdk-for-go + version: 0984e0641ae43b89283223034574d6465be93bf4 + subpackages: + - arm/compute + - storage +- name: github.com/Azure/go-autorest + version: 8a25372bbfec739b8719a9e3987400d15ef9e179 + subpackages: + - autorest + - autorest/azure + - autorest/date + - autorest/to + - autorest/validation - name: github.com/cesanta/ucl version: 97c016fce90e6af1b14558563ac46852167e6a76 - name: github.com/cesanta/validate-json @@ -83,15 +96,17 @@ imports: subpackages: - logrus - name: github.com/davecgh/go-spew - version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9 + version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 subpackages: - spew +- name: github.com/dgrijalva/jwt-go + version: 9ed569b5d1ac936e6494082958d63a6aa4fff99a - name: github.com/digitalocean/godo version: 2ff8a02a86cd6918b384a5000ceebe886844fbce - name: github.com/fsnotify/fsnotify - version: a904159b9206978bb6d53fcc7a769e5cd726c737 + version: fd9ec7deca8bf46ecd2a795baaacf2b3a9be1197 - name: github.com/go-ini/ini - version: ee900ca565931451fe4e4409bcbd4316331cec1c + version: 6e4869b434bd001f6983749881c7ead3545887d8 - name: github.com/golang/protobuf version: 8ee79997227bf9b34611aee7946ae64735e6fd93 subpackages: @@ -105,9 +120,9 @@ imports: - name: github.com/gorilla/context version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 - name: github.com/gorilla/mux - version: 392c28fe23e1c45ddba891b0320b3b5df220beea + version: 757bef944d0f21880861c2dd9c871ca543023cba - name: github.com/hashicorp/hcl - version: 372e8ddaa16fd67e371e9323807d056b799360af + version: f74cf8281543a0797d7b4ab7d88e76e7ba125308 subpackages: - hcl/ast - hcl/parser @@ -126,14 +141,20 @@ imports: version: 9b883c5eb462dd5cb1b0a7a104fe86bc6b9bd391 repo: https://github.com/kardianos/osext.git vcs: git +- name: github.com/kr/fs + version: 2788f0dbd16903de03cb8186e5c7d97b69ad387b - name: github.com/magiconair/properties - version: b3b15ef068fd0b17ddf408a23669f20811d194d2 + version: 0723e352fa358f9322c938cc2dadda874e9151a9 - name: github.com/mitchellh/mapstructure - version: db1efb556f84b25a0a13a04aad883943538ad2e0 + version: f3009df150dadf309fdee4a54ed65c124afad715 - name: github.com/pelletier/go-buffruneio version: df1e16fde7fc330a0ca68167c23bf7ed6ac31d6d - name: github.com/pelletier/go-toml - version: c9506ee96398e7571356462217b9e24d6a628d71 + version: 45932ad32dfdd20826f5671da37a5f3ce9f26a8d +- name: github.com/pkg/errors + version: 248dadf4e9068a0b3e79f02ed0a610d935de5302 +- name: github.com/pkg/sftp + version: 4d0e916071f68db74f8a73926335f809396d6b42 - name: github.com/pmezard/go-difflib version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: @@ -153,27 +174,44 @@ imports: - pagination - testhelper - testhelper/client +- name: github.com/rubiojr/go-vhd + version: 96a0db67ea8209453cfa694bdf03de202d6dd8f8 + repo: https://github.com/codenrhoden/go-vhd + subpackages: + - vhd - name: github.com/Sirupsen/logrus version: 5f376aa629ac60c3215cc368e674bd996093a01a repo: https://github.com/akutz/logrus - name: github.com/spf13/afero - version: 72b31426848c6ef12a7a8e216708cb0d1530f074 + version: 52e4a6cfac46163658bd4f123c49b6ee7dc75f78 subpackages: - mem + - sftp - name: github.com/spf13/cast - version: d1139bab1c07d5ad390a65e7305876b3c1a8370b + version: 2580bc98dc0e62908119e4737030cc2fdfc45e4c - name: github.com/spf13/jwalterweatherman - version: fa7ca7e836cf3a8bb4ebf799f472c12d7e903d66 + version: 33c24e77fb80341fe7130ee7c594256ff08ccc46 - name: github.com/spf13/pflag version: 5ccb023bc27df288a957c5e994cd44fd19619465 - name: github.com/spf13/viper version: 651d9d916abc3c3d6a91a12549495caba5edffd2 - name: github.com/stretchr/testify - version: 4d4bfba8f1d1027c4fdbe371823030df51419987 + version: 976c720a22c8eb4eb6a0b4348ad85ad12491a506 subpackages: - assert - name: github.com/tent/http-link-go version: ac974c61c2f990f4115b119354b5e0b47550e888 +- name: golang.org/x/crypto + version: f671756e047d6bc429798536b39e1bbd761e5ce5 + repo: https://github.com/golang/crypto.git + vcs: git + subpackages: + - curve25519 + - ed25519 + - ed25519/internal/edwards25519 + - pkcs12 + - pkcs12/internal/rc2 + - ssh - name: golang.org/x/net version: b336a971b799939dd16ae9b1df8334cb8b977c4d subpackages: @@ -197,8 +235,11 @@ imports: subpackages: - unix - name: golang.org/x/text - version: 06d6eba81293389cafdff7fca90d75592194b2d9 + version: dafb3384ad25363d928a9e97ce4ad3a2f0667e34 + repo: https://github.com/golang/text.git + vcs: git subpackages: + - . - transform - unicode/norm - name: google.golang.org/api @@ -210,7 +251,7 @@ imports: - googleapi - googleapi/internal/uritemplates - name: google.golang.org/appengine - version: 2e4a801b39fc199db615bfca7d0b9f8cd9580599 + version: 8758a385849434ba5eac8aeedcf5192c5a0f5f10 subpackages: - internal - internal/app_identity @@ -222,7 +263,7 @@ imports: - internal/urlfetch - urlfetch - name: google.golang.org/grpc - version: cbcceb2942a489498cf22b2f918536e819d33f0a + version: 50955793b0183f9de69bd78e2ec251cf20aab121 subpackages: - codes - credentials diff --git a/glide.yaml b/glide.yaml index 0acdb931..a25b603a 100644 --- a/glide.yaml +++ b/glide.yaml @@ -83,6 +83,15 @@ import: - package: github.com/digitalocean/godo version: 2ff8a02a86cd6918b384a5000ceebe886844fbce +### Azure + - package: github.com/Azure/azure-sdk-for-go + version: v7.0.1-beta + - package: github.com/Azure/go-autorest + version: v7.2.2 + - package: github.com/rubiojr/go-vhd + ref: 96a0db67ea8209453cfa694bdf03de202d6dd8f8 + repo: https://github.com/codenrhoden/go-vhd + ################################################################################ ## Build System Tools ## ################################################################################ diff --git a/imports/executors/imports_executor.go b/imports/executors/imports_executor.go index 34deffd2..a4b0f290 100644 --- a/imports/executors/imports_executor.go +++ b/imports/executors/imports_executor.go @@ -4,6 +4,7 @@ package executors import ( // load the storage executors + _ "github.com/codedellemc/libstorage/drivers/storage/azure/executor" _ "github.com/codedellemc/libstorage/drivers/storage/digitalocean/executor" _ "github.com/codedellemc/libstorage/drivers/storage/ebs/executor" _ "github.com/codedellemc/libstorage/drivers/storage/efs/executor" diff --git a/imports/executors/imports_executor_azure.go b/imports/executors/imports_executor_azure.go new file mode 100644 index 00000000..d1533f62 --- /dev/null +++ b/imports/executors/imports_executor_azure.go @@ -0,0 +1,8 @@ +// +build libstorage_storage_executor,libstorage_storage_executor_azure + +package executors + +import ( + // load the packages + _ "github.com/codedellemc/libstorage/drivers/storage/azure/executor" +) diff --git a/imports/remote/imports_remote.go b/imports/remote/imports_remote.go index 19e6436c..13ee6d4f 100644 --- a/imports/remote/imports_remote.go +++ b/imports/remote/imports_remote.go @@ -4,6 +4,7 @@ package remote import ( // import to load + _ "github.com/codedellemc/libstorage/drivers/storage/azure/storage" _ "github.com/codedellemc/libstorage/drivers/storage/digitalocean/storage" _ "github.com/codedellemc/libstorage/drivers/storage/ebs/storage" _ "github.com/codedellemc/libstorage/drivers/storage/efs/storage" diff --git a/imports/remote/imports_remote_azure.go b/imports/remote/imports_remote_azure.go new file mode 100644 index 00000000..0c3e4f47 --- /dev/null +++ b/imports/remote/imports_remote_azure.go @@ -0,0 +1,8 @@ +// +build libstorage_storage_driver,libstorage_storage_driver_azure + +package remote + +import ( + // load the packages + _ "github.com/codedellemc/libstorage/drivers/storage/azure/storage" +) From a33728710a80a1894913de1f5106a032f514a2b2 Mon Sep 17 00:00:00 2001 From: Travis Rhoden Date: Thu, 9 Feb 2017 10:36:30 -0700 Subject: [PATCH 2/2] Azure driver renaming and enhancements This patchset renames the driver to `azureud`, to signify Azure Unmanaged Disk driver. This set of enhancements builds off of previous work, and completes the driver for inclusion into libStorage. Use metadata server to detect azure instance Don't return '.vhd' as part of volume name Don't return OS disks Proper attachment logic, remove instance filtering improve login/init logic Combine toTypeVolume and toTypesVolume No need for separate functions here. This is in preparation for a call to get VM details when getting attached device info. When we receive a list of several volumes, and attachments.Devices() is true, we don't want to have to query the VM status for every volume. By combining these functions we can ensure that we only make one additional API call, instead of making 1 call for every volume. make executor localDevices return LUN number The only way we have to match up an attached disk on a VM is via its LUN number. When we query a disk, we get the name of the VM its attached to, and that's it. When we query the VM, we get a list of data disks attached to it, but not it's device path, only its LUN. Therefore, in order to match up a volume with a device, we have to match on the LUN. The easiest way to get the LUN is with the `lsscsi` utility. This introduces a new, external dependency. It's technically possible to parse through /sys/bus/scsi/devices/*/block/sd* to get this without an external utility, but that is more effor than I am willing to put in at this point. This is the last hurdle needed to match up a volume with a device when attachments.Devices() is requested. Return attachment device info when requested Remove IID restriction from VolumeCreate VolumeCreate was requiring an IID, which is not necessary. --- Makefile | 7 + drivers/storage/azure/tests/README.md | 17 - drivers/storage/azure/utils/utils.go | 81 ---- .../{azure/azure.go => azureud/azureud.go} | 26 +- .../executor/azure_executor.go | 75 ++- .../storage/azure_storage.go | 454 +++++++++--------- drivers/storage/azureud/tests/README.md | 16 + .../{azure => azureud}/tests/azure_test.go | 161 +++---- drivers/storage/azureud/tests/coverage.mk | 2 + drivers/storage/azureud/utils/utils.go | 56 +++ drivers/storage/azureud/utils/utils_go17.go | 22 + .../storage/azureud/utils/utils_pre_go17.go | 23 + .../{azure => azureud}/utils/utils_test.go | 4 +- .../{azure => azureud}/utils/utils_unix.go | 2 +- imports/executors/imports_executor.go | 2 +- ...r_azure.go => imports_executor_azureud.go} | 4 +- imports/remote/imports_remote.go | 2 +- ...ote_azure.go => imports_remote_azureud.go} | 4 +- 18 files changed, 496 insertions(+), 462 deletions(-) delete mode 100644 drivers/storage/azure/tests/README.md delete mode 100644 drivers/storage/azure/utils/utils.go rename drivers/storage/{azure/azure.go => azureud/azureud.go} (88%) rename drivers/storage/{azure => azureud}/executor/azure_executor.go (65%) rename drivers/storage/{azure => azureud}/storage/azure_storage.go (72%) create mode 100644 drivers/storage/azureud/tests/README.md rename drivers/storage/{azure => azureud}/tests/azure_test.go (72%) create mode 100644 drivers/storage/azureud/tests/coverage.mk create mode 100644 drivers/storage/azureud/utils/utils.go create mode 100644 drivers/storage/azureud/utils/utils_go17.go create mode 100644 drivers/storage/azureud/utils/utils_pre_go17.go rename drivers/storage/{azure => azureud}/utils/utils_test.go (84%) rename drivers/storage/{azure => azureud}/utils/utils_unix.go (98%) rename imports/executors/{imports_executor_azure.go => imports_executor_azureud.go} (58%) rename imports/remote/{imports_remote_azure.go => imports_remote_azureud.go} (59%) diff --git a/Makefile b/Makefile index 49414377..be31dba9 100644 --- a/Makefile +++ b/Makefile @@ -1099,6 +1099,13 @@ test-digitalocean: test-digitalocean-clean: DRIVERS=digitalocean $(MAKE) clean +test-azureud: + DRIVERS=azureud $(MAKE) deps + DRIVERS=azureud $(MAKE) ./drivers/storage/azureud/tests/azureud.test + +test-azureud-clean: + DRIVERS=azureud $(MAKE) clean + clean: $(GO_CLEAN) clobber: clean $(GO_CLOBBER) diff --git a/drivers/storage/azure/tests/README.md b/drivers/storage/azure/tests/README.md deleted file mode 100644 index 60517f0e..00000000 --- a/drivers/storage/azure/tests/README.md +++ /dev/null @@ -1,17 +0,0 @@ -Functional tests for Azure driver. - -It requires to be run either inside of Azure instance or instance ID should be defined via environment variable 'AZURE_INSTANCE_ID' that points to existing running Azure instance. - -In order to run test the following environment variables should be defined (they should be filled with your data): - AZURE_INSTANCE_ID= # it is required if run test outside Azure instance - AZURE_SUBSCRIPTION_ID= # your subscription ID - AZURE_RESOURCE_GROUP= # your resource group name - AZURE_TENANT_ID= # your tenant ID - AZURE_CLIENT_ID= # id of your client (application) - AZURE_CLIENT_SECRET= # your client(application) secret key - AZURE_CONTAINER= # name of container for disk blob objects, e.g. 'vhds' - AZURE_STORAGE_ACCOUNT= # your storage account name - AZURE_STORAGE_ACCESS_KEY= # your storage account access key - -The driver and tests do not create container, instance, etc, all entities should be created before to run tests / use of libstorage. - diff --git a/drivers/storage/azure/utils/utils.go b/drivers/storage/azure/utils/utils.go deleted file mode 100644 index 23404a67..00000000 --- a/drivers/storage/azure/utils/utils.go +++ /dev/null @@ -1,81 +0,0 @@ -// +build !libstorage_storage_driver libstorage_storage_driver_azure - -package utils - -import ( - "bufio" - "bytes" - "os" - - "github.com/akutz/goof" - - "github.com/codedellemc/libstorage/api/types" - "github.com/codedellemc/libstorage/drivers/storage/azure" -) - -func checkAzureMarkInFile(ctx types.Context) bool { - file := "/var/lib/dhcp/dhclient.eth0.leases" - pattern := []byte("unknown-245") - - f, err := os.Open(file) - if err != nil { - ctx.Debug("Specific file (" + file + ") could not be opened:") - ctx.Debug(err) - return false - } - defer f.Close() - scanner := bufio.NewScanner(f) - for scanner.Scan() { - if bytes.Contains(scanner.Bytes(), pattern) { - return true - } - } - if err := scanner.Err(); err != nil { - ctx.Debugf("Specific file %s could not be read: %s", file, err) - } - return false -} - -// IsAzureInstance returns a flag indicating whether the executing host -// is an Azure instance . -func IsAzureInstance(ctx types.Context) (bool, error) { - // http://blog.mszcool.com/index.php/2015/04/ - // detecting-if-a-virtual-machine-runs-in-microsoft-azure-linux- - // windows-to-protect-your-software-when-distributed-via-the- - // azure-marketplace/ - if id := os.Getenv("AZURE_INSTANCE_ID"); id != "" { - return true, nil - } - result := checkAzureMarkInFile(ctx) - return result, nil -} - -// InstanceID returns the instance ID for the local host. -func InstanceID(ctx types.Context) (*types.InstanceID, error) { - hostname := os.Getenv("AZURE_INSTANCE_ID") - if hostname == "" { - isAzure, err := IsAzureInstance(ctx) - if err != nil { - return nil, err - } - if !isAzure { - return nil, goof.New("Executing outside of Instance.") - } - - // UUID can be obtained as descried in - // https://azure.microsoft.com/en-us/blog/accessing-and-using- - // azure-vm-unique-id/ - // but this code will use hostname as ID - - hostname, err = os.Hostname() - if err != nil { - return nil, err - } - } else { - ctx.Info("Use InstanceID from env " + hostname) - } - return &types.InstanceID{ - ID: hostname, - Driver: azure.Name, - }, nil -} diff --git a/drivers/storage/azure/azure.go b/drivers/storage/azureud/azureud.go similarity index 88% rename from drivers/storage/azure/azure.go rename to drivers/storage/azureud/azureud.go index 1c6321ec..93620136 100644 --- a/drivers/storage/azure/azure.go +++ b/drivers/storage/azureud/azureud.go @@ -1,6 +1,6 @@ -// +build !libstorage_storage_driver libstorage_storage_driver_azure +// +build !libstorage_storage_driver libstorage_storage_driver_azureud -package azure +package azureud import ( gofigCore "github.com/akutz/gofig" @@ -9,7 +9,7 @@ import ( const ( // Name is the provider's name. - Name = "azure" + Name = "azureud" // TagDelimiter separates tags from volume or snapshot names TagDelimiter = "/" @@ -18,31 +18,40 @@ const ( // or not for Azure URI's DefaultUseHTTPS = true - // TenantIDKey is a Directory ID from Azure + // TenantIDKey is an Active Directory ID from Azure TenantIDKey = "tenantID" + // ClientIDKey is an Application ID from Azure ClientIDKey = "clientID" + // ClientSecretKey is a secret of the application ClientSecretKey = "clientSecret" + // CertPathKey is a path to application certificate in case of // authorization via certificate CertPathKey = "certPath" // StorageAccountKey is a name of storage account StorageAccountKey = "storageAccount" + // StorageAccessKey is an access key of storage account StorageAccessKey = "storageAccessKey" + // TODO: add option to pass StorageURI // SubscriptionIDKey is an ID of subscription SubscriptionIDKey = "subscriptionID" + // ResourceGroupKey is a name of resource group ResourceGroupKey = "resourceGroup" + // ContainerKey is a name of container in the storage account // ('vhds' by default) ContainerKey = "container" + // UseHTTPSKey is a flag about use https or not for making Azure URI's UseHTTPSKey = "useHTTPS" + // TagKey is a tag key TagKey = "tag" ) @@ -63,8 +72,8 @@ const ( // ConfigAzureStorageAccountKey is a config key ConfigAzureStorageAccountKey = ConfigAzure + "." + StorageAccountKey - // ConfigAzureStorageAccessKeyKey is a config key - ConfigAzureStorageAccessKeyKey = ConfigAzure + "." + StorageAccessKey + // ConfigAzureStorageAccessKey is a config key + ConfigAzureStorageAccessKey = ConfigAzure + "." + StorageAccessKey // ConfigAzureContainerKey is a config key ConfigAzureContainerKey = ConfigAzure + "." + ContainerKey @@ -86,12 +95,13 @@ const ( ) func init() { - r := gofigCore.NewRegistration("Azure") + r := gofigCore.NewRegistration("AzureUnmanagedDisk") r.Key(gofig.String, "", "", "", ConfigAzureSubscriptionIDKey) r.Key(gofig.String, "", "", "", ConfigAzureResourceGroupKey) r.Key(gofig.String, "", "", "", ConfigAzureTenantIDKey) r.Key(gofig.String, "", "", "", ConfigAzureStorageAccountKey) - r.Key(gofig.String, "", "", "", ConfigAzureContainerKey) + r.Key(gofig.String, "", "", "", ConfigAzureStorageAccessKey) + r.Key(gofig.String, "", "vhds", "", ConfigAzureContainerKey) r.Key(gofig.String, "", "", "", ConfigAzureClientIDKey) r.Key(gofig.String, "", "", "", ConfigAzureClientSecretKey) r.Key(gofig.String, "", "", "", ConfigAzureCertPathKey) diff --git a/drivers/storage/azure/executor/azure_executor.go b/drivers/storage/azureud/executor/azure_executor.go similarity index 65% rename from drivers/storage/azure/executor/azure_executor.go rename to drivers/storage/azureud/executor/azure_executor.go index b73c12b0..3a0029dc 100644 --- a/drivers/storage/azure/executor/azure_executor.go +++ b/drivers/storage/azureud/executor/azure_executor.go @@ -1,31 +1,34 @@ -// +build !libstorage_storage_executor libstorage_storage_executor_azure +// +build !libstorage_storage_executor libstorage_storage_executor_azureud package executor import ( "bufio" + "bytes" "fmt" - "os" - "path" + "os/exec" "regexp" "strings" + log "github.com/Sirupsen/logrus" + gofig "github.com/akutz/gofig/types" "github.com/akutz/goof" + "github.com/akutz/gotil" "github.com/codedellemc/libstorage/api/registry" "github.com/codedellemc/libstorage/api/types" - "github.com/codedellemc/libstorage/drivers/storage/azure" - "github.com/codedellemc/libstorage/drivers/storage/azure/utils" + "github.com/codedellemc/libstorage/drivers/storage/azureud" + "github.com/codedellemc/libstorage/drivers/storage/azureud/utils" ) -// driver is the storage executor for the azure storage driver. +// driver is the storage executor for the azureud storage driver. type driver struct { config gofig.Config } func init() { - registry.RegisterStorageExecutor(azure.Name, newDriver) + registry.RegisterStorageExecutor(azureud.Name, newDriver) } func newDriver() types.StorageExecutor { @@ -33,13 +36,13 @@ func newDriver() types.StorageExecutor { } func (d *driver) Init(ctx types.Context, config gofig.Config) error { - ctx.Info("azure_executor: Init") + ctx.Info("azureud_executor: Init") d.config = config return nil } func (d *driver) Name() string { - return azure.Name + return azureud.Name } // Supported returns a flag indicating whether or not the platform @@ -48,6 +51,12 @@ func (d *driver) Name() string { func (d *driver) Supported( ctx types.Context, opts types.Store) (bool, error) { + + if !gotil.FileExistsInPath("lsscsi") { + ctx.Error("lsscsi executable not found in PATH") + return false, nil + } + return utils.IsAzureInstance(ctx) } @@ -63,7 +72,7 @@ var nextDevRe = regexp.MustCompile("^/dev/" + utils.NextDeviceInfo.Prefix + "(" + utils.NextDeviceInfo.Pattern + ")") var availLetters = []string{ - "c", "d", "f", "g", "h", "i", "j", "k", "l", "m", "n", + "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"} // NextDevice returns the next available device. @@ -100,36 +109,39 @@ func (d *driver) NextDevice( return "", errNoAvaiDevice } -const procPartitions = "/proc/partitions" - -var devRX = regexp.MustCompile(`^sd[a-z]$`) +var ( + devRX = regexp.MustCompile(`^/dev/sd[c-z]$`) + scsiRx = regexp.MustCompile(`^\[\d+:\d+:\d+:(\d+)\]$`) +) // Retrieve device paths currently attached and/or mounted func (d *driver) LocalDevices( ctx types.Context, opts *types.LocalDevicesOpts) (*types.LocalDevices, error) { - f, err := os.Open(procPartitions) + // Read all of the attached devices + scsiDevs, err := getSCSIDevs() if err != nil { - return nil, goof.WithError( - "error reading "+procPartitions, err) + return nil, err } - defer f.Close() devMap := map[string]string{} - scanner := bufio.NewScanner(f) + scanner := bufio.NewScanner(bytes.NewReader(scsiDevs)) for scanner.Scan() { fields := strings.Fields(scanner.Text()) - if len(fields) != 4 { + device := fields[len(fields)-1] + if !devRX.MatchString(device) { continue } - devName := fields[3] - if !devRX.MatchString(devName) { + + matches := scsiRx.FindStringSubmatch(fields[0]) + if matches == nil { continue } - devPath := path.Join("/dev/", devName) - devMap[devPath] = devPath + + lun := matches[1] + devMap[device] = lun } ld := &types.LocalDevices{Driver: d.Name()} @@ -141,3 +153,20 @@ func (d *driver) LocalDevices( return ld, nil } + +func getSCSIDevs() ([]byte, error) { + + out, err := exec.Command("lsscsi").Output() + if err != nil { + if exiterr, ok := err.(*exec.ExitError); ok { + stderr := string(exiterr.Stderr) + log.Errorf("Unable to get scsi devices: %s", stderr) + return nil, + goof.Newf("Unable to get scsi devices: %s", + stderr) + } + return nil, goof.WithError("Unable to get scsci devices", err) + } + + return out, nil +} diff --git a/drivers/storage/azure/storage/azure_storage.go b/drivers/storage/azureud/storage/azure_storage.go similarity index 72% rename from drivers/storage/azure/storage/azure_storage.go rename to drivers/storage/azureud/storage/azure_storage.go index 3a34e729..ea13a394 100644 --- a/drivers/storage/azure/storage/azure_storage.go +++ b/drivers/storage/azureud/storage/azure_storage.go @@ -1,4 +1,4 @@ -// +build !libstorage_storage_driver libstorage_storage_driver_azure +// +build !libstorage_storage_driver libstorage_storage_driver_azureud package storage @@ -11,6 +11,7 @@ import ( "fmt" "hash" "io/ioutil" + "strconv" "strings" "sync" "time" @@ -28,8 +29,8 @@ import ( "github.com/codedellemc/libstorage/api/context" "github.com/codedellemc/libstorage/api/registry" "github.com/codedellemc/libstorage/api/types" - "github.com/codedellemc/libstorage/drivers/storage/azure" - "github.com/codedellemc/libstorage/drivers/storage/azure/utils" + "github.com/codedellemc/libstorage/drivers/storage/azureud" + "github.com/codedellemc/libstorage/drivers/storage/azureud/utils" ) const ( @@ -62,11 +63,11 @@ type driver struct { } func init() { - registry.RegisterStorageDriver(azure.Name, newDriver) + registry.RegisterStorageDriver(azureud.Name, newDriver) } func newDriver() types.StorageDriver { - return &driver{name: azure.Name} + return &driver{name: azureud.Name} } func (d *driver) Name() string { @@ -76,36 +77,47 @@ func (d *driver) Name() string { // Init initializes the driver. func (d *driver) Init(context types.Context, config gofig.Config) error { d.config = config + d.tenantID = d.getTenantID() + if d.tenantID == "" { + return goof.New("tenantID is a required config item") + } + d.clientID = d.getClientID() - if d.tenantID == "" || d.clientID == "" { - context.Error( - "tenantID or clientID are not set. Login will fail.") + if d.clientID == "" { + return goof.New("clientID is a required config item") } + d.clientSecret = d.getClientSecret() d.certPath = d.getCertPath() if d.clientSecret == "" && d.certPath == "" { - context.Error( + return goof.New( "clientSecret or certPath must be set for login.") } + if d.clientSecret != "" && d.certPath != "" { + context.Warn("certPath will be ignored since clientSecret is set") + } d.storageAccount = d.getStorageAccount() + if d.storageAccount == "" { + return goof.New("storageAccount is a required config item") + } + d.storageAccessKey = d.getStorageAccessKey() - if d.storageAccount == "" || d.storageAccessKey == "" { - context.Error( - "storageAccount and storageAccessKey " + - "are needed for correct work.") + if d.storageAccessKey == "" { + return goof.New("storageAccessKey is a required config item") } + d.container = d.getContainer() d.subscriptionID = d.getSubscriptionID() if d.subscriptionID == "" { - context.Error("subscriptionID must be set for correct work.") + return goof.New("subscriptionID is a required config item") } + d.resourceGroup = d.getResourceGroup() if d.resourceGroup == "" { - context.Warning("resourceGroup is not set." + - " Some operations will fail.") + return goof.New("resourceGroup is a required config item") } d.useHTTPS = d.getUseHTTPS() @@ -179,31 +191,10 @@ func (d *driver) Login(ctx types.Context) (interface{}, error) { err error ) - if d.tenantID == "" { - return nil, goof.New("Empty tenantID") - } - writeHkey(hkey, &d.subscriptionID) writeHkey(hkey, &d.tenantID) writeHkey(hkey, &d.storageAccount) - writeHkey(hkey, &d.storageAccessKey) - if d.clientID != "" && d.clientSecret != "" { - ctx.Debug("login to azure using clientID and clientSecret") - writeHkey(hkey, &d.clientID) - writeHkey(hkey, &d.clientSecret) - } else if d.certPath != "" { - ctx.Debug("login to azure using clientCert") - certData, err = ioutil.ReadFile(d.certPath) - if err != nil { - return nil, goof.WithError( - "Failed to read provided certificate file", - err) - } - writeHkeyB(hkey, certData) - } else { - ctx.Error("No login information provided") - return nil, errAuthFailed - } + writeHkey(hkey, &d.clientID) ckey = fmt.Sprintf("%x", hkey.Sum(nil)) if session, ok := sessions[ckey]; ok { @@ -212,6 +203,18 @@ func (d *driver) Login(ctx types.Context) (interface{}, error) { return session, nil } + if d.clientSecret != "" { + ctx.Info("Authenticating via clientSecret") + } else { + ctx.Info("Authenticating via client certificate") + certData, err = ioutil.ReadFile(d.certPath) + if err != nil { + return nil, goof.WithError( + "Failed to read provided certificate file", + err) + } + } + oauthConfig, err := autorest.PublicCloud.OAuthConfigForTenant( d.tenantID) if err != nil { @@ -219,7 +222,7 @@ func (d *driver) Login(ctx types.Context) (interface{}, error) { "Failed to create OAuthConfig for tenant", err) } - if d.clientID != "" && d.clientSecret != "" { + if d.clientSecret != "" { spt, err = autorest.NewServicePrincipalToken( *oauthConfig, d.clientID, d.clientSecret, autorest.PublicCloud.ResourceManagerEndpoint) @@ -249,6 +252,14 @@ func (d *driver) Login(ctx types.Context) (interface{}, error) { newVMC := armCompute.NewVirtualMachinesClient(d.subscriptionID) newVMC.Authorizer = spt newVMC.PollingDelay = 5 * time.Second + + // Verify login is working by listing VMs + _, err = newVMC.List(d.resourceGroup) + if err != nil { + return nil, goof.WithError("Login does not appear functional", + err) + } + bc, err := blobStorage.NewBasicClient( d.storageAccount, d.storageAccessKey) @@ -264,7 +275,7 @@ func (d *driver) Login(ctx types.Context) (interface{}, error) { sessions[ckey] = &session ctx.WithField(cacheKeyC, ckey).Info( - "login to azure storage driver created and cached") + "login to azureud storage driver created and cached") return &session, nil } @@ -289,9 +300,7 @@ func (d *driver) InstanceInspect( iid := context.MustInstanceID(ctx) return &types.Instance{ - Name: iid.ID, - InstanceID: iid, - ProviderName: iid.Driver, + InstanceID: iid, }, nil } @@ -306,7 +315,7 @@ func (d *driver) Volumes( return nil, goof.WithError("error listing blobs", err) } // Convert retrieved volumes to libStorage types.Volume - vols, convErr := d.toTypesVolume(ctx, &list.Blobs, opts.Attachments) + vols, convErr := d.toTypesVolume(ctx, list.Blobs, opts.Attachments) if convErr != nil { return nil, goof.WithError( "error converting to types.Volume", convErr) @@ -331,16 +340,8 @@ func (d *driver) VolumeCreate(ctx types.Context, volumeName string, return nil, types.ErrNotImplemented } - id, ok := context.InstanceID(ctx) - if !ok || id == nil { - return nil, goof.New( - "Can't create volume outside of Azure instance") - } - vmName := id.ID - if !strings.HasSuffix(volumeName, vhdExtension) { - ctx.Warning("Disk name doesn't end with '.vhd'." + - " This extension will be added automatically.") + ctx.Debugf("Auto-adding %s extension", vhdExtension) volumeName = volumeName + vhdExtension } @@ -351,34 +352,18 @@ func (d *driver) VolumeCreate(ctx types.Context, volumeName string, size *= size1GB fields := map[string]interface{}{ - "provider": d.Name(), - "vmName": vmName, "volumeName": volumeName, "size_in_bytes": size, } - _, err := d.getVM(ctx, vmName) - if err != nil { - return nil, goof.WithFieldsE(fields, - "VM could not be obtained.", err) - } - blobClient := mustSession(ctx).blobClient - err = d.createDiskBlob(volumeName, size, blobClient) + err := d.createDiskBlob(volumeName, size, blobClient) if err != nil { return nil, goof.WithFieldsE(fields, "failed to create volume for VM", err) } - var volume *types.Volume - volume, err = d.getVolume(ctx, volumeName, types.VolAttNone) - if err != nil { - d.deleteDiskBlob(volumeName, blobClient) - return nil, goof.WithFieldsE(fields, - "failed to get just created/attached volume", err) - } - - return volume, nil + return d.getVolume(ctx, volumeName, types.VolAttNone) } // VolumeCreateFromSnapshot creates a new volume from an existing snapshot. @@ -420,7 +405,6 @@ func (d *driver) VolumeRemove( d.container, volumeID, nil) if err != nil { fields := map[string]interface{}{ - "provider": d.Name(), "volumeID": volumeID, } return goof.WithFieldsE(fields, "error removing volume", err) @@ -440,15 +424,13 @@ func (d *driver) VolumeAttach( volumeID string, opts *types.VolumeAttachOpts) (*types.Volume, string, error) { - id, ok := context.InstanceID(ctx) - if !ok || id == nil { - return nil, "", goof.New( - "Can't attach volume outside of Azure instance") + vmName := context.MustInstanceID(ctx).ID + + if opts.NextDevice == nil || *opts.NextDevice == "" { + return nil, "", errMissingNextDevice } - vmName := id.ID fields := map[string]interface{}{ - "provider": d.Name(), "vmName": vmName, "volumeID": volumeID, "nextDevice": *opts.NextDevice, @@ -467,33 +449,25 @@ func (d *driver) VolumeAttach( if !opts.Force { return nil, "", errVolAlreadyAttached } - _, err = d.VolumeDetach( - ctx, - volumeID, - &types.VolumeDetachOpts{ - Force: opts.Force, - Opts: opts.Opts, - }) - if err != nil { - return nil, "", goof.WithFieldsE(fields, - "failed to detach volume first", err) + for _, att := range volume.Attachments { + err = d.detachDisk(ctx, &volumeID, &att.InstanceID.ID) + if err != nil { + return nil, "", goof.WithError( + "failed to detach volume first", err) + } } } - if opts.NextDevice == nil { - return nil, "", errMissingNextDevice - } - vm, err := d.getVM(ctx, vmName) if err != nil { return nil, "", goof.WithFieldsE(fields, - "VM could not be obtained.", err) + "VM could not be obtained", err) } - err = d.attachDisk(ctx, volumeID, volume.Size*size1GB, vm) + err = d.attachDisk(ctx, volumeID, volume.Size, vm) if err != nil { return nil, "", goof.WithFieldsE(fields, - "failed to attach volume.", err) + "failed to attach volume", err) } volume, err = d.getVolume(ctx, volumeID, @@ -502,10 +476,6 @@ func (d *driver) VolumeAttach( return nil, "", goof.WithFieldsE(fields, "failed to get just created/attached volume", err) } - if len(volume.Attachments) == 0 { - return nil, "", goof.WithFieldsE(fields, - "volume is not attached to VM", err) - } return volume, *opts.NextDevice, nil } @@ -518,15 +488,9 @@ func (d *driver) VolumeDetach( volumeID string, opts *types.VolumeDetachOpts) (*types.Volume, error) { - id, ok := context.InstanceID(ctx) - if !ok || id == nil { - return nil, goof.New( - "Can't detach volume outside of Azure instance") - } - vmName := id.ID + vmName := context.MustInstanceID(ctx).ID fields := map[string]interface{}{ - "provider": d.Name(), "vmName": vmName, "volumeID": volumeID, } @@ -541,35 +505,9 @@ func (d *driver) VolumeDetach( return nil, errVolAlreadyDetached } - vm, err := d.getVM(ctx, vmName) + err = d.detachDisk(ctx, &volumeID, &vmName) if err != nil { - return nil, goof.WithFieldsE(fields, - "failed to detach volume to VM", err) - } - - disks := *vm.StorageProfile.DataDisks - for i, disk := range disks { - // Disk is paged blob in Azure. So blob name is case sensitive. - if disk.Name != nil && *disk.Name == volumeID { - // found the disk - disks = append(disks[:i], disks[i+1:]...) - break - } - } - newVM := armCompute.VirtualMachine{ - Location: vm.Location, - VirtualMachineProperties: &armCompute.VirtualMachineProperties{ - StorageProfile: &armCompute.StorageProfile{ - DataDisks: &disks, - }, - }, - } - - _, err = mustSession(ctx).vmClient.CreateOrUpdate(d.resourceGroup, - vmName, newVM, nil) - if err != nil { - return nil, goof.WithFieldsE(fields, - "failed to detach volume", err) + return nil, err } volume, err = d.getVolume(ctx, volumeID, @@ -578,10 +516,6 @@ func (d *driver) VolumeDetach( return nil, goof.WithFieldsE(fields, "failed to get volume", err) } - if len(volume.Attachments) != 0 { - return nil, goof.WithFieldsE(fields, - "volume is not detached", err) - } return volume, nil } @@ -622,64 +556,60 @@ func (d *driver) SnapshotRemove( // Get volume or snapshot name without config tag func (d *driver) getPrintableName(name string) string { - return strings.TrimPrefix(name, d.tag()+azure.TagDelimiter) + return strings.TrimPrefix(name, d.tag()+azureud.TagDelimiter) } // Prefix volume or snapshot name with config tag func (d *driver) getFullName(name string) string { if d.tag() != "" { - return d.tag() + azure.TagDelimiter + name + return d.tag() + azureud.TagDelimiter + name } return name } // Retrieve config arguments func (d *driver) getSubscriptionID() string { - return d.config.GetString(azure.ConfigAzureSubscriptionIDKey) + return d.config.GetString(azureud.ConfigAzureSubscriptionIDKey) } func (d *driver) getResourceGroup() string { - return d.config.GetString(azure.ConfigAzureResourceGroupKey) + return d.config.GetString(azureud.ConfigAzureResourceGroupKey) } func (d *driver) getTenantID() string { - return d.config.GetString(azure.ConfigAzureTenantIDKey) + return d.config.GetString(azureud.ConfigAzureTenantIDKey) } func (d *driver) getStorageAccount() string { - return d.config.GetString(azure.ConfigAzureStorageAccountKey) + return d.config.GetString(azureud.ConfigAzureStorageAccountKey) } func (d *driver) getStorageAccessKey() string { - return d.config.GetString(azure.ConfigAzureStorageAccessKeyKey) + return d.config.GetString(azureud.ConfigAzureStorageAccessKey) } func (d *driver) getContainer() string { - result := d.config.GetString(azure.ConfigAzureContainerKey) - if result == "" { - result = "vhds" - } - return result + return d.config.GetString(azureud.ConfigAzureContainerKey) } func (d *driver) getClientID() string { - return d.config.GetString(azure.ConfigAzureClientIDKey) + return d.config.GetString(azureud.ConfigAzureClientIDKey) } func (d *driver) getClientSecret() string { - return d.config.GetString(azure.ConfigAzureClientSecretKey) + return d.config.GetString(azureud.ConfigAzureClientSecretKey) } func (d *driver) getCertPath() string { - return d.config.GetString(azure.ConfigAzureCertPathKey) + return d.config.GetString(azureud.ConfigAzureCertPathKey) } func (d *driver) getUseHTTPS() bool { - return d.config.GetBool(azure.ConfigAzureUseHTTPSKey) + return d.config.GetBool(azureud.ConfigAzureUseHTTPSKey) } func (d *driver) tag() string { - return d.config.GetString(azure.ConfigAzureTagKey) + return d.config.GetString(azureud.ConfigAzureTagKey) } // TODO rexrayTag @@ -687,45 +617,38 @@ func (d *driver) tag() string { return d.config.GetString("azure.rexrayTag") }*/ -// var errGetLocDevs = goof.New("error getting local devices from context") +var errGetLocDevs = goof.New("error getting local devices from context") func (d *driver) toTypesVolume( ctx types.Context, - blobs *[]blobStorage.Blob, + blobs []blobStorage.Blob, attachments types.VolumeAttachmentsTypes) ([]*types.Volume, error) { - /* TODO: var ( - ld *types.LocalDevices - ldOK bool + ld *types.LocalDevices + ldOK bool + volumes []*types.Volume + iid *types.InstanceID + vmDisks []armCompute.DataDisk ) if attachments.Devices() { - // Get local devices map from context if ld, ldOK = context.LocalDevices(ctx); !ldOK { return nil, errGetLocDevs } - } - */ - var volumesSD []*types.Volume - for _, blob := range *blobs { - volumeSD, err := d.toTypeVolume(ctx, &blob, attachments) + // We will need to query the VM to get its list of + // attached disks, to match on the LUN number + iid = context.MustInstanceID(ctx) + vm, err := d.getVM(ctx, iid.ID) if err != nil { - ctx.WithError(err).Warning("Failed to convert volume") - } else if volumeSD != nil { - volumesSD = append(volumesSD, volumeSD) + return nil, goof.WithError( + "Unable to lookup devices on VM", err) } + vmDisks = *vm.VirtualMachineProperties.StorageProfile.DataDisks } - return volumesSD, nil -} - -func (d *driver) toTypeVolume( - ctx types.Context, - blob *blobStorage.Blob, - attachments types.VolumeAttachmentsTypes) (*types.Volume, error) { - // Metadata can have these fileds: + // Metadata can have these fields: // microsoftazurecompute_resourcegroupname:trex // microsoftazurecompute_vmname:ttt // microsoftazurecompute_disktype:DataDisk (or OSDisk) @@ -733,66 +656,78 @@ func (d *driver) toTypeVolume( // microsoftazurecompute_diskname:ttt-20161221-130722 // microsoftazurecompute_disksizeingb:50 - btype := blob.Metadata["microsoftazurecompute_disktype"] - if btype == "" && !strings.HasSuffix(blob.Name, vhdExtension) { - return nil, nil - } - attachState := types.VolumeAvailable - bstate := "detached" - if blob.Metadata["microsoftazurecompute_vmname"] != "" { - bstate = "attached" - attachState = types.VolumeAttached - } - var attachmentsSD []*types.VolumeAttachment - if attachments.Requested() && attachState == types.VolumeAttached { - attachedIID := types.InstanceID{ - ID: blob.Metadata["microsoftazurecompute_vmname"], - Driver: d.name, + for _, blob := range blobs { + + btype := blob.Metadata["microsoftazurecompute_disktype"] + if btype == "" && !strings.HasSuffix(blob.Name, vhdExtension) { + continue } - if attachments.Mine() { - id, ok := context.InstanceID(ctx) - if !ok || id == nil { - return nil, goof.New("Can't get isntance" + - " ID to filter volume") - } + if btype == "OSDisk" { + continue + } + + bName := strings.TrimSuffix(blob.Name, vhdExtension) - if id.ID == attachedIID.ID { - attachmentsSD = append( - attachmentsSD, - &types.VolumeAttachment{ - InstanceID: &attachedIID, - VolumeID: blob.Name, - }) + volume := &types.Volume{ + Name: bName, + ID: blob.Name, + Type: btype, + Size: blob.Properties.ContentLength / size1GB, + // TODO: + //AvailabilityZone: *volume.AvailabilityZone, + //Encrypted: *volume.Encrypted, + } + + if attachments.Requested() { + var attachedVols []*types.VolumeAttachment + attVM := blob.Metadata["microsoftazurecompute_vmname"] + if attVM != "" { + att := &types.VolumeAttachment{ + VolumeID: blob.Name, + InstanceID: &types.InstanceID{ + ID: attVM, + Driver: azureud.Name, + }, + } + if attachments.Devices() { + if iid.ID == attVM { + att.DeviceName = getDevice( + ctx, vmDisks, &bName, + ld.DeviceMap, + ) + } + } + attachedVols = append(attachedVols, att) + volume.Attachments = attachedVols } - } else { - attachmentsSD = append( - attachmentsSD, - &types.VolumeAttachment{ - InstanceID: &attachedIID, - VolumeID: blob.Name, - }) } - } - volumeSD := &types.Volume{ - Name: blob.Name, - ID: blob.Name, - Status: bstate, - Type: btype, - Size: blob.Properties.ContentLength / size1GB, - AttachmentState: attachState, - Attachments: attachmentsSD, - // TODO: - //AvailabilityZone: *volume.AvailabilityZone, - //Encrypted: *volume.Encrypted, + volumes = append(volumes, volume) } + return volumes, nil +} - // Some volume types have no IOPS, so we get nil in volume.Iops - //if volume.Iops != nil { - // volumeSD.IOPS = *volume.Iops - //} - - return volumeSD, nil +func getDevice( + ctx types.Context, + vmDisks []armCompute.DataDisk, + bName *string, + devMap map[string]string) string { + + for _, disk := range vmDisks { + name := strings.TrimSuffix(*disk.Name, vhdExtension) + if name == *bName { + strLun := strconv.Itoa(int(*disk.Lun)) + ctx.Debugf("Found matching disk %v on LUN %v on "+ + "instance, looking up dev from %v", + name, strLun, devMap) + for dev, lun := range devMap { + if lun == strLun { + return dev + } + } + } + } + return "" } func (d *driver) diskURI(name string) string { @@ -812,8 +747,7 @@ func (d *driver) getVM(ctx types.Context, name string) ( vm, err := mustSession(ctx).vmClient.Get(d.resourceGroup, name, "") if err != nil { fields := map[string]interface{}{ - "provider": d.Name(), - "vmName": name, + "vmName": name, } return nil, goof.WithFieldsE( fields, "failed to get virtual machine", err) @@ -835,10 +769,17 @@ func (d *driver) getVolume( return nil, goof.WithError("error listing blobs", err) } if len(list.Blobs) == 0 { - return nil, goof.New("error to get volume") + return nil, goof.New("volume not found") + } + if len(list.Blobs) > 1 { + return nil, goof.New("multiple volumes found") } // Convert retrieved volumes to libStorage types.Volume - return d.toTypeVolume(ctx, &list.Blobs[0], attachments) + vols, err := d.toTypesVolume(ctx, list.Blobs, attachments) + if err != nil { + return nil, goof.WithError("failed to convert volume", err) + } + return vols[0], nil } func (d *driver) createDiskBlob( @@ -915,7 +856,7 @@ func (d *driver) attachDisk( uri := d.diskURI(volumeName) disks := *vm.StorageProfile.DataDisks - sizeGB := int32(size / size1GB) + sizeGB := int32(size) disks = append(disks, armCompute.DataDisk{ Name: &volumeName, @@ -952,3 +893,46 @@ func (d *driver) attachDisk( return nil } + +func (d *driver) detachDisk( + ctx types.Context, + volumeID *string, + vmName *string) error { + + vm, err := d.getVM(ctx, *vmName) + if err != nil { + return goof.WithError("VM could not be obtained", err) + } + + found := false + disks := *vm.StorageProfile.DataDisks + for i, disk := range disks { + // Disk is paged blob in Azure. So blob name is case sensitive. + if disk.Name != nil && *disk.Name == *volumeID { + ctx.Debugf("Removing %v from VM", volumeID) + // found the disk + disks = append(disks[:i], disks[i+1:]...) + found = true + break + } + } + if !found { + return goof.New("VolumeID not found on given instance") + } + newVM := armCompute.VirtualMachine{ + Location: vm.Location, + VirtualMachineProperties: &armCompute.VirtualMachineProperties{ + StorageProfile: &armCompute.StorageProfile{ + DataDisks: &disks, + }, + }, + } + + _, err = mustSession(ctx).vmClient.CreateOrUpdate( + d.resourceGroup, *vmName, newVM, nil) + if err != nil { + return goof.WithError("failed to detach volume", err) + } + + return nil +} diff --git a/drivers/storage/azureud/tests/README.md b/drivers/storage/azureud/tests/README.md new file mode 100644 index 00000000..b8c20be4 --- /dev/null +++ b/drivers/storage/azureud/tests/README.md @@ -0,0 +1,16 @@ +Functional tests for Azure Unmanaged Disk driver. + +It requires to be run inside of Azure instance. + +In order to run test the following environment variables should be defined +(they should be filled with your data): + AZUREUD_SUBSCRIPTION_ID= # your subscription ID + AZUREUD_RESOURCE_GROUP= # your resource group name + AZUREUD_TENANT_ID= # your tenant ID + AZUREUD_CLIENT_ID= # id of your client (application) + AZUREUD_CLIENT_SECRET= # your client(application) secret key + AZUREUD_STORAGE_ACCOUNT= # your storage account name + AZUREUD_STORAGE_ACCESS_KEY= # your storage account access key + +The driver and tests do not create container, instance, etc, all entities should +be created before to run tests / use of libstorage. diff --git a/drivers/storage/azure/tests/azure_test.go b/drivers/storage/azureud/tests/azure_test.go similarity index 72% rename from drivers/storage/azure/tests/azure_test.go rename to drivers/storage/azureud/tests/azure_test.go index c2f8d51d..8072699f 100644 --- a/drivers/storage/azure/tests/azure_test.go +++ b/drivers/storage/azureud/tests/azure_test.go @@ -1,6 +1,6 @@ -// +build !libstorage_storage_driver libstorage_storage_driver_azure +// +build !libstorage_storage_driver libstorage_storage_driver_azureud -package azure +package azureud import ( "os" @@ -10,32 +10,30 @@ import ( log "github.com/Sirupsen/logrus" gofig "github.com/akutz/gofig/types" + "github.com/akutz/goof" "github.com/stretchr/testify/assert" "github.com/codedellemc/libstorage/api/context" - "github.com/codedellemc/libstorage/api/registry" "github.com/codedellemc/libstorage/api/server" apitests "github.com/codedellemc/libstorage/api/tests" "github.com/codedellemc/libstorage/api/types" "github.com/codedellemc/libstorage/api/utils" - "github.com/codedellemc/libstorage/drivers/storage/azure" - azureUtils "github.com/codedellemc/libstorage/drivers/storage/azure/utils" + "github.com/codedellemc/libstorage/drivers/storage/azureud" + azureUtils "github.com/codedellemc/libstorage/drivers/storage/azureud/utils" ) // Put contents of sample config.yml here var ( configYAMLazure = []byte(` - azure: - resourceGroup: "trex" - subscriptionID: "c971aa51-5850-460a-b300-3265d4af154b" - tenantID: "ebbc4596-9828-453c-b95e-b8cb122f45bd" - clientID: "5d7fbebc-2e7b-487d-bf6e-04e4bee8e8cc" - clientSecret: "fill_your_secret" - certPath: "" - container: "vhds" - storageAccount: "trexdisks256" - storageAccessKey: "fill_your_seceret" +azureud: + resourceGroup: "trex" + subscriptionID: "c971aa51-5850-460a-b300-3265d4af154b" + tenantID: "ebbc4596-9828-453c-b95e-b8cb122f45bd" + clientID: "5d7fbebc-2e7b-487d-bf6e-04e4bee8e8cc" + clientSecret: "fill_your_secret" + storageAccount: "trexdisks256" + storageAccessKey: "fill_your_seceret" `) ) @@ -68,20 +66,25 @@ type CleanupVolume struct { client types.Client } -func (ctx *CleanupObjectContextT) add(key string, vol *types.Volume, client types.Client) { +func (ctx *CleanupObjectContextT) add( + key string, + vol *types.Volume, + client types.Client) { + cobj := &CleanupVolume{vol: vol, client: client} ctx.objects[key] = cobj } func (cvol *CleanupVolume) cleanup(key string) { - cvol.client.API().VolumeDetach(nil, azure.Name, cvol.vol.Name, &types.VolumeDetachRequest{Force: true}) - cvol.client.API().VolumeRemove(nil, azure.Name, cvol.vol.Name) + cvol.client.API().VolumeDetach(nil, azureud.Name, cvol.vol.Name, + &types.VolumeDetachRequest{Force: true}) + cvol.client.API().VolumeRemove(nil, azureud.Name, cvol.vol.Name, false) } // Check environment vars to see whether or not to run this test func skipTests() bool { travis, _ := strconv.ParseBool(os.Getenv("TRAVIS")) - noTestAZURE, _ := strconv.ParseBool(os.Getenv("TEST_SKIP_AZURE")) + noTestAZURE, _ := strconv.ParseBool(os.Getenv("TEST_SKIP_AZUREDISK")) return travis || noTestAZURE } @@ -89,10 +92,10 @@ func skipTests() bool { func init() { uuid, _ := types.NewUUID() uuids := strings.Split(uuid.String(), "-") - volumeName = "test-vol-" + uuids[0] + ".vhd" + volumeName = "test-vol-" + uuids[0] uuid, _ = types.NewUUID() uuids = strings.Split(uuid.String(), "-") - volumeName2 = "test-vol-" + uuids[0] + ".vhd" + volumeName2 = "test-vol-" + uuids[0] cleanupObjectContext = CleanupObjectContextT{ objects: make(map[string]CleanupIface), } @@ -107,19 +110,6 @@ func TestMain(m *testing.M) { /////////////////////////////////////////////////////////////////////// ///////// PUBLIC TESTS ///////// /////////////////////////////////////////////////////////////////////// -func TestConfig(t *testing.T) { - if skipTests() { - t.SkipNow() - } - tf := func(config gofig.Config, client types.Client, t *testing.T) { - assert.NotEqual(t, config.GetString("azure.clientID"), "") - assert.Equal(t, config.GetString("azure.clientID"), - "5d7fbebc-2e7b-487d-bf6e-04e4bee8e8cc") - } - apitests.Run(t, azure.Name, configYAMLazure, tf) - cleanupObjectContext.cleanup() -} - // Check if InstanceID metadata is properly returned by executor // and InstanceID.ID is filled out by InstanceInspect func TestInstanceID(t *testing.T) { @@ -127,38 +117,21 @@ func TestInstanceID(t *testing.T) { t.SkipNow() } - // create storage driver - sd, err := registry.NewStorageDriver(azure.Name) - if err != nil { - t.Fatal(err) - } - - // initialize storage driver ctx := context.Background() - if err := sd.Init(ctx, registry.NewConfig()); err != nil { - t.Fatal(err) - } - // Get Instance ID metadata from executor + // Get Instance ID from executor iid, err := azureUtils.InstanceID(ctx) assert.NoError(t, err) if err != nil { t.Fatal(err) } - // Fill in Instance ID's ID field with InstanceInspect - ctx = ctx.WithValue(context.InstanceIDKey, iid) - i, err := sd.InstanceInspect(ctx, utils.NewStore()) - if err != nil { - t.Fatal(err) - } - - iid = i.InstanceID + assert.NotEqual(t, iid.ID, "") // test resulting InstanceID apitests.Run( - t, azure.Name, configYAMLazure, + t, azureud.Name, configYAMLazure, (&apitests.InstanceIDTest{ - Driver: azure.Name, + Driver: azureud.Name, Expected: iid, }).Test) cleanupObjectContext.cleanup() @@ -175,10 +148,10 @@ func TestServices(t *testing.T) { assert.NoError(t, err) assert.Equal(t, len(reply), 1) - _, ok := reply[azure.Name] + _, ok := reply[azureud.Name] assert.True(t, ok) } - apitests.Run(t, azure.Name, configYAMLazure, tf) + apitests.Run(t, azureud.Name, configYAMLazure, tf) cleanupObjectContext.cleanup() } @@ -192,7 +165,7 @@ func TestVolumeCreateRemove(t *testing.T) { vol := volumeCreate(t, client, volumeName) volumeRemove(t, client, vol.ID) } - apitests.Run(t, azure.Name, configYAMLazure, tf) + apitests.Run(t, azureud.Name, configYAMLazure, tf) cleanupObjectContext.cleanup() } @@ -212,7 +185,7 @@ func TestVolumes(t *testing.T) { volumeRemove(t, client, vol1.ID) volumeRemove(t, client, vol2.ID) } - apitests.Run(t, azure.Name, configYAMLazure, tf) + apitests.Run(t, azureud.Name, configYAMLazure, tf) cleanupObjectContext.cleanup() } @@ -229,16 +202,18 @@ func TestVolumeAttachDetach(t *testing.T) { _ = volumeInspectAttachedToMyInstance(t, client, vol.ID) } tf2 := func(config gofig.Config, client types.Client, t *testing.T) { - _ = volumeInspectAttachedToMyInstanceWithForeignInstance(t, client, vol.ID) + _ = volumeInspectAttachedToMyInstanceWithForeignInstance( + t, client, vol.ID) } tf3 := func(config gofig.Config, client types.Client, t *testing.T) { _ = volumeDetach(t, client, vol.ID) _ = volumeInspectDetached(t, client, vol.ID) volumeRemove(t, client, vol.ID) } - apitests.Run(t, azure.Name, configYAMLazure, tf) - apitests.RunWithClientType(t, types.ControllerClient, azure.Name, configYAMLazure, tf2) - apitests.Run(t, azure.Name, configYAMLazure, tf3) + apitests.Run(t, azureud.Name, configYAMLazure, tf) + apitests.RunWithClientType(t, types.ControllerClient, azureud.Name, + configYAMLazure, tf2) + apitests.Run(t, azureud.Name, configYAMLazure, tf3) cleanupObjectContext.cleanup() } @@ -262,7 +237,8 @@ func volumeCreate( Opts: opts, } // Send request and retrieve created libStorage types.Volume - vol, err := client.API().VolumeCreate(nil, azure.Name, volumeCreateRequest) + vol, err := client.API().VolumeCreate(nil, azureud.Name, + volumeCreateRequest) assert.NoError(t, err) if err != nil { t.FailNow() @@ -277,11 +253,12 @@ func volumeCreate( return vol } -// Test volume retrieval by volume name using Volumes, which retrieves all volumes -// from the storage driver without filtering, and filters the volumes externally. +// Test volume retrieval by volume name using Volumes, which retrieves all +// volumes from the storage driver without filtering, and filters the volumes +// externally. func volumeByName( t *testing.T, client types.Client, volumeName string) *types.Volume { - log.WithField("volumeName", volumeName).Info("get volume by azure.Name") + log.WithField("volumeName", volumeName).Info("get volume by name") // Retrieve all volumes vols, err := client.API().Volumes(nil, 0) assert.NoError(t, err) @@ -290,8 +267,8 @@ func volumeByName( } // Filter volumes to those under the azure service, // and find a volume matching inputted volume name - assert.Contains(t, vols, azure.Name) - for _, vol := range vols[azure.Name] { + assert.Contains(t, vols, azureud.Name) + for _, vol := range vols[azureud.Name] { if vol.Name == volumeName { return vol } @@ -306,7 +283,7 @@ func volumeByName( func volumeRemove(t *testing.T, client types.Client, volumeID string) { log.WithField("volumeID", volumeID).Info("removing volume") err := client.API().VolumeRemove( - nil, azure.Name, volumeID) + nil, azureud.Name, volumeID, false) assert.NoError(t, err) if err != nil { t.Error("failed volumeRemove") @@ -320,7 +297,9 @@ func volumeAttach( t *testing.T, client types.Client, volumeID string) *types.Volume { log.WithField("volumeID", volumeID).Info("attaching volume") // Get next device name from executor - nextDevice, err := client.Executor().NextDevice(context.Background().WithValue(context.ServiceKey, azure.Name), + nextDevice, err := client.Executor().NextDevice( + context.Background().WithValue( + context.ServiceKey, azureud.Name), utils.NewStore()) assert.NoError(t, err) if err != nil { @@ -328,7 +307,7 @@ func volumeAttach( t.FailNow() } reply, token, err := client.API().VolumeAttach( - nil, azure.Name, volumeID, &types.VolumeAttachRequest{ + nil, azureud.Name, volumeID, &types.VolumeAttachRequest{ NextDeviceName: &nextDevice, }) assert.NoError(t, err) @@ -348,7 +327,8 @@ func volumeAttach( func volumeInspect( t *testing.T, client types.Client, volumeID string) *types.Volume { log.WithField("volumeID", volumeID).Info("inspecting volume") - reply, err := client.API().VolumeInspect(nil, azure.Name, volumeID, 0) + reply, err := client.API().VolumeInspect( + nil, azureud.Name, volumeID, 0) assert.NoError(t, err) if err != nil { t.Error("failed volumeInspect") @@ -363,7 +343,7 @@ func volumeInspectAttached( t *testing.T, client types.Client, volumeID string) *types.Volume { log.WithField("volumeID", volumeID).Info("inspecting volume") reply, err := client.API().VolumeInspect( - nil, azure.Name, volumeID, + nil, azureud.Name, volumeID, types.VolAttReq) assert.NoError(t, err) if err != nil { @@ -378,8 +358,11 @@ func volumeInspectAttached( // Test if volume is attached to specified instance func volumeInspectAttachedToMyInstance( t *testing.T, client types.Client, volumeID string) *types.Volume { - log.WithField("volumeID", volumeID).Info("inspecting volume attached to my instance") - reply, err := client.API().VolumeInspect(nil, azure.Name, volumeID, types.VolAttReqForInstance) + log.WithField("volumeID", volumeID).Info( + "inspecting volume attached to my instance") + reply, err := client.API().VolumeInspect( + nil, azureud.Name, volumeID, + types.VolAttReqOnlyVolsAttachedToInstance) assert.NoError(t, err) if err != nil { t.Error("failed volumeInspectAttached") @@ -394,20 +377,20 @@ func volumeInspectAttachedToMyInstance( func volumeInspectAttachedToMyInstanceWithForeignInstance( t *testing.T, client types.Client, volumeID string) *types.Volume { ctx := context.Background() - iidm := types.InstanceIDMap{"azure": &types.InstanceID{ID: "none", Driver: "azure"}} + iidm := types.InstanceIDMap{"azureud": &types.InstanceID{ + ID: "none", + Driver: "azureud"}} ctx = ctx.WithValue(context.AllInstanceIDsKey, iidm) log.WithField("volumeID", volumeID).Info( - "inspecting volume attached to my instance with foreign instance in filter") + "inspecting volume attached to my instance with " + + "foreign instance in filter") reply, err := client.API().VolumeInspect( - ctx, azure.Name, volumeID, - types.VolAttReqForInstance) - assert.NoError(t, err) - if err != nil { - t.Error("failed volumeInspectAttached") - t.FailNow() - } - apitests.LogAsJSON(reply, t) - assert.Len(t, reply.Attachments, 0) + ctx, azureud.Name, volumeID, + types.VolAttReqOnlyVolsAttachedToInstance) + // Filtering as "to me" returns a 404 since its not attached to us + assert.Error(t, err) + httpErr := err.(goof.HTTPError) + assert.Equal(t, 404, httpErr.Status()) return reply } @@ -416,7 +399,7 @@ func volumeInspectDetached( t *testing.T, client types.Client, volumeID string) *types.Volume { log.WithField("volumeID", volumeID).Info("inspecting volume") reply, err := client.API().VolumeInspect( - nil, azure.Name, volumeID, + nil, azureud.Name, volumeID, types.VolAttNone) assert.NoError(t, err) @@ -435,7 +418,7 @@ func volumeDetach( t *testing.T, client types.Client, volumeID string) *types.Volume { log.WithField("volumeID", volumeID).Info("detaching volume") reply, err := client.API().VolumeDetach( - nil, azure.Name, volumeID, &types.VolumeDetachRequest{}) + nil, azureud.Name, volumeID, &types.VolumeDetachRequest{}) assert.NoError(t, err) if err != nil { t.Error("failed volumeDetach") diff --git a/drivers/storage/azureud/tests/coverage.mk b/drivers/storage/azureud/tests/coverage.mk new file mode 100644 index 00000000..5db64912 --- /dev/null +++ b/drivers/storage/azureud/tests/coverage.mk @@ -0,0 +1,2 @@ +AZUREUD_COVERPKG := $(ROOT_IMPORT_PATH)/drivers/storage/azureud +TEST_COVERPKG_./drivers/storage/azureud/tests := $(AZUREUD_COVERPKG),$(AZUREUD_COVERPKG)/executor diff --git a/drivers/storage/azureud/utils/utils.go b/drivers/storage/azureud/utils/utils.go new file mode 100644 index 00000000..65ada5d3 --- /dev/null +++ b/drivers/storage/azureud/utils/utils.go @@ -0,0 +1,56 @@ +// +build !libstorage_storage_driver libstorage_storage_driver_azureud + +package utils + +import ( + "net" + "net/http" + "os" + "time" + + "github.com/codedellemc/libstorage/api/types" + "github.com/codedellemc/libstorage/drivers/storage/azureud" +) + +const ( + raddr = "169.254.169.254" + maintURL = "http://" + raddr + "/metadata/v1/maintenance" +) + +// IsAzureInstance returns a flag indicating whether the executing host +// is an Azure instance . +func IsAzureInstance(ctx types.Context) (bool, error) { + client := &http.Client{Timeout: time.Duration(1 * time.Second)} + req, err := http.NewRequest(http.MethodGet, maintURL, nil) + if err != nil { + return false, err + } + res, err := doRequestWithClient(ctx, client, req) + if err != nil { + if terr, ok := err.(net.Error); ok && terr.Timeout() { + return false, nil + } + return false, err + } + if res.StatusCode >= 200 && res.StatusCode <= 299 { + return true, nil + } + return false, nil +} + +// InstanceID returns the instance ID for the local host. +func InstanceID(ctx types.Context) (*types.InstanceID, error) { + + // UUID can be obtained as descried in + // https://azure.microsoft.com/en-us/blog/accessing-and-using-azure-vm-unique-id/ + // but this code will use hostname as ID + + hostname, err := os.Hostname() + if err != nil { + return nil, err + } + return &types.InstanceID{ + ID: hostname, + Driver: azureud.Name, + }, nil +} diff --git a/drivers/storage/azureud/utils/utils_go17.go b/drivers/storage/azureud/utils/utils_go17.go new file mode 100644 index 00000000..9dfbe0b1 --- /dev/null +++ b/drivers/storage/azureud/utils/utils_go17.go @@ -0,0 +1,22 @@ +// +build go1.7 +// +build !libstorage_storage_driver libstorage_storage_driver_azureud + +package utils + +import ( + "net/http" + + "github.com/codedellemc/libstorage/api/types" +) + +func doRequest(ctx types.Context, req *http.Request) (*http.Response, error) { + return doRequestWithClient(ctx, http.DefaultClient, req) +} + +func doRequestWithClient( + ctx types.Context, + client *http.Client, + req *http.Request) (*http.Response, error) { + req = req.WithContext(ctx) + return client.Do(req) +} diff --git a/drivers/storage/azureud/utils/utils_pre_go17.go b/drivers/storage/azureud/utils/utils_pre_go17.go new file mode 100644 index 00000000..61ce7f51 --- /dev/null +++ b/drivers/storage/azureud/utils/utils_pre_go17.go @@ -0,0 +1,23 @@ +// +build !go1.7 +// +build !libstorage_storage_driver libstorage_storage_driver_azureud + +package utils + +import ( + "net/http" + + "golang.org/x/net/context/ctxhttp" + + "github.com/codedellemc/libstorage/api/types" +) + +func doRequest(ctx types.Context, req *http.Request) (*http.Response, error) { + return doRequestWithClient(ctx, http.DefaultClient, req) +} + +func doRequestWithClient( + ctx types.Context, + client *http.Client, + req *http.Request) (*http.Response, error) { + return ctxhttp.Do(ctx, client, req) +} diff --git a/drivers/storage/azure/utils/utils_test.go b/drivers/storage/azureud/utils/utils_test.go similarity index 84% rename from drivers/storage/azure/utils/utils_test.go rename to drivers/storage/azureud/utils/utils_test.go index 7f673f5b..cd748ec2 100644 --- a/drivers/storage/azure/utils/utils_test.go +++ b/drivers/storage/azureud/utils/utils_test.go @@ -1,4 +1,4 @@ -// +build !libstorage_storage_driver libstorage_storage_driver_azure +// +build !libstorage_storage_driver libstorage_storage_driver_azureud package utils @@ -13,7 +13,7 @@ import ( ) func skipTest(t *testing.T) { - if ok, _ := strconv.ParseBool(os.Getenv("AZURE_UTILS_TEST")); !ok { + if ok, _ := strconv.ParseBool(os.Getenv("AZUREUD_UTILS_TEST")); !ok { t.Skip() } } diff --git a/drivers/storage/azure/utils/utils_unix.go b/drivers/storage/azureud/utils/utils_unix.go similarity index 98% rename from drivers/storage/azure/utils/utils_unix.go rename to drivers/storage/azureud/utils/utils_unix.go index b96d1b0a..67640bbe 100644 --- a/drivers/storage/azure/utils/utils_unix.go +++ b/drivers/storage/azureud/utils/utils_unix.go @@ -1,5 +1,5 @@ // +build !windows -// +build !libstorage_storage_driver libstorage_storage_driver_azure +// +build !libstorage_storage_driver libstorage_storage_driver_azureud package utils diff --git a/imports/executors/imports_executor.go b/imports/executors/imports_executor.go index a4b0f290..3e488b90 100644 --- a/imports/executors/imports_executor.go +++ b/imports/executors/imports_executor.go @@ -4,7 +4,7 @@ package executors import ( // load the storage executors - _ "github.com/codedellemc/libstorage/drivers/storage/azure/executor" + _ "github.com/codedellemc/libstorage/drivers/storage/azureud/executor" _ "github.com/codedellemc/libstorage/drivers/storage/digitalocean/executor" _ "github.com/codedellemc/libstorage/drivers/storage/ebs/executor" _ "github.com/codedellemc/libstorage/drivers/storage/efs/executor" diff --git a/imports/executors/imports_executor_azure.go b/imports/executors/imports_executor_azureud.go similarity index 58% rename from imports/executors/imports_executor_azure.go rename to imports/executors/imports_executor_azureud.go index d1533f62..43d90b29 100644 --- a/imports/executors/imports_executor_azure.go +++ b/imports/executors/imports_executor_azureud.go @@ -1,8 +1,8 @@ -// +build libstorage_storage_executor,libstorage_storage_executor_azure +// +build libstorage_storage_executor,libstorage_storage_executor_azureud package executors import ( // load the packages - _ "github.com/codedellemc/libstorage/drivers/storage/azure/executor" + _ "github.com/codedellemc/libstorage/drivers/storage/azureud/executor" ) diff --git a/imports/remote/imports_remote.go b/imports/remote/imports_remote.go index 13ee6d4f..9fe2d0a8 100644 --- a/imports/remote/imports_remote.go +++ b/imports/remote/imports_remote.go @@ -4,7 +4,7 @@ package remote import ( // import to load - _ "github.com/codedellemc/libstorage/drivers/storage/azure/storage" + _ "github.com/codedellemc/libstorage/drivers/storage/azureud/storage" _ "github.com/codedellemc/libstorage/drivers/storage/digitalocean/storage" _ "github.com/codedellemc/libstorage/drivers/storage/ebs/storage" _ "github.com/codedellemc/libstorage/drivers/storage/efs/storage" diff --git a/imports/remote/imports_remote_azure.go b/imports/remote/imports_remote_azureud.go similarity index 59% rename from imports/remote/imports_remote_azure.go rename to imports/remote/imports_remote_azureud.go index 0c3e4f47..c25f702a 100644 --- a/imports/remote/imports_remote_azure.go +++ b/imports/remote/imports_remote_azureud.go @@ -1,8 +1,8 @@ -// +build libstorage_storage_driver,libstorage_storage_driver_azure +// +build libstorage_storage_driver,libstorage_storage_driver_azureud package remote import ( // load the packages - _ "github.com/codedellemc/libstorage/drivers/storage/azure/storage" + _ "github.com/codedellemc/libstorage/drivers/storage/azureud/storage" )