yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/ucloud/ucloud.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 ucloud 16 17 import ( 18 "bytes" 19 "fmt" 20 "io/ioutil" 21 "net/http" 22 "strings" 23 24 "yunion.io/x/jsonutils" 25 "yunion.io/x/log" 26 "yunion.io/x/pkg/errors" 27 28 api "yunion.io/x/cloudmux/pkg/apis/compute" 29 "yunion.io/x/cloudmux/pkg/cloudprovider" 30 ) 31 32 /* 33 UCLOUD 项目:https://docs.ucloud.cn/management_monitor/uproject/projects 34 项目可认为是云账户下承载资源的容器,当您注册一个UCloud云账户后,系统会默认创建一个项目,您属于的资源都落在此项目下。如您有新的业务要使用云服务,可创建一个新项目,并将新业务部署在新项目下,实现业务之间的网络与逻辑隔离。 35 36 1、项目之间默认网络与逻辑隔离,即项目A的主机无法绑定项目B的EIP,默认也无法与项目B的主机内网通信。但联通项目后,uhost、udb、umem可实现内网通信。 37 38 2、资源不能在项目间迁移,即项目A内的主机无法迁移至项目B,因其不在一个基础网络内,且逻辑上也是隔离的。但诸如自主镜像等静态资源,您可以提交工单申请迁移至其他项目。 39 40 3、只有云账户本身,才能删除项目,且必须是项目被没有资源、没有任何子成员、未与其他项目联通的情况下才可删除。 41 42 43 UCloud DiskType貌似也是一个奇葩的存在 44 // https://docs.ucloud.cn/api/uhost-api/disk_type 45 1.在主机创建查询接口中 DISK type 对应 CLOUD_SSD|CLOUD_NORMAL|... 46 2.在数据盘创建中对应 DataDisk|SSDDataDisk 47 3.在数据盘查询接口请求中对应 DataDisk|SystemDisk 。在结果中对应DataDisk|SSDDataDisk|SSDSystemDisk|SystemDisk 48 49 目前存在的问题: 50 1.很多国外区域都需要单独申请开通权限才能使用。onecloud有可能调度到未开通权限区域导致失败。 51 */ 52 53 const ( 54 CLOUD_PROVIDER_UCLOUD = api.CLOUD_PROVIDER_UCLOUD 55 CLOUD_PROVIDER_UCLOUD_CN = "UCloud" 56 57 UCLOUD_DEFAULT_REGION = "cn-bj2" 58 59 UCLOUD_API_VERSION = "2019-02-28" 60 ) 61 62 type UcloudClientConfig struct { 63 cpcfg cloudprovider.ProviderConfig 64 65 accessKeyId string 66 accessKeySecret string 67 projectId string 68 69 debug bool 70 } 71 72 func NewUcloudClientConfig(accessKeyId, accessKeySecret string) *UcloudClientConfig { 73 cfg := &UcloudClientConfig{ 74 accessKeyId: accessKeyId, 75 accessKeySecret: accessKeySecret, 76 } 77 return cfg 78 } 79 80 func (cfg *UcloudClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *UcloudClientConfig { 81 cfg.cpcfg = cpcfg 82 return cfg 83 } 84 85 func (cfg *UcloudClientConfig) ProjectId(projectId string) *UcloudClientConfig { 86 cfg.projectId = projectId 87 return cfg 88 } 89 90 func (cfg *UcloudClientConfig) Debug(debug bool) *UcloudClientConfig { 91 cfg.debug = debug 92 return cfg 93 } 94 95 type SUcloudClient struct { 96 *UcloudClientConfig 97 98 iregions []cloudprovider.ICloudRegion 99 iBuckets []cloudprovider.ICloudBucket 100 101 httpClient *http.Client 102 } 103 104 // 进行资源操作时参数account 对应数据库cloudprovider表中的account字段,由accessKey和projectID两部分组成,通过"/"分割。 105 // 初次导入Subaccount时,参数account对应cloudaccounts表中的account字段,即accesskey。此时projectID为空,只能进行同步子账号(项目)、查询region列表等projectId无关的操作。 106 func NewUcloudClient(cfg *UcloudClientConfig) (*SUcloudClient, error) { 107 httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient() 108 ts, _ := httpClient.Transport.(*http.Transport) 109 httpClient.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response), error) { 110 if cfg.cpcfg.ReadOnly { 111 if req.ContentLength > 0 { 112 body, err := ioutil.ReadAll(req.Body) 113 if err != nil { 114 return nil, errors.Wrapf(err, "ioutil.ReadAll") 115 } 116 req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 117 obj, err := jsonutils.Parse(body) 118 if err != nil { 119 return nil, errors.Wrapf(err, "Parse request body") 120 } 121 action, err := obj.GetString("Action") 122 if err != nil { 123 return nil, errors.Wrapf(err, "Get request action") 124 } 125 for _, prefix := range []string{"Get", "Describe", "List"} { 126 if strings.HasPrefix(action, prefix) { 127 return nil, nil 128 } 129 } 130 return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) 131 } 132 } 133 return nil, nil 134 }) 135 client := SUcloudClient{ 136 UcloudClientConfig: cfg, 137 httpClient: httpClient, 138 } 139 140 err := client.fetchRegions() 141 if err != nil { 142 return nil, err 143 } 144 145 err = client.fetchBuckets() 146 if err != nil { 147 return nil, err 148 } 149 return &client, nil 150 } 151 152 func (self *SUcloudClient) UpdateAccount(accessKey, secret string) error { 153 if self.accessKeyId != accessKey || self.accessKeySecret != secret { 154 self.accessKeyId = accessKey 155 self.accessKeySecret = secret 156 return self.fetchRegions() 157 } else { 158 return nil 159 } 160 } 161 162 func (self *SUcloudClient) commonParams(params SParams, action string) (string, SParams) { 163 resultKey, exists := UCLOUD_API_RESULT_KEYS[action] 164 if !exists || len(resultKey) == 0 { 165 // default key for describe actions 166 if strings.HasPrefix(action, "Describe") { 167 resultKey = "DataSet" 168 } 169 } 170 171 if len(self.projectId) > 0 { 172 params.Set("ProjectId", self.projectId) 173 } 174 params.Set("PublicKey", self.accessKeyId) 175 176 return resultKey, params 177 } 178 179 func (self *SUcloudClient) DoListAll(action string, params SParams, result interface{}) error { 180 resultKey, params := self.commonParams(params, action) 181 return DoListAll(self, action, params, resultKey, result) 182 } 183 184 func (self *SUcloudClient) DoListPart(action string, limit int, offset int, params SParams, result interface{}) (int, int, error) { 185 resultKey, params := self.commonParams(params, action) 186 params.SetPagination(limit, offset) 187 return doListPart(self, action, params, resultKey, result) 188 } 189 190 func (self *SUcloudClient) DoAction(action string, params SParams, result interface{}) error { 191 resultKey, params := self.commonParams(params, action) 192 err := DoAction(self, action, params, resultKey, result) 193 if err != nil { 194 return err 195 } 196 197 return nil 198 } 199 200 func (self *SUcloudClient) fetchRegions() error { 201 type Region struct { 202 RegionID int64 `json:"RegionId"` 203 RegionName string `json:"RegionName"` 204 IsDefault bool `json:"IsDefault"` 205 BitMaps string `json:"BitMaps"` 206 Region string `json:"Region"` 207 Zone string `json:"Zone"` 208 } 209 210 params := NewUcloudParams() 211 regions := make([]Region, 0) 212 err := self.DoListAll("GetRegion", params, ®ions) 213 if err != nil { 214 return err 215 } 216 217 regionSet := make(map[string]string, 0) 218 for _, region := range regions { 219 regionSet[region.Region] = region.Region 220 } 221 222 sregions := make([]SRegion, len(regionSet)) 223 self.iregions = make([]cloudprovider.ICloudRegion, len(regionSet)) 224 i := 0 225 for regionId := range regionSet { 226 sregions[i].client = self 227 sregions[i].RegionID = regionId 228 self.iregions[i] = &sregions[i] 229 i += 1 230 } 231 232 return nil 233 } 234 235 func (client *SUcloudClient) invalidateIBuckets() { 236 client.iBuckets = nil 237 } 238 239 func (client *SUcloudClient) getIBuckets() ([]cloudprovider.ICloudBucket, error) { 240 if client.iBuckets == nil { 241 err := client.fetchBuckets() 242 if err != nil { 243 return nil, errors.Wrap(err, "fetchBuckets") 244 } 245 } 246 return client.iBuckets, nil 247 } 248 249 func (client *SUcloudClient) fetchBuckets() error { 250 buckets := make([]SBucket, 0) 251 offset := 0 252 limit := 50 253 for { 254 parts, err := client.listBuckets("", offset, limit) 255 if err != nil { 256 return errors.Wrap(err, "client.listBuckets") 257 } 258 if len(parts) > 0 { 259 buckets = append(buckets, parts...) 260 } 261 if len(parts) < limit { 262 break 263 } else { 264 offset += limit 265 } 266 } 267 ret := make([]cloudprovider.ICloudBucket, 0) 268 for i := range buckets { 269 region, err := client.getIRegionByRegionId(buckets[i].Region) 270 if err != nil { 271 log.Errorf("fail to find iregion %s", buckets[i].Region) 272 continue 273 } 274 buckets[i].region = region.(*SRegion) 275 ret = append(ret, &buckets[i]) 276 } 277 278 client.iBuckets = ret 279 280 return nil 281 } 282 283 func (self *SUcloudClient) GetRegions() []SRegion { 284 regions := make([]SRegion, len(self.iregions)) 285 for i := 0; i < len(regions); i += 1 { 286 region := self.iregions[i].(*SRegion) 287 regions[i] = *region 288 } 289 return regions 290 } 291 292 func (self *SUcloudClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { 293 projects, err := self.FetchProjects() 294 if err != nil { 295 return nil, err 296 } 297 298 subAccounts := make([]cloudprovider.SSubAccount, 0) 299 for _, project := range projects { 300 subAccount := cloudprovider.SSubAccount{} 301 subAccount.Name = fmt.Sprintf("%s-%s", self.cpcfg.Name, project.ProjectName) 302 // ucloud账号ID中可能包含/。因此使用::作为分割符号 303 subAccount.Account = fmt.Sprintf("%s::%s", self.accessKeyId, project.ProjectID) 304 subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL 305 306 subAccounts = append(subAccounts, subAccount) 307 } 308 309 return subAccounts, nil 310 } 311 312 func (self *SUcloudClient) GetAccountId() string { 313 return "" // no account ID found for ucloud 314 } 315 316 func (self *SUcloudClient) GetIRegions() []cloudprovider.ICloudRegion { 317 return self.iregions 318 } 319 320 func removeDigit(idstr string) string { 321 for len(idstr) > 0 && idstr[len(idstr)-1] >= '0' && idstr[len(idstr)-1] <= '9' { 322 idstr = idstr[:len(idstr)-1] 323 } 324 return idstr 325 } 326 327 func (self *SUcloudClient) getIRegionByRegionId(id string) (cloudprovider.ICloudRegion, error) { 328 for i := 0; i < len(self.iregions); i += 1 { 329 if self.iregions[i].GetId() == id { 330 return self.iregions[i], nil 331 } 332 } 333 // retry 334 for i := 0; i < len(self.iregions); i += 1 { 335 rid := removeDigit(self.iregions[i].GetId()) 336 rid2 := removeDigit(id) 337 if rid == rid2 { 338 return self.iregions[i], nil 339 } 340 } 341 return nil, cloudprovider.ErrNotFound 342 } 343 344 func (self *SUcloudClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { 345 for i := 0; i < len(self.iregions); i += 1 { 346 if self.iregions[i].GetGlobalId() == id { 347 return self.iregions[i], nil 348 } 349 } 350 return nil, cloudprovider.ErrNotFound 351 } 352 353 func (self *SUcloudClient) GetRegion(regionId string) *SRegion { 354 if len(regionId) == 0 { 355 regionId = UCLOUD_DEFAULT_REGION 356 } 357 for i := 0; i < len(self.iregions); i += 1 { 358 if self.iregions[i].GetId() == regionId { 359 return self.iregions[i].(*SRegion) 360 } 361 } 362 return nil 363 } 364 365 func (self *SUcloudClient) GetIHostById(id string) (cloudprovider.ICloudHost, error) { 366 for i := 0; i < len(self.iregions); i += 1 { 367 ihost, err := self.iregions[i].GetIHostById(id) 368 if err == nil { 369 return ihost, nil 370 } else if errors.Cause(err) != cloudprovider.ErrNotFound { 371 return nil, err 372 } 373 } 374 return nil, cloudprovider.ErrNotFound 375 } 376 377 func (self *SUcloudClient) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) { 378 for i := 0; i < len(self.iregions); i += 1 { 379 ihost, err := self.iregions[i].GetIVpcById(id) 380 if err == nil { 381 return ihost, nil 382 } else if errors.Cause(err) != cloudprovider.ErrNotFound { 383 return nil, err 384 } 385 } 386 return nil, cloudprovider.ErrNotFound 387 } 388 389 func (self *SUcloudClient) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { 390 for i := 0; i < len(self.iregions); i += 1 { 391 ihost, err := self.iregions[i].GetIStorageById(id) 392 if err == nil { 393 return ihost, nil 394 } else if errors.Cause(err) != cloudprovider.ErrNotFound { 395 return nil, err 396 } 397 } 398 return nil, cloudprovider.ErrNotFound 399 } 400 401 func (self *SUcloudClient) GetCapabilities() []string { 402 caps := []string{ 403 // cloudprovider.CLOUD_CAPABILITY_PROJECT, 404 cloudprovider.CLOUD_CAPABILITY_COMPUTE, 405 cloudprovider.CLOUD_CAPABILITY_NETWORK, 406 cloudprovider.CLOUD_CAPABILITY_EIP, 407 // cloudprovider.CLOUD_CAPABILITY_LOADBALANCER, 408 // cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE, 409 // cloudprovider.CLOUD_CAPABILITY_RDS, 410 // cloudprovider.CLOUD_CAPABILITY_CACHE, 411 // cloudprovider.CLOUD_CAPABILITY_EVENT, 412 } 413 return caps 414 }