2017-08-02 17:27:17 +00:00
|
|
|
/*
|
|
|
|
Copyright 2017 Heptio Inc.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package azure
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2017-08-15 20:20:23 +00:00
|
|
|
"os"
|
2017-08-02 17:27:17 +00:00
|
|
|
"time"
|
|
|
|
|
2017-08-15 20:20:23 +00:00
|
|
|
"github.com/Azure/azure-sdk-for-go/arm/disk"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/arm/examples/helpers"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/arm/resources/subscriptions"
|
2017-08-18 17:53:05 +00:00
|
|
|
"github.com/Azure/go-autorest/autorest"
|
2017-08-15 20:20:23 +00:00
|
|
|
"github.com/Azure/go-autorest/autorest/azure"
|
2017-09-14 21:27:31 +00:00
|
|
|
"github.com/pkg/errors"
|
2017-08-02 17:27:17 +00:00
|
|
|
"github.com/satori/uuid"
|
|
|
|
|
|
|
|
"github.com/heptio/ark/pkg/cloudprovider"
|
|
|
|
)
|
|
|
|
|
2017-08-15 20:20:23 +00:00
|
|
|
const (
|
|
|
|
azureClientIDKey string = "AZURE_CLIENT_ID"
|
|
|
|
azureClientSecretKey string = "AZURE_CLIENT_SECRET"
|
|
|
|
azureSubscriptionIDKey string = "AZURE_SUBSCRIPTION_ID"
|
|
|
|
azureTenantIDKey string = "AZURE_TENANT_ID"
|
|
|
|
azureStorageAccountIDKey string = "AZURE_STORAGE_ACCOUNT_ID"
|
|
|
|
azureStorageKeyKey string = "AZURE_STORAGE_KEY"
|
|
|
|
azureResourceGroupKey string = "AZURE_RESOURCE_GROUP"
|
2017-11-13 23:31:36 +00:00
|
|
|
|
|
|
|
locationKey = "location"
|
|
|
|
apiTimeoutKey = "apiTimeout"
|
2017-08-15 20:20:23 +00:00
|
|
|
)
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
type blockStore struct {
|
|
|
|
disks *disk.DisksClient
|
|
|
|
snaps *disk.SnapshotsClient
|
|
|
|
subscription string
|
|
|
|
resourceGroup string
|
|
|
|
location string
|
|
|
|
apiTimeout time.Duration
|
|
|
|
}
|
|
|
|
|
2017-08-15 20:20:23 +00:00
|
|
|
func getConfig() map[string]string {
|
|
|
|
cfg := map[string]string{
|
|
|
|
azureClientIDKey: "",
|
|
|
|
azureClientSecretKey: "",
|
|
|
|
azureSubscriptionIDKey: "",
|
|
|
|
azureTenantIDKey: "",
|
|
|
|
azureStorageAccountIDKey: "",
|
|
|
|
azureStorageKeyKey: "",
|
|
|
|
azureResourceGroupKey: "",
|
|
|
|
}
|
|
|
|
|
|
|
|
for key := range cfg {
|
|
|
|
cfg[key] = os.Getenv(key)
|
|
|
|
}
|
|
|
|
|
|
|
|
return cfg
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
func NewBlockStore() cloudprovider.BlockStore {
|
|
|
|
return &blockStore{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *blockStore) Init(config map[string]string) error {
|
|
|
|
var (
|
|
|
|
location = config[locationKey]
|
|
|
|
apiTimeoutVal = config[apiTimeoutKey]
|
|
|
|
apiTimeout time.Duration
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
2017-08-15 20:20:23 +00:00
|
|
|
if location == "" {
|
2017-11-13 23:31:36 +00:00
|
|
|
return errors.Errorf("missing %s in azure configuration", locationKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
if apiTimeout, err = time.ParseDuration(apiTimeoutVal); err != nil {
|
|
|
|
return errors.Wrapf(err, "could not parse %s (expected time.Duration)", apiTimeoutKey)
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if apiTimeout == 0 {
|
2017-09-13 23:20:12 +00:00
|
|
|
apiTimeout = 2 * time.Minute
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cfg := getConfig()
|
|
|
|
|
|
|
|
spt, err := helpers.NewServicePrincipalTokenFromCredentials(cfg, azure.PublicCloud.ResourceManagerEndpoint)
|
|
|
|
if err != nil {
|
2017-11-13 23:31:36 +00:00
|
|
|
return errors.Wrap(err, "error creating new service principal token")
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
disksClient := disk.NewDisksClient(cfg[azureSubscriptionIDKey])
|
|
|
|
snapsClient := disk.NewSnapshotsClient(cfg[azureSubscriptionIDKey])
|
|
|
|
|
2017-08-18 17:53:05 +00:00
|
|
|
authorizer := autorest.NewBearerAuthorizer(spt)
|
|
|
|
disksClient.Authorizer = authorizer
|
|
|
|
snapsClient.Authorizer = authorizer
|
2017-08-15 20:20:23 +00:00
|
|
|
|
|
|
|
// validate the location
|
|
|
|
groupClient := subscriptions.NewGroupClient()
|
2017-08-18 17:53:05 +00:00
|
|
|
groupClient.Authorizer = authorizer
|
2017-08-15 20:20:23 +00:00
|
|
|
|
|
|
|
locs, err := groupClient.ListLocations(cfg[azureSubscriptionIDKey])
|
|
|
|
if err != nil {
|
2017-11-13 23:31:36 +00:00
|
|
|
return errors.WithStack(err)
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if locs.Value == nil {
|
2017-11-13 23:31:36 +00:00
|
|
|
return errors.New("no locations returned from Azure API")
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
locationExists := false
|
|
|
|
for _, loc := range *locs.Value {
|
|
|
|
if (loc.Name != nil && *loc.Name == location) || (loc.DisplayName != nil && *loc.DisplayName == location) {
|
|
|
|
locationExists = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !locationExists {
|
2017-11-13 23:31:36 +00:00
|
|
|
return errors.Errorf("location %q not found", location)
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
b.disks = &disksClient
|
|
|
|
b.snaps = &snapsClient
|
|
|
|
b.subscription = cfg[azureSubscriptionIDKey]
|
|
|
|
b.resourceGroup = cfg[azureResourceGroupKey]
|
|
|
|
b.location = location
|
|
|
|
b.apiTimeout = apiTimeout
|
|
|
|
|
|
|
|
return nil
|
2017-08-15 20:20:23 +00:00
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
func (b *blockStore) CreateVolumeFromSnapshot(snapshotID, volumeType, volumeAZ string, iops *int64) (string, error) {
|
|
|
|
fullSnapshotName := getFullSnapshotName(b.subscription, b.resourceGroup, snapshotID)
|
2017-08-02 17:27:17 +00:00
|
|
|
diskName := "restore-" + uuid.NewV4().String()
|
|
|
|
|
2017-08-15 20:20:23 +00:00
|
|
|
disk := disk.Model{
|
2017-08-02 17:27:17 +00:00
|
|
|
Name: &diskName,
|
2017-11-13 23:31:36 +00:00
|
|
|
Location: &b.location,
|
2017-08-15 20:20:23 +00:00
|
|
|
Properties: &disk.Properties{
|
|
|
|
CreationData: &disk.CreationData{
|
|
|
|
CreateOption: disk.Copy,
|
2017-08-02 17:27:17 +00:00
|
|
|
SourceResourceID: &fullSnapshotName,
|
|
|
|
},
|
2017-08-15 20:20:23 +00:00
|
|
|
AccountType: disk.StorageAccountTypes(volumeType),
|
2017-08-02 17:27:17 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), b.apiTimeout)
|
2017-08-02 17:27:17 +00:00
|
|
|
defer cancel()
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
_, errChan := b.disks.CreateOrUpdate(b.resourceGroup, *disk.Name, disk, ctx.Done())
|
2017-08-02 17:27:17 +00:00
|
|
|
|
|
|
|
err := <-errChan
|
|
|
|
|
|
|
|
if err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return "", errors.WithStack(err)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
return diskName, nil
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
func (b *blockStore) GetVolumeInfo(volumeID, volumeAZ string) (string, *int64, error) {
|
|
|
|
res, err := b.disks.Get(b.resourceGroup, volumeID)
|
2017-08-02 17:27:17 +00:00
|
|
|
if err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return "", nil, errors.WithStack(err)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return string(res.AccountType), nil, nil
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
func (b *blockStore) IsVolumeReady(volumeID, volumeAZ string) (ready bool, err error) {
|
|
|
|
res, err := b.disks.Get(b.resourceGroup, volumeID)
|
2017-08-02 17:27:17 +00:00
|
|
|
if err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return false, errors.WithStack(err)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if res.ProvisioningState == nil {
|
|
|
|
return false, errors.New("nil ProvisioningState returned from Get call")
|
|
|
|
}
|
|
|
|
|
|
|
|
return *res.ProvisioningState == "Succeeded", nil
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
func (b *blockStore) ListSnapshots(tagFilters map[string]string) ([]string, error) {
|
|
|
|
res, err := b.snaps.ListByResourceGroup(b.resourceGroup)
|
2017-08-02 17:27:17 +00:00
|
|
|
if err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return nil, errors.WithStack(err)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if res.Value == nil {
|
|
|
|
return nil, errors.New("nil Value returned from ListByResourceGroup call")
|
|
|
|
}
|
|
|
|
|
|
|
|
ret := make([]string, 0, len(*res.Value))
|
|
|
|
Snapshot:
|
|
|
|
for _, snap := range *res.Value {
|
|
|
|
if snap.Tags == nil && len(tagFilters) > 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if snap.ID == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Azure doesn't offer tag-filtering through the API so we have to manually
|
|
|
|
// filter results. Require all filter keys to be present, with matching vals.
|
|
|
|
for filterKey, filterVal := range tagFilters {
|
|
|
|
if val, ok := (*snap.Tags)[filterKey]; !ok || val == nil || *val != filterVal {
|
|
|
|
continue Snapshot
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = append(ret, *snap.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
func (b *blockStore) CreateSnapshot(volumeID, volumeAZ string, tags map[string]string) (string, error) {
|
|
|
|
fullDiskName := getFullDiskName(b.subscription, b.resourceGroup, volumeID)
|
2017-08-02 17:27:17 +00:00
|
|
|
// snapshot names must be <= 80 characters long
|
|
|
|
var snapshotName string
|
|
|
|
suffix := "-" + uuid.NewV4().String()
|
|
|
|
|
|
|
|
if len(volumeID) <= (80 - len(suffix)) {
|
|
|
|
snapshotName = volumeID + suffix
|
|
|
|
} else {
|
|
|
|
snapshotName = volumeID[0:80-len(suffix)] + suffix
|
|
|
|
}
|
|
|
|
|
2017-08-15 20:20:23 +00:00
|
|
|
snap := disk.Snapshot{
|
2017-08-02 17:27:17 +00:00
|
|
|
Name: &snapshotName,
|
2017-08-15 20:20:23 +00:00
|
|
|
Properties: &disk.Properties{
|
|
|
|
CreationData: &disk.CreationData{
|
|
|
|
CreateOption: disk.Copy,
|
2017-08-02 17:27:17 +00:00
|
|
|
SourceResourceID: &fullDiskName,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Tags: &map[string]*string{},
|
2017-11-13 23:31:36 +00:00
|
|
|
Location: &b.location,
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range tags {
|
|
|
|
val := v
|
|
|
|
(*snap.Tags)[k] = &val
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), b.apiTimeout)
|
2017-08-02 17:27:17 +00:00
|
|
|
defer cancel()
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
_, errChan := b.snaps.CreateOrUpdate(b.resourceGroup, *snap.Name, snap, ctx.Done())
|
2017-08-02 17:27:17 +00:00
|
|
|
|
|
|
|
err := <-errChan
|
|
|
|
|
|
|
|
if err != nil {
|
2017-09-14 21:27:31 +00:00
|
|
|
return "", errors.WithStack(err)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return snapshotName, nil
|
|
|
|
}
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
func (b *blockStore) DeleteSnapshot(snapshotID string) error {
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), b.apiTimeout)
|
2017-08-02 17:27:17 +00:00
|
|
|
defer cancel()
|
|
|
|
|
2017-11-13 23:31:36 +00:00
|
|
|
_, errChan := b.snaps.Delete(b.resourceGroup, snapshotID, ctx.Done())
|
2017-08-02 17:27:17 +00:00
|
|
|
|
|
|
|
err := <-errChan
|
|
|
|
|
2017-09-14 21:27:31 +00:00
|
|
|
return errors.WithStack(err)
|
2017-08-02 17:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func getFullDiskName(subscription string, resourceGroup string, diskName string) string {
|
|
|
|
return fmt.Sprintf("/subscriptions/%v/resourceGroups/%v/providers/Microsoft.Compute/disks/%v", subscription, resourceGroup, diskName)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getFullSnapshotName(subscription string, resourceGroup string, snapshotName string) string {
|
|
|
|
return fmt.Sprintf("/subscriptions/%v/resourceGroups/%v/providers/Microsoft.Compute/snapshots/%v", subscription, resourceGroup, snapshotName)
|
|
|
|
}
|