github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/azure/internal/azurecli/az.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package azurecli 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "os/exec" 11 "strings" 12 13 "github.com/Azure/go-autorest/autorest/adal" 14 "github.com/juju/errors" 15 "github.com/juju/loggo" 16 ) 17 18 // Logger for the Azure provider. 19 var logger = loggo.GetLogger("juju.provider.azure.internal.azurecli") 20 21 // AzureCLI 22 type AzureCLI struct { 23 // Exec is a function that executes system commands and returns 24 // the output. If this is nil then a default implementation using 25 // os.exec will be used. 26 Exec func(cmd string, args []string) (stdout []byte, err error) 27 } 28 29 // Error represents an error returned from the Azure CLI. 30 type Error struct { 31 exec.ExitError 32 } 33 34 // Error implements the error interface. 35 func (e *Error) Error() string { 36 if len(e.Stderr) == 0 { 37 return e.ExitError.Error() 38 } 39 n := bytes.IndexByte(e.Stderr, '\n') 40 if n < 0 { 41 return string(e.Stderr) 42 } 43 return string(e.Stderr[:n]) 44 } 45 46 // exec runs the given command using Exec if specified, or 47 // os.exec.Command. 48 func (a AzureCLI) exec(cmd string, args []string) ([]byte, error) { 49 var out []byte 50 var err error 51 if a.Exec != nil { 52 out, err = a.Exec(cmd, args) 53 } else { 54 out, err = exec.Command(cmd, args...).Output() 55 } 56 if exitError, ok := errors.Cause(err).(*exec.ExitError); ok { 57 err = &Error{ 58 ExitError: *exitError, 59 } 60 } 61 return out, err 62 } 63 64 // run attempts to execute "az" with the given arguments. Unmarshalling 65 // the json output into v. 66 func (a AzureCLI) run(v interface{}, args ...string) error { 67 args = append(args, "-o", "json") 68 logger.Debugf("running az %s", strings.Join(args, " ")) 69 b, err := a.exec("az", args) 70 if err != nil { 71 return errors.Annotate(err, "execution failure") 72 } 73 if err := json.Unmarshal(b, v); err != nil { 74 return errors.Annotate(err, "cannot unmarshal output") 75 } 76 return nil 77 } 78 79 // AccessToken contains the result of the GetAccessToken function. 80 type AccessToken struct { 81 AccessToken string `json:"accessToken"` 82 ExpiresOn string `json:"expiresOn"` 83 Subscription string `json:"subscription"` 84 Tenant string `json:"tenant"` 85 TokenType string `json:"tokenType"` 86 } 87 88 // Token creates an adal.Token from the AccessToken. This token can be 89 // used with go-autorest to access azure endpoints. 90 func (t AccessToken) Token() *adal.Token { 91 return &adal.Token{ 92 AccessToken: t.AccessToken, 93 Type: t.TokenType, 94 } 95 } 96 97 // GetAccessToken gets an access token from the Azure CLI to access the 98 // given resource using the given subscription. Either subscription or 99 // resource may be empty in which case the default from the az 100 // application are used. 101 func (a AzureCLI) GetAccessToken(subscription, resource string) (*AccessToken, error) { 102 cmd := []string{"account", "get-access-token"} 103 if subscription != "" { 104 cmd = append(cmd, "--subscription", subscription) 105 } 106 if resource != "" { 107 cmd = append(cmd, "--resource", resource) 108 } 109 var tok AccessToken 110 if err := a.run(&tok, cmd...); err != nil { 111 return nil, errors.Trace(err) 112 } 113 return &tok, nil 114 } 115 116 // Account contains details of an azure account (subscription). 117 type Account struct { 118 CloudName string `json:"cloudName"` 119 ID string `json:"id"` 120 IsDefault bool `json:"isDefault"` 121 Name string `json:"name"` 122 State string `json:"state"` 123 TenantId string `json:"tenantId"` 124 } 125 126 // showAccount is a version of Account, but that can handle the subtle 127 // difference in output from az account show. 128 type showAccount struct { 129 Account 130 EnvironmentName string `json:"environmentName"` 131 } 132 133 // ShowAccount returns the account details for the account with the given 134 // subscription ID. If the subscription is empty then the default Azure 135 // CLI account is returned. 136 func (a AzureCLI) ShowAccount(subscription string) (*Account, error) { 137 cmd := []string{"account", "show"} 138 if subscription != "" { 139 cmd = append(cmd, "--subscription", subscription) 140 } 141 var acc showAccount 142 if err := a.run(&acc, cmd...); err != nil { 143 return nil, errors.Trace(err) 144 } 145 if acc.Account.CloudName == "" { 146 acc.Account.CloudName = acc.EnvironmentName 147 } 148 return &acc.Account, nil 149 } 150 151 // ListAccounts returns the details for all accounts available in the 152 // Azure CLI. 153 func (a AzureCLI) ListAccounts() ([]Account, error) { 154 var accounts []Account 155 if err := a.run(&accounts, "account", "list"); err != nil { 156 return nil, errors.Trace(err) 157 } 158 return accounts, nil 159 } 160 161 // FindAccountsWithCloudName returns the details for all accounts with 162 // the given cloud name.. 163 func (a AzureCLI) FindAccountsWithCloudName(name string) ([]Account, error) { 164 var accounts []Account 165 cmd := []string{ 166 "account", 167 "list", 168 "--query", fmt.Sprintf("[?cloudName=='%s']", name), 169 } 170 if err := a.run(&accounts, cmd...); err != nil { 171 return nil, errors.Trace(err) 172 } 173 return accounts, nil 174 } 175 176 // Cloud contains details of a cloud configured in the Azure CLI. 177 type Cloud struct { 178 Endpoints CloudEndpoints `json:"endpoints"` 179 IsActive bool `json:"isActive"` 180 Name string `json:"name"` 181 Profile string `json:"profile"` 182 Suffixes CloudSuffixes `json:"suffixes"` 183 } 184 185 // CloudEndpoints contains the endpoints used by a cloud. 186 type CloudEndpoints struct { 187 ActiveDirectory string `json:"activeDirectory"` 188 ActiveDirectoryGraphResourceID string `json:"activeDirectoryGraphResourceId"` 189 ActiveDirectoryResourceID string `json:"activeDirectoryResourceId"` 190 BatchResourceID string `json:"batchResourceId"` 191 Management string `json:"management"` 192 ResourceManager string `json:"resourceManager"` 193 SQLManagement string `json:"sqlManagement"` 194 } 195 196 // CloudSuffixes contains the suffixes used with a cloud. 197 type CloudSuffixes struct { 198 AzureDatalakeAnalyticsCatalogAndJobEndpoint string `json:"azureDatalakeAnalyticsCatalogAndJobEndpoint"` 199 AzureDatalakeStoreFileSystemEndpoint string `json:"azureDatalakeStoreFileSystemEndpoint"` 200 KeyvaultDNS string `json:"keyvaultDns"` 201 SQLServerHostname string `json:"sqlServerHostname"` 202 StorageEndpoint string `json:"storageEndpoint"` 203 } 204 205 // ShowCloud returns the details of the cloud with the given name. If the 206 // name is empty then the details of the default cloud will be returned. 207 func (a AzureCLI) ShowCloud(name string) (*Cloud, error) { 208 cmd := []string{"cloud", "show"} 209 if name != "" { 210 cmd = append(cmd, "--name", name) 211 } 212 var cloud Cloud 213 if err := a.run(&cloud, cmd...); err != nil { 214 return nil, err 215 } 216 return &cloud, nil 217 } 218 219 // FindCloudsWithResourceManagerEndpoint returns a list of clouds which 220 // use the given url for it's resource manager endpoint. 221 func (a AzureCLI) FindCloudsWithResourceManagerEndpoint(url string) ([]Cloud, error) { 222 var clouds []Cloud 223 cmd := []string{ 224 "cloud", 225 "list", 226 "--query", 227 fmt.Sprintf("[?endpoints.resourceManager=='%s']", url), 228 } 229 if err := a.run(&clouds, cmd...); err != nil { 230 return nil, err 231 } 232 return clouds, nil 233 } 234 235 // ListClouds returns the details for all clouds available in the Azure 236 // CLI. 237 func (a AzureCLI) ListClouds() ([]Cloud, error) { 238 var clouds []Cloud 239 if err := a.run(&clouds, "cloud", "list"); err != nil { 240 return nil, errors.Trace(err) 241 } 242 return clouds, nil 243 }