yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/ctyun/ctyun.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 ctyun 16 17 import ( 18 "context" 19 "crypto/hmac" 20 "crypto/sha1" 21 "encoding/base64" 22 "fmt" 23 "net/http" 24 "net/url" 25 "strconv" 26 "strings" 27 "time" 28 29 "github.com/aliyun/alibaba-cloud-sdk-go/sdk/utils" 30 31 "yunion.io/x/jsonutils" 32 "yunion.io/x/pkg/errors" 33 34 api "yunion.io/x/cloudmux/pkg/apis/compute" 35 "yunion.io/x/cloudmux/pkg/cloudprovider" 36 "yunion.io/x/onecloud/pkg/util/httputils" 37 ) 38 39 const ( 40 CTYUN_API_HOST = "https://api.ctyun.cn" 41 CLOUD_PROVIDER_CTYUN = api.CLOUD_PROVIDER_CTYUN 42 CLOUD_PROVIDER_CTYUN_CN = "天翼云" 43 CLOUD_PROVIDER_CTYUN_EN = "Ctyun" 44 CTYUN_DEFAULT_REGION = "cn-bj4" 45 46 CTYUN_API_VERSION = "2019-11-22" 47 ) 48 49 type CtyunClientConfig struct { 50 cpcfg cloudprovider.ProviderConfig 51 options *cloudprovider.SCtyunExtraOptions 52 53 projectId string 54 accessKey string 55 accessSecret string 56 57 debug bool 58 } 59 60 func NewSCtyunClientConfig(accessKey, accessSecret string, options *cloudprovider.SCtyunExtraOptions) *CtyunClientConfig { 61 cfg := &CtyunClientConfig{ 62 accessKey: accessKey, 63 accessSecret: accessSecret, 64 options: options, 65 } 66 return cfg 67 } 68 69 func (cfg *CtyunClientConfig) ProjectId(projectId string) *CtyunClientConfig { 70 cfg.projectId = projectId 71 return cfg 72 } 73 74 func (cfg *CtyunClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *CtyunClientConfig { 75 cfg.cpcfg = cpcfg 76 return cfg 77 } 78 79 func (cfg *CtyunClientConfig) Debug(debug bool) *CtyunClientConfig { 80 cfg.debug = debug 81 return cfg 82 } 83 84 type SCtyunClient struct { 85 *CtyunClientConfig 86 87 httpClient *http.Client 88 iregions []cloudprovider.ICloudRegion 89 } 90 91 func NewSCtyunClient(cfg *CtyunClientConfig) (*SCtyunClient, error) { 92 httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient() 93 ts, _ := httpClient.Transport.(*http.Transport) 94 httpClient.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response), error) { 95 if cfg.cpcfg.ReadOnly { 96 if req.Method == "GET" { 97 return nil, nil 98 } 99 return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) 100 } 101 return nil, nil 102 }) 103 client := &SCtyunClient{ 104 CtyunClientConfig: cfg, 105 httpClient: httpClient, 106 } 107 108 err := client.init() 109 if err != nil { 110 return nil, err 111 } 112 113 return client, nil 114 } 115 116 func (client *SCtyunClient) init() error { 117 err := client.fetchRegions() 118 if err != nil { 119 return err 120 } 121 122 return nil 123 } 124 125 func (client *SCtyunClient) fetchRegions() error { 126 resp, err := client.DoGet("/apiproxy/v3/order/getZoneConfig", map[string]string{}) 127 if err != nil { 128 return err 129 } 130 131 zones := []SZone{} 132 err = resp.Unmarshal(&zones, "returnObj") 133 if err != nil { 134 return err 135 } 136 137 regions := map[string]SRegion{} 138 for i := range zones { 139 zone := zones[i] 140 141 if len(client.projectId) > 0 && !strings.Contains(client.projectId, zone.RegionID) { 142 continue 143 } 144 145 if region, ok := regions[zone.RegionID]; !ok { 146 region = SRegion{ 147 client: client, 148 Description: zone.ZoneName, 149 ID: zone.RegionID, 150 ParentRegionID: zone.RegionID, 151 RegionName: zone.ZoneName, 152 izones: []cloudprovider.ICloudZone{&zone}, 153 } 154 155 zone.region = ®ion 156 zone.host = &SHost{zone: &zone} 157 regions[zone.RegionID] = region 158 } else { 159 zone.region = ®ion 160 zone.host = &SHost{zone: &zone} 161 region.izones = append(region.izones, &zone) 162 } 163 } 164 165 client.iregions = []cloudprovider.ICloudRegion{} 166 for k := range regions { 167 region := regions[k] 168 client.iregions = append(client.iregions, ®ion) 169 } 170 return nil 171 } 172 173 func (client *SCtyunClient) DoGet(apiName string, queries map[string]string) (jsonutils.JSONObject, error) { 174 return formRequest(client, httputils.GET, apiName, queries, nil) 175 } 176 177 func (client *SCtyunClient) DoPost(apiName string, params map[string]jsonutils.JSONObject) (jsonutils.JSONObject, error) { 178 return formRequest(client, httputils.POST, apiName, nil, params) 179 } 180 181 func getCustiomInfo(t string, crmBizId string, accountId string) jsonutils.JSONObject { 182 if len(t) == 0 { 183 return nil 184 } 185 186 customeInfo := jsonutils.NewDict() 187 //customeInfo.Set("name", jsonutils.NewString("")) 188 //customeInfo.Set("email", jsonutils.NewString("")) 189 //customeInfo.Set("phone", jsonutils.NewString("")) 190 indentity := jsonutils.NewDict() 191 if len(crmBizId) > 0 { 192 indentity.Set("crmBizId", jsonutils.NewString(crmBizId)) 193 } 194 195 if len(accountId) > 0 { 196 indentity.Set("accountId", jsonutils.NewString(accountId)) 197 } 198 199 if len(t) > 0 { 200 customeInfo.Set("type", jsonutils.NewString(t)) 201 customeInfo.Set("identity", indentity) 202 } 203 204 return customeInfo 205 } 206 207 func formRequest(client *SCtyunClient, method httputils.THttpMethod, apiName string, queries map[string]string, params map[string]jsonutils.JSONObject) (jsonutils.JSONObject, error) { 208 header := http.Header{} 209 // signer 210 { 211 content := []string{} 212 for k, v := range queries { 213 content = append(content, v) 214 header.Set(k, v) 215 } 216 217 for _, v := range params { 218 c, _ := v.GetString() 219 content = append(content, c) 220 } 221 222 // contentMd5 := fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(content, "\n")))) 223 // contentMd5 = base64.StdEncoding.EncodeToString([]byte(contentMd5)) 224 contentRaw := strings.Join(content, "\n") 225 contentMd5 := utils.GetMD5Base64([]byte(contentRaw)) 226 227 // EEE, d MMM yyyy HH:mm:ss z 228 // Mon, 2 Jan 2006 15:04:05 MST 229 requestDate := time.Now().Format("Mon, 2 Jan 2006 15:04:05 MST") 230 hashMac := hmac.New(sha1.New, []byte(client.accessSecret)) 231 hashRawString := strings.Join([]string{contentMd5, requestDate, apiName}, "\n") 232 hashMac.Write([]byte(hashRawString)) 233 hsum := base64.StdEncoding.EncodeToString(hashMac.Sum(nil)) 234 235 header.Set("accessKey", client.accessKey) 236 header.Set("contentMD5", contentMd5) 237 header.Set("requestDate", requestDate) 238 header.Set("hmac", hsum) 239 // 平台类型,整数类型,取值范围:2或3,传2表示2.0自营资源,传3表示3.0合营资源,该参数不需要加密。 240 header.Set("platform", "3") 241 // crm账号需要设置customInfo。目前发现只有VPC列表查询需要用到这个参数,其他的接口还没有发现此要求。为了简便对于crm 统一设置此项 242 if client.options != nil && len(client.options.CrmBizId) > 0 { 243 customInfo := getCustiomInfo("1", client.options.CrmBizId, "") 244 header.Set("customInfo", customInfo.String()) 245 } 246 } 247 248 var reqbody string 249 ioData := strings.NewReader("") 250 if method == httputils.GET { 251 for k, v := range queries { 252 header.Set(k, v) 253 } 254 } else { 255 datas := url.Values{} 256 for k, v := range params { 257 c, _ := v.GetString() 258 datas.Add(k, c) 259 } 260 261 reqbody = datas.Encode() 262 ioData = strings.NewReader(reqbody) 263 } 264 265 header.Set("Content-Length", strconv.FormatInt(int64(ioData.Len()), 10)) 266 header.Set("Content-Type", "application/x-www-form-urlencoded") 267 268 ctx := context.Background() 269 MAX_RETRY := 3 270 retry := 0 271 272 var err error 273 for retry < MAX_RETRY { 274 resp, err := httputils.Request( 275 client.httpClient, 276 ctx, 277 method, 278 CTYUN_API_HOST+apiName, 279 header, 280 ioData, 281 client.debug) 282 283 _, jsonResp, err := httputils.ParseJSONResponse(reqbody, resp, err, client.debug) 284 if err == nil { 285 if code, _ := jsonResp.Int("statusCode"); code != 800 { 286 if strings.Contains(jsonResp.String(), "NotFound") { 287 return nil, cloudprovider.ErrNotFound 288 } 289 290 return nil, &httputils.JSONClientError{Code: 400, Details: jsonResp.String()} 291 } 292 293 return jsonResp, nil 294 } 295 296 switch e := err.(type) { 297 case *httputils.JSONClientError: 298 if e.Code >= 499 { 299 time.Sleep(3 * time.Second) 300 retry += 1 301 continue 302 } else { 303 return nil, err 304 } 305 default: 306 return nil, err 307 } 308 } 309 310 return nil, fmt.Errorf("timeout for request: %s \n\n with params: %s", err, params) 311 } 312 313 func (self *SCtyunClient) GetIRegions() []cloudprovider.ICloudRegion { 314 return self.iregions 315 } 316 317 func (self *SCtyunClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { 318 subAccounts := make([]cloudprovider.SSubAccount, 0) 319 for i := range self.iregions { 320 iregion := self.iregions[i] 321 322 s := cloudprovider.SSubAccount{ 323 Name: fmt.Sprintf("%s-%s", self.cpcfg.Name, iregion.GetId()), 324 Account: fmt.Sprintf("%s/%s", self.accessKey, iregion.GetId()), 325 HealthStatus: api.CLOUD_PROVIDER_HEALTH_NORMAL, 326 } 327 328 subAccounts = append(subAccounts, s) 329 } 330 331 return subAccounts, nil 332 } 333 334 func (client *SCtyunClient) GetAccountId() string { 335 return client.accessKey 336 } 337 338 func (self *SCtyunClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { 339 for i := 0; i < len(self.iregions); i += 1 { 340 if self.iregions[i].GetGlobalId() == id { 341 return self.iregions[i], nil 342 } 343 } 344 345 return nil, cloudprovider.ErrNotFound 346 } 347 348 func (self *SCtyunClient) GetIProjects() ([]cloudprovider.ICloudProject, error) { 349 return nil, cloudprovider.ErrNotImplemented 350 } 351 352 func (self *SCtyunClient) GetAccessEnv() string { 353 return api.CLOUD_ACCESS_ENV_CTYUN_CHINA 354 } 355 356 func (self *SCtyunClient) GetRegions() []SRegion { 357 regions := make([]SRegion, len(self.iregions)) 358 for i := 0; i < len(regions); i += 1 { 359 region := self.iregions[i].(*SRegion) 360 regions[i] = *region 361 } 362 return regions 363 } 364 365 func (self *SCtyunClient) GetRegion(regionId string) *SRegion { 366 if len(regionId) == 0 { 367 regionId = CTYUN_DEFAULT_REGION 368 } 369 for i := 0; i < len(self.iregions); i += 1 { 370 if self.iregions[i].GetId() == regionId { 371 return self.iregions[i].(*SRegion) 372 } 373 } 374 return nil 375 } 376 377 func (self *SCtyunClient) GetCloudRegionExternalIdPrefix() string { 378 if len(self.projectId) > 0 { 379 return self.iregions[0].GetGlobalId() 380 } else { 381 return CLOUD_PROVIDER_CTYUN 382 } 383 } 384 385 func (self *SCtyunClient) GetCapabilities() []string { 386 caps := []string{ 387 // cloudprovider.CLOUD_CAPABILITY_PROJECT, 388 cloudprovider.CLOUD_CAPABILITY_COMPUTE, 389 cloudprovider.CLOUD_CAPABILITY_NETWORK, 390 cloudprovider.CLOUD_CAPABILITY_EIP, 391 // cloudprovider.CLOUD_CAPABILITY_LOADBALANCER, 392 // cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE, 393 // cloudprovider.CLOUD_CAPABILITY_RDS, 394 // cloudprovider.CLOUD_CAPABILITY_CACHE, 395 // cloudprovider.CLOUD_CAPABILITY_EVENT, 396 } 397 return caps 398 }