github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/provider/azure/config.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package azure 5 6 import ( 7 "net/url" 8 "strings" 9 10 "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/azure" 11 "github.com/Azure/azure-sdk-for-go/arm/storage" 12 "github.com/juju/errors" 13 "github.com/juju/names" 14 "github.com/juju/schema" 15 16 "github.com/juju/juju/environs/config" 17 ) 18 19 const ( 20 configAttrAppId = "application-id" 21 configAttrSubscriptionId = "subscription-id" 22 configAttrTenantId = "tenant-id" 23 configAttrAppPassword = "application-password" 24 configAttrLocation = "location" 25 configAttrEndpoint = "endpoint" 26 configAttrStorageEndpoint = "storage-endpoint" 27 configAttrStorageAccountType = "storage-account-type" 28 29 // The below bits are internal book-keeping things, rather than 30 // configuration. Config is just what we have to work with. 31 32 // configAttrStorageAccount is the name of the storage account. We 33 // can't just use a well-defined name for the storage acocunt because 34 // storage account names must be globally unique; each storage account 35 // has an associated public DNS entry. 36 configAttrStorageAccount = "storage-account" 37 38 // configAttrStorageAccountKey is the primary key for the storage 39 // account. 40 configAttrStorageAccountKey = "storage-account-key" 41 42 // resourceNameLengthMax is the maximum length of resource 43 // names in Azure. 44 resourceNameLengthMax = 80 45 ) 46 47 var configFields = schema.Fields{ 48 configAttrLocation: schema.String(), 49 configAttrEndpoint: schema.String(), 50 configAttrStorageEndpoint: schema.String(), 51 configAttrAppId: schema.String(), 52 configAttrSubscriptionId: schema.String(), 53 configAttrTenantId: schema.String(), 54 configAttrAppPassword: schema.String(), 55 configAttrStorageAccount: schema.String(), 56 configAttrStorageAccountKey: schema.String(), 57 configAttrStorageAccountType: schema.String(), 58 } 59 60 var configDefaults = schema.Defaults{ 61 configAttrStorageAccount: schema.Omit, 62 configAttrStorageAccountKey: schema.Omit, 63 configAttrStorageAccountType: string(storage.StandardLRS), 64 } 65 66 var requiredConfigAttributes = []string{ 67 configAttrAppId, 68 configAttrAppPassword, 69 configAttrSubscriptionId, 70 configAttrTenantId, 71 configAttrLocation, 72 configAttrEndpoint, 73 configAttrStorageEndpoint, 74 } 75 76 var immutableConfigAttributes = []string{ 77 configAttrSubscriptionId, 78 configAttrTenantId, 79 configAttrStorageAccount, 80 configAttrStorageAccountType, 81 } 82 83 var internalConfigAttributes = []string{ 84 configAttrStorageAccount, 85 configAttrStorageAccountKey, 86 } 87 88 type azureModelConfig struct { 89 *config.Config 90 token *azure.ServicePrincipalToken 91 subscriptionId string 92 location string // canonicalized 93 endpoint string 94 storageEndpoint string 95 storageAccount string 96 storageAccountKey string 97 storageAccountType storage.AccountType 98 } 99 100 var knownStorageAccountTypes = []string{ 101 "Standard_LRS", "Standard_GRS", "Standard_RAGRS", "Standard_ZRS", "Premium_LRS", 102 } 103 104 // Validate ensures that the provided configuration is valid for this 105 // provider, and that changes between the old (if provided) and new 106 // configurations are valid. 107 func (*azureEnvironProvider) Validate(newCfg, oldCfg *config.Config) (*config.Config, error) { 108 _, err := validateConfig(newCfg, oldCfg) 109 if err != nil { 110 return nil, errors.Trace(err) 111 } 112 return newCfg, nil 113 } 114 115 func validateConfig(newCfg, oldCfg *config.Config) (*azureModelConfig, error) { 116 err := config.Validate(newCfg, oldCfg) 117 if err != nil { 118 return nil, err 119 } 120 121 validated, err := newCfg.ValidateUnknownAttrs(configFields, configDefaults) 122 if err != nil { 123 return nil, err 124 } 125 126 // Ensure required configuration is provided. 127 for _, key := range requiredConfigAttributes { 128 if value, ok := validated[key].(string); !ok || value == "" { 129 return nil, errors.Errorf("%q config not specified", key) 130 } 131 } 132 if oldCfg != nil { 133 // Ensure immutable configuration isn't changed. 134 oldUnknownAttrs := oldCfg.UnknownAttrs() 135 for _, key := range immutableConfigAttributes { 136 oldValue, hadValue := oldUnknownAttrs[key].(string) 137 if hadValue { 138 newValue, haveValue := validated[key].(string) 139 if !haveValue { 140 return nil, errors.Errorf( 141 "cannot remove immutable %q config", key, 142 ) 143 } 144 if newValue != oldValue { 145 return nil, errors.Errorf( 146 "cannot change immutable %q config (%v -> %v)", 147 key, oldValue, newValue, 148 ) 149 } 150 } 151 // It's valid to go from not having to having. 152 } 153 // TODO(axw) figure out how we intend to handle changing 154 // secrets, such as application key 155 } 156 157 // Resource group names must not exceed 80 characters. Resource group 158 // names are based on the model UUID and model name, the latter of 159 // which the model creator controls. 160 modelTag := names.NewModelTag(newCfg.UUID()) 161 resourceGroup := resourceGroupName(modelTag, newCfg.Name()) 162 if n := len(resourceGroup); n > resourceNameLengthMax { 163 smallestResourceGroup := resourceGroupName(modelTag, "") 164 return nil, errors.Errorf(`resource group name %q is too long 165 166 Please choose a model name of no more than %d characters.`, 167 resourceGroup, 168 resourceNameLengthMax-len(smallestResourceGroup), 169 ) 170 } 171 172 location := canonicalLocation(validated[configAttrLocation].(string)) 173 endpoint := validated[configAttrEndpoint].(string) 174 storageEndpoint := validated[configAttrStorageEndpoint].(string) 175 appId := validated[configAttrAppId].(string) 176 subscriptionId := validated[configAttrSubscriptionId].(string) 177 tenantId := validated[configAttrTenantId].(string) 178 appPassword := validated[configAttrAppPassword].(string) 179 storageAccount, _ := validated[configAttrStorageAccount].(string) 180 storageAccountKey, _ := validated[configAttrStorageAccountKey].(string) 181 storageAccountType := validated[configAttrStorageAccountType].(string) 182 183 if newCfg.FirewallMode() == config.FwGlobal { 184 // We do not currently support the "global" firewall mode. 185 return nil, errNoFwGlobal 186 } 187 188 if !isKnownStorageAccountType(storageAccountType) { 189 return nil, errors.Errorf( 190 "invalid storage account type %q, expected one of: %q", 191 storageAccountType, knownStorageAccountTypes, 192 ) 193 } 194 195 // The Azure storage code wants the endpoint host only, not the URL. 196 storageEndpointURL, err := url.Parse(storageEndpoint) 197 if err != nil { 198 return nil, errors.Annotate(err, "parsing storage endpoint URL") 199 } 200 201 token, err := azure.NewServicePrincipalToken( 202 appId, appPassword, tenantId, 203 azure.AzureResourceManagerScope, 204 ) 205 if err != nil { 206 return nil, errors.Annotate(err, "constructing service principal token") 207 } 208 209 azureConfig := &azureModelConfig{ 210 newCfg, 211 token, 212 subscriptionId, 213 location, 214 endpoint, 215 storageEndpointURL.Host, 216 storageAccount, 217 storageAccountKey, 218 storage.AccountType(storageAccountType), 219 } 220 221 return azureConfig, nil 222 } 223 224 // isKnownStorageAccountType reports whether or not the given string identifies 225 // a known storage account type. 226 func isKnownStorageAccountType(t string) bool { 227 for _, knownStorageAccountType := range knownStorageAccountTypes { 228 if t == knownStorageAccountType { 229 return true 230 } 231 } 232 return false 233 } 234 235 // canonicalLocation returns the canonicalized location string. This involves 236 // stripping whitespace, and lowercasing. The ARM APIs do not support embedded 237 // whitespace, whereas the old Service Management APIs used to; we allow the 238 // user to provide either, and canonicalize them to one form that ARM allows. 239 func canonicalLocation(s string) string { 240 s = strings.Replace(s, " ", "", -1) 241 return strings.ToLower(s) 242 }