yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/ecloud/client.go (about) 1 // Copyright 2019 Yunion 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ecloud 16 17 import ( 18 "bytes" 19 "context" 20 "crypto/sha256" 21 "encoding/hex" 22 "fmt" 23 "io/ioutil" 24 "net/http" 25 "net/url" 26 "os" 27 "strings" 28 29 "yunion.io/x/jsonutils" 30 "yunion.io/x/pkg/errors" 31 32 api "yunion.io/x/cloudmux/pkg/apis/compute" 33 "yunion.io/x/cloudmux/pkg/cloudprovider" 34 "yunion.io/x/onecloud/pkg/util/httputils" 35 ) 36 37 const ( 38 CLOUD_PROVIDER_ECLOUD = api.CLOUD_PROVIDER_ECLOUD 39 CLOUD_PROVIDER_ECLOUD_CN = "移动云" 40 CLOUD_PROVIDER_ECLOUD_EN = "Ecloud" 41 CLOUD_API_VERSION = "2016-12-05" 42 43 ECLOUD_DEFAULT_REGION = "beijing-1" 44 ) 45 46 type SEcloudClientConfig struct { 47 cpcfg cloudprovider.ProviderConfig 48 signer ISigner 49 50 debug bool 51 } 52 53 func NewEcloudClientConfig(signer ISigner) *SEcloudClientConfig { 54 cfg := &SEcloudClientConfig{ 55 signer: signer, 56 } 57 return cfg 58 } 59 60 func (cfg *SEcloudClientConfig) SetCloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *SEcloudClientConfig { 61 cfg.cpcfg = cpcfg 62 return cfg 63 } 64 65 func (cfg *SEcloudClientConfig) SetDebug(debug bool) *SEcloudClientConfig { 66 cfg.debug = debug 67 return cfg 68 } 69 70 type SEcloudClient struct { 71 *SEcloudClientConfig 72 73 httpClient *http.Client 74 iregions []cloudprovider.ICloudRegion 75 } 76 77 func NewEcloudClient(cfg *SEcloudClientConfig) (*SEcloudClient, error) { 78 httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient() 79 return &SEcloudClient{ 80 SEcloudClientConfig: cfg, 81 httpClient: httpClient, 82 }, nil 83 } 84 85 func (self *SEcloudClient) GetAccessEnv() string { 86 return api.CLOUD_ACCESS_ENV_ECLOUD_CHINA 87 } 88 89 func (ec *SEcloudClient) fetchRegions() { 90 regions := make([]SRegion, 0, len(regionList)) 91 for id, name := range regionList { 92 region := SRegion{} 93 region.ID = id 94 region.Name = name 95 region.client = ec 96 regions = append(regions, region) 97 } 98 iregions := make([]cloudprovider.ICloudRegion, len(regions)) 99 for i := range iregions { 100 iregions[i] = ®ions[i] 101 } 102 ec.iregions = iregions 103 return 104 } 105 106 func (ec *SEcloudClient) TryConnect() error { 107 iregions := ec.GetIRegions() 108 if len(iregions) == 0 { 109 return fmt.Errorf("no invalid region for ecloud") 110 } 111 _, err := iregions[0].GetIZones() 112 if err != nil { 113 return errors.Wrap(err, "try to connect failed") 114 } 115 return nil 116 } 117 118 func (ec *SEcloudClient) GetIRegions() []cloudprovider.ICloudRegion { 119 if ec.iregions == nil { 120 ec.fetchRegions() 121 } 122 return ec.iregions 123 } 124 125 func (ec *SEcloudClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { 126 iregions := ec.GetIRegions() 127 for i := range iregions { 128 if iregions[i].GetGlobalId() == id { 129 return iregions[i], nil 130 } 131 } 132 return nil, cloudprovider.ErrNotFound 133 } 134 135 func (ec *SEcloudClient) GetRegionById(id string) (*SRegion, error) { 136 iregions := ec.GetIRegions() 137 for i := range iregions { 138 if iregions[i].GetId() == id { 139 return iregions[i].(*SRegion), nil 140 } 141 } 142 return nil, cloudprovider.ErrNotFound 143 } 144 145 func (ec *SEcloudClient) GetCapabilities() []string { 146 caps := []string{ 147 cloudprovider.CLOUD_CAPABILITY_COMPUTE + cloudprovider.READ_ONLY_SUFFIX, 148 cloudprovider.CLOUD_CAPABILITY_NETWORK + cloudprovider.READ_ONLY_SUFFIX, 149 cloudprovider.CLOUD_CAPABILITY_EIP + cloudprovider.READ_ONLY_SUFFIX, 150 } 151 return caps 152 } 153 154 func (ec *SEcloudClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { 155 subAccount := cloudprovider.SSubAccount{} 156 subAccount.Name = ec.cpcfg.Name 157 subAccount.Account = ec.signer.GetAccessKeyId() 158 subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL 159 return []cloudprovider.SSubAccount{subAccount}, nil 160 } 161 162 func (ec *SEcloudClient) GetAccountId() string { 163 return ec.signer.GetAccessKeyId() 164 } 165 166 func (ec *SEcloudClient) GetCloudRegionExternalIdPrefix() string { 167 return CLOUD_PROVIDER_ECLOUD 168 } 169 170 func (ec *SEcloudClient) completeSingParams(request IRequest) (err error) { 171 queryParams := request.GetQueryParams() 172 queryParams["AccessKey"] = ec.signer.GetAccessKeyId() 173 queryParams["Version"] = request.GetVersion() 174 queryParams["Timestamp"] = request.GetTimestamp() 175 queryParams["SignatureMethod"] = ec.signer.GetName() 176 queryParams["SignatureVersion"] = ec.signer.GetVersion() 177 queryParams["SignatureNonce"] = ec.signer.GetNonce() 178 return 179 } 180 181 func (ec *SEcloudClient) buildStringToSign(request IRequest) string { 182 signParams := request.GetQueryParams() 183 queryString := getUrlFormedMap(signParams) 184 queryString = strings.Replace(queryString, "+", "%20", -1) 185 queryString = strings.Replace(queryString, "*", "%2A", -1) 186 queryString = strings.Replace(queryString, "%7E", "~", -1) 187 shaString := sha256.Sum256([]byte(queryString)) 188 summaryQuery := hex.EncodeToString(shaString[:]) 189 serverPath := strings.Replace(request.GetServerPath(), "/", "%2F", -1) 190 return fmt.Sprintf("%s\n%s\n%s", request.GetMethod(), serverPath, summaryQuery) 191 } 192 193 func (ec *SEcloudClient) doGet(ctx context.Context, r IRequest, result interface{}) error { 194 r.SetMethod("GET") 195 data, err := ec.request(ctx, r) 196 if err != nil { 197 return err 198 } 199 return data.Unmarshal(result) 200 } 201 202 func (ec *SEcloudClient) doList(ctx context.Context, r IRequest, result interface{}) error { 203 r.SetMethod("GET") 204 // TODO Paging query 205 data, err := ec.request(ctx, r) 206 if err != nil { 207 return err 208 } 209 var ( 210 datas *jsonutils.JSONArray 211 ok bool 212 ) 213 214 if datas, ok = data.(*jsonutils.JSONArray); !ok { 215 if !data.Contains("content") { 216 return ErrMissKey{ 217 Key: "content", 218 Jo: data, 219 } 220 } 221 content, _ := data.Get("content") 222 datas, ok = content.(*jsonutils.JSONArray) 223 if !ok { 224 return fmt.Errorf("The return result should be an array, but:\n%s", content) 225 } 226 } 227 return datas.Unmarshal(result) 228 } 229 230 func (ec *SEcloudClient) request(ctx context.Context, r IRequest) (jsonutils.JSONObject, error) { 231 jrbody, err := ec.doRequest(ctx, r) 232 if err != nil { 233 return nil, err 234 } 235 return r.ForMateResponseBody(jrbody) 236 } 237 238 func (ec *SEcloudClient) doRequest(ctx context.Context, r IRequest) (jsonutils.JSONObject, error) { 239 // sign 240 ec.completeSingParams(r) 241 signature := ec.signer.Sign(ec.buildStringToSign(r), "BC_SIGNATURE&") 242 query := r.GetQueryParams() 243 query["Signature"] = signature 244 header := r.GetHeaders() 245 header["Content-Type"] = "application/json" 246 var urlStr string 247 port := r.GetPort() 248 if len(port) > 0 { 249 urlStr = fmt.Sprintf("%s://%s:%s%s", r.GetScheme(), r.GetEndpoint(), port, r.GetServerPath()) 250 } else { 251 urlStr = fmt.Sprintf("%s://%s%s", r.GetScheme(), r.GetEndpoint(), r.GetServerPath()) 252 } 253 queryString := getUrlFromedMapUnescaped(r.GetQueryParams()) 254 if len(queryString) > 0 { 255 urlStr = urlStr + "?" + queryString 256 } 257 resp, err := httputils.Request( 258 ec.httpClient, 259 ctx, 260 httputils.THttpMethod(r.GetMethod()), 261 urlStr, 262 convertHeader(header), 263 r.GetBodyReader(), 264 ec.debug, 265 ) 266 defer httputils.CloseResponse(resp) 267 if err != nil { 268 return nil, err 269 } 270 rbody, err := ioutil.ReadAll(resp.Body) 271 if err != nil { 272 return nil, errors.Wrap(err, "unable to read body of response") 273 } 274 if ec.debug { 275 fmt.Fprintf(os.Stderr, "Response body: %s\n", string(rbody)) 276 } 277 rbody = bytes.TrimSpace(rbody) 278 279 var jrbody jsonutils.JSONObject 280 if len(rbody) > 0 && (rbody[0] == '{' || rbody[0] == '[') { 281 var err error 282 jrbody, err = jsonutils.Parse(rbody) 283 if err != nil { 284 return nil, errors.Wrapf(err, "unable to parsing json: %s", rbody) 285 } 286 } 287 return jrbody, nil 288 } 289 290 type ErrMissKey struct { 291 Key string 292 Jo jsonutils.JSONObject 293 } 294 295 func (mk ErrMissKey) Error() string { 296 return fmt.Sprintf("The response body should contain the %q key, but it doesn't. It is:\n%s", mk.Key, mk.Jo) 297 } 298 299 func convertHeader(mh map[string]string) http.Header { 300 header := http.Header{} 301 for k, v := range mh { 302 header.Add(k, v) 303 } 304 return header 305 } 306 307 func getUrlFromedMapUnescaped(source map[string]string) string { 308 kvs := make([]string, 0, len(source)) 309 for k, v := range source { 310 kvs = append(kvs, fmt.Sprintf("%s=%s", k, v)) 311 } 312 return strings.Join(kvs, "&") 313 } 314 315 func getUrlFormedMap(source map[string]string) (urlEncoded string) { 316 urlEncoder := url.Values{} 317 for key, value := range source { 318 urlEncoder.Add(key, value) 319 } 320 urlEncoded = urlEncoder.Encode() 321 return 322 }