yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/openstack/openstack.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 openstack 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "net/http" 22 "net/url" 23 "strings" 24 25 "yunion.io/x/jsonutils" 26 "yunion.io/x/log" 27 "yunion.io/x/pkg/errors" 28 "yunion.io/x/pkg/utils" 29 30 api "yunion.io/x/cloudmux/pkg/apis/compute" 31 "yunion.io/x/cloudmux/pkg/cloudprovider" 32 "yunion.io/x/onecloud/pkg/httperrors" 33 "yunion.io/x/onecloud/pkg/mcclient" 34 "yunion.io/x/cloudmux/pkg/multicloud" 35 "yunion.io/x/onecloud/pkg/util/httputils" 36 "yunion.io/x/onecloud/pkg/util/version" 37 ) 38 39 const ( 40 CLOUD_PROVIDER_OPENSTACK = api.CLOUD_PROVIDER_OPENSTACK 41 OPENSTACK_DEFAULT_REGION = "RegionOne" 42 43 OPENSTACK_SERVICE_COMPUTE = "compute" 44 OPENSTACK_SERVICE_NETWORK = "network" 45 OPENSTACK_SERVICE_IDENTITY = "identity" 46 OPENSTACK_SERVICE_VOLUMEV3 = "volumev3" 47 OPENSTACK_SERVICE_VOLUMEV2 = "volumev2" 48 OPENSTACK_SERVICE_VOLUME = "volume" 49 OPENSTACK_SERVICE_IMAGE = "image" 50 OPENSTACK_SERVICE_LOADBALANCER = "load-balancer" 51 52 ErrNoEndpoint = errors.Error("no valid endpoint") 53 ) 54 55 type OpenstackClientConfig struct { 56 cpcfg cloudprovider.ProviderConfig 57 58 authURL string 59 username string 60 password string 61 project string 62 projectDomain string 63 64 domainName string 65 endpointType string 66 67 debug bool 68 } 69 70 func NewOpenstackClientConfig(authURL, username, password, project, projectDomain string) *OpenstackClientConfig { 71 cfg := &OpenstackClientConfig{ 72 authURL: authURL, 73 username: username, 74 password: password, 75 project: project, 76 projectDomain: projectDomain, 77 } 78 return cfg 79 } 80 81 func (cfg *OpenstackClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *OpenstackClientConfig { 82 cfg.cpcfg = cpcfg 83 return cfg 84 } 85 86 func (cfg *OpenstackClientConfig) DomainName(domainName string) *OpenstackClientConfig { 87 cfg.domainName = domainName 88 return cfg 89 } 90 91 func (cfg *OpenstackClientConfig) EndpointType(endpointType string) *OpenstackClientConfig { 92 cfg.endpointType = endpointType 93 return cfg 94 } 95 96 func (cfg *OpenstackClientConfig) Debug(debug bool) *OpenstackClientConfig { 97 cfg.debug = debug 98 return cfg 99 } 100 101 type SOpenStackClient struct { 102 *OpenstackClientConfig 103 104 tokenCredential mcclient.TokenCredential 105 iregions []cloudprovider.ICloudRegion 106 107 defaultRegionName string 108 109 projects []SProject 110 } 111 112 func NewOpenStackClient(cfg *OpenstackClientConfig) (*SOpenStackClient, error) { 113 cli := &SOpenStackClient{ 114 OpenstackClientConfig: cfg, 115 } 116 err := cli.fetchToken() 117 if err != nil { 118 return nil, err 119 } 120 return cli, cli.fetchRegions() 121 } 122 123 func (cli *SOpenStackClient) getDefaultRegionName() string { 124 return cli.defaultRegionName 125 } 126 127 func (cli *SOpenStackClient) getProjectToken(projectId, projectName string) (mcclient.TokenCredential, error) { 128 client := cli.getDefaultClient() 129 tokenCredential, err := client.Authenticate(cli.username, cli.password, cli.domainName, projectName, cli.projectDomain) 130 if err != nil { 131 e, ok := err.(*httputils.JSONClientError) 132 if ok { 133 // 避免有泄漏密码的风险 134 e.Request.Body = nil 135 return nil, errors.Wrap(e, "Authenticate") 136 } 137 return nil, errors.Wrap(err, "Authenticate") 138 } 139 return tokenCredential, nil 140 } 141 142 func (cli *SOpenStackClient) GetCloudRegionExternalIdPrefix() string { 143 return fmt.Sprintf("%s/%s/", CLOUD_PROVIDER_OPENSTACK, cli.cpcfg.Id) 144 } 145 146 func (cli *SOpenStackClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { 147 subAccount := cloudprovider.SSubAccount{ 148 Account: fmt.Sprintf("%s/%s", cli.project, cli.username), 149 Name: cli.cpcfg.Name, 150 151 HealthStatus: api.CLOUD_PROVIDER_HEALTH_NORMAL, 152 } 153 if len(cli.domainName) > 0 { 154 subAccount.Account = fmt.Sprintf("%s/%s", subAccount.Account, cli.domainName) 155 } 156 return []cloudprovider.SSubAccount{subAccount}, nil 157 } 158 159 func (cli *SOpenStackClient) fetchRegions() error { 160 regions := cli.tokenCredential.GetRegions() 161 cli.iregions = make([]cloudprovider.ICloudRegion, len(regions)) 162 for i := 0; i < len(regions); i++ { 163 region := SRegion{client: cli, Name: regions[i]} 164 cli.iregions[i] = ®ion 165 cli.defaultRegionName = regions[0] 166 } 167 return nil 168 } 169 170 type OpenstackError struct { 171 httputils.JSONClientError 172 } 173 174 func (ce *OpenstackError) ParseErrorFromJsonResponse(statusCode int, body jsonutils.JSONObject) error { 175 if body != nil { 176 body.Unmarshal(ce) 177 } 178 if ce.Code == 0 { 179 ce.Code = statusCode 180 } 181 if len(ce.Details) == 0 && body != nil { 182 ce.Details = body.String() 183 } 184 if len(ce.Class) == 0 { 185 ce.Class = http.StatusText(statusCode) 186 } 187 if statusCode == 404 { 188 return errors.Wrap(cloudprovider.ErrNotFound, ce.Error()) 189 } 190 return ce 191 } 192 193 type sApiVersion struct { 194 MinVersion string 195 Version string 196 } 197 198 type sApiVersions struct { 199 Versions []sApiVersion 200 Version sApiVersion 201 } 202 203 func (v *sApiVersions) GetMaxVersion() string { 204 maxVersion := v.Version.Version 205 for _, _version := range v.Versions { 206 if version.GT(_version.Version, maxVersion) { 207 maxVersion = _version.Version 208 } 209 } 210 return maxVersion 211 } 212 213 func (cli *SOpenStackClient) getApiVerion(token mcclient.TokenCredential, url string, debug bool) (string, error) { 214 client := httputils.NewJsonClient(cli.getDefaultClient().HttpClient()) 215 req := httputils.NewJsonRequest(httputils.THttpMethod("GET"), strings.TrimSuffix(url, token.GetTenantId()), nil) 216 header := http.Header{} 217 header.Set("X-Auth-Token", token.GetTokenString()) 218 req.SetHeader(header) 219 oe := &OpenstackError{} 220 _, resp, err := client.Send(context.Background(), req, oe, debug) 221 if err != nil { 222 return "", errors.Wrap(err, "get api version") 223 } 224 versions := &sApiVersions{} 225 resp.Unmarshal(&versions) 226 return versions.GetMaxVersion(), nil 227 } 228 229 func (cli *SOpenStackClient) GetMaxVersion(region, service string) (string, error) { 230 serviceUrl, err := cli.tokenCredential.GetServiceURL(service, region, "", cli.endpointType) 231 if err != nil { 232 return "", errors.Wrapf(err, "GetServiceURL(%s, %s, %s)", service, region, cli.endpointType) 233 } 234 header := http.Header{} 235 header.Set("X-Auth-Token", cli.tokenCredential.GetTokenString()) 236 return cli.getApiVerion(cli.tokenCredential, serviceUrl, cli.debug) 237 } 238 239 func (cli *SOpenStackClient) jsonReuest(token mcclient.TokenCredential, service, region, endpointType string, method httputils.THttpMethod, resource string, query url.Values, body interface{}, debug bool) (jsonutils.JSONObject, error) { 240 serviceUrl, err := token.GetServiceURL(service, region, "", endpointType) 241 if err != nil { 242 return nil, errors.Wrapf(err, "GetServiceURL(%s, %s, %s)", service, region, endpointType) 243 } 244 header := http.Header{} 245 header.Set("X-Auth-Token", token.GetTokenString()) 246 apiVersion := "" 247 if !utils.IsInStringArray(service, []string{OPENSTACK_SERVICE_IMAGE, OPENSTACK_SERVICE_IDENTITY}) { 248 apiVersion, err = cli.getApiVerion(token, serviceUrl, debug) 249 if err != nil { 250 log.Errorf("get service %s api version error: %v", service, err) 251 } 252 } 253 if len(apiVersion) > 0 { 254 switch service { 255 case OPENSTACK_SERVICE_COMPUTE: 256 header.Set("X-Openstack-Nova-API-Version", apiVersion) 257 case OPENSTACK_SERVICE_IMAGE: 258 header.Set("X-Openstack-Glance-API-Version", apiVersion) 259 case OPENSTACK_SERVICE_VOLUME, OPENSTACK_SERVICE_VOLUMEV2, OPENSTACK_SERVICE_VOLUMEV3: 260 header.Set("Openstack-API-Version", fmt.Sprintf("volume %s", apiVersion)) 261 case OPENSTACK_SERVICE_NETWORK: 262 header.Set("X-Openstack-Neutron-API-Version", apiVersion) 263 case OPENSTACK_SERVICE_IDENTITY: 264 header.Set("X-Openstack-Identity-API-Version", apiVersion) 265 } 266 } 267 268 if service == OPENSTACK_SERVICE_IDENTITY { 269 if strings.HasSuffix(serviceUrl, "/v3/") { 270 serviceUrl = strings.TrimSuffix(serviceUrl, "/v3/") 271 } else if strings.HasSuffix(serviceUrl, "/v3") { 272 serviceUrl = strings.TrimSuffix(serviceUrl, "/v3") 273 } 274 } 275 276 requestUrl := resource 277 if !strings.HasPrefix(resource, serviceUrl) { 278 requestUrl = fmt.Sprintf("%s/%s", strings.TrimSuffix(serviceUrl, "/"), strings.TrimPrefix(resource, "/")) 279 } 280 281 if query != nil && len(query) > 0 { 282 requestUrl = fmt.Sprintf("%s?%s", requestUrl, query.Encode()) 283 } 284 285 return cli._jsonRequest(method, requestUrl, header, body, debug) 286 } 287 288 func (cli *SOpenStackClient) _jsonRequest(method httputils.THttpMethod, url string, header http.Header, params interface{}, debug bool) (jsonutils.JSONObject, error) { 289 client := httputils.NewJsonClient(cli.getDefaultClient().HttpClient()) 290 req := httputils.NewJsonRequest(method, url, params) 291 req.SetHeader(header) 292 oe := &OpenstackError{} 293 _, resp, err := client.Send(context.Background(), req, oe, debug) 294 return resp, err 295 } 296 297 func (cli *SOpenStackClient) ecsRequest(region string, method httputils.THttpMethod, resource string, query url.Values, body interface{}) (jsonutils.JSONObject, error) { 298 token := cli.tokenCredential 299 if method == httputils.POST && query != nil && len(query.Get("project_id")) > 0 { 300 projectId := query.Get("project_id") 301 var err error 302 token, err = cli.getProjectTokenCredential(projectId) 303 if err != nil { 304 return nil, errors.Wrapf(err, "getProjectTokenCredential(%s)", projectId) 305 } 306 } 307 return cli.jsonReuest(token, OPENSTACK_SERVICE_COMPUTE, region, cli.endpointType, method, resource, query, body, cli.debug) 308 } 309 310 func (cli *SOpenStackClient) ecsCreate(projectId, region, resource string, body interface{}) (jsonutils.JSONObject, error) { 311 token := cli.tokenCredential 312 if len(projectId) > 0 { 313 var err error 314 token, err = cli.getProjectTokenCredential(projectId) 315 if err != nil { 316 return nil, errors.Wrapf(err, "getProjectTokenCredential(%s)", projectId) 317 } 318 } 319 return cli.jsonReuest(token, OPENSTACK_SERVICE_COMPUTE, region, cli.endpointType, httputils.POST, resource, nil, body, cli.debug) 320 } 321 322 func (cli *SOpenStackClient) ecsDo(projectId, region, resource string, body interface{}) (jsonutils.JSONObject, error) { 323 token := cli.tokenCredential 324 if len(projectId) > 0 { 325 var err error 326 token, err = cli.getProjectTokenCredential(projectId) 327 if err != nil { 328 return nil, errors.Wrapf(err, "getProjectTokenCredential(%s)", projectId) 329 } 330 } 331 return cli.jsonReuest(token, OPENSTACK_SERVICE_COMPUTE, region, cli.endpointType, httputils.POST, resource, nil, body, cli.debug) 332 } 333 334 func (cli *SOpenStackClient) iamRequest(region string, method httputils.THttpMethod, resource string, query url.Values, body interface{}) (jsonutils.JSONObject, error) { 335 return cli.jsonReuest(cli.tokenCredential, OPENSTACK_SERVICE_IDENTITY, region, cli.endpointType, method, resource, query, body, cli.debug) 336 } 337 338 func (cli *SOpenStackClient) vpcRequest(region string, method httputils.THttpMethod, resource string, query url.Values, body interface{}) (jsonutils.JSONObject, error) { 339 return cli.jsonReuest(cli.tokenCredential, OPENSTACK_SERVICE_NETWORK, region, cli.endpointType, method, resource, query, body, cli.debug) 340 } 341 342 func (cli *SOpenStackClient) imageRequest(region string, method httputils.THttpMethod, resource string, query url.Values, body interface{}) (jsonutils.JSONObject, error) { 343 return cli.jsonReuest(cli.tokenCredential, OPENSTACK_SERVICE_IMAGE, region, cli.endpointType, method, resource, query, body, cli.debug) 344 } 345 346 func (cli *SOpenStackClient) bsRequest(region string, method httputils.THttpMethod, resource string, query url.Values, body interface{}) (jsonutils.JSONObject, error) { 347 for _, service := range []string{OPENSTACK_SERVICE_VOLUMEV3, OPENSTACK_SERVICE_VOLUMEV2, OPENSTACK_SERVICE_VOLUME} { 348 _, err := cli.tokenCredential.GetServiceURL(service, region, "", cli.endpointType) 349 if err == nil { 350 return cli.jsonReuest(cli.tokenCredential, service, region, cli.endpointType, method, resource, query, body, cli.debug) 351 } 352 } 353 return nil, errors.Wrap(ErrNoEndpoint, "cinder service") 354 } 355 356 func (cli *SOpenStackClient) bsCreate(projectId, region, resource string, body interface{}) (jsonutils.JSONObject, error) { 357 token := cli.tokenCredential 358 if len(projectId) > 0 { 359 var err error 360 token, err = cli.getProjectTokenCredential(projectId) 361 if err != nil { 362 return nil, errors.Wrapf(err, "getProjectTokenCredential(%s)", projectId) 363 } 364 } 365 for _, service := range []string{OPENSTACK_SERVICE_VOLUMEV3, OPENSTACK_SERVICE_VOLUMEV2, OPENSTACK_SERVICE_VOLUME} { 366 _, err := token.GetServiceURL(service, region, "", cli.endpointType) 367 if err == nil { 368 return cli.jsonReuest(token, service, region, cli.endpointType, httputils.POST, resource, nil, body, cli.debug) 369 } 370 } 371 return nil, errors.Wrap(ErrNoEndpoint, "cinder service") 372 } 373 374 func (cli *SOpenStackClient) imageUpload(region, url string, size int64, body io.Reader, callback func(progress float32)) (*http.Response, error) { 375 header := http.Header{} 376 header.Set("Content-Type", "application/octet-stream") 377 session := cli.getDefaultSession(region) 378 reader := multicloud.NewProgress(size, 99, body, callback) 379 return session.RawRequest(OPENSTACK_SERVICE_IMAGE, "", httputils.PUT, url, header, reader) 380 } 381 382 func (cli *SOpenStackClient) lbRequest(region string, method httputils.THttpMethod, resource string, query url.Values, body interface{}) (jsonutils.JSONObject, error) { 383 return cli.jsonReuest(cli.tokenCredential, OPENSTACK_SERVICE_LOADBALANCER, region, cli.endpointType, method, resource, query, body, cli.debug) 384 } 385 386 func (cli *SOpenStackClient) fetchToken() error { 387 if cli.tokenCredential != nil { 388 return nil 389 } 390 var err error 391 cli.tokenCredential, err = cli.getDefaultToken() 392 if err != nil { 393 return err 394 } 395 return cli.checkEndpointType() 396 } 397 398 func (cli *SOpenStackClient) checkEndpointType() error { 399 for _, regionName := range cli.tokenCredential.GetRegions() { 400 _, err := cli.tokenCredential.GetServiceURL(OPENSTACK_SERVICE_COMPUTE, regionName, "", cli.endpointType) 401 if err == nil { 402 return nil 403 } 404 for _, endpointType := range []string{"internal", "admin", "public"} { 405 _, err = cli.tokenCredential.GetServiceURL(OPENSTACK_SERVICE_COMPUTE, regionName, "", endpointType) 406 if err == nil { 407 cli.endpointType = endpointType 408 return nil 409 } 410 } 411 } 412 return errors.Errorf("failed to find right endpoint type for compute service") 413 } 414 415 func (cli *SOpenStackClient) getDefaultSession(regionName string) *mcclient.ClientSession { 416 if len(regionName) == 0 { 417 regionName = cli.getDefaultRegionName() 418 } 419 client := cli.getDefaultClient() 420 return client.NewSession(context.Background(), regionName, "", cli.endpointType, cli.tokenCredential) 421 } 422 423 func (cli *SOpenStackClient) getDefaultClient() *mcclient.Client { 424 client := mcclient.NewClient(cli.authURL, 5, cli.debug, false, "", "") 425 client.SetHttpTransportProxyFunc(cli.cpcfg.ProxyFunc) 426 _client := client.GetClient() 427 ts, _ := _client.Transport.(*http.Transport) 428 _client.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response), error) { 429 if cli.cpcfg.ReadOnly { 430 if req.Method == "GET" || req.Method == "HEAD" { 431 return nil, nil 432 } 433 // 认证 434 if req.Method == "POST" && strings.HasSuffix(req.URL.Path, "auth/tokens") { 435 return nil, nil 436 } 437 return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) 438 } 439 return nil, nil 440 }) 441 442 return client 443 } 444 445 func (cli *SOpenStackClient) getDefaultToken() (mcclient.TokenCredential, error) { 446 client := cli.getDefaultClient() 447 token, err := client.Authenticate(cli.username, cli.password, cli.domainName, cli.project, cli.projectDomain) 448 if err != nil { 449 if e, ok := err.(*httputils.JSONClientError); ok { 450 if e.Class == "Unauthorized" { 451 return nil, errors.Wrapf(httperrors.ErrInvalidAccessKey, err.Error()) 452 } 453 } 454 return nil, errors.Wrap(err, "Authenticate") 455 } 456 return token, nil 457 } 458 459 func (cli *SOpenStackClient) getProjectTokenCredential(projectId string) (mcclient.TokenCredential, error) { 460 project, err := cli.GetProject(projectId) 461 if err != nil { 462 return nil, errors.Wrapf(err, "GetProject(%s)", projectId) 463 } 464 return cli.getProjectToken(project.Id, project.Name) 465 } 466 467 func (cli *SOpenStackClient) GetRegion(regionId string) *SRegion { 468 for i := 0; i < len(cli.iregions); i++ { 469 if cli.iregions[i].GetId() == regionId { 470 return cli.iregions[i].(*SRegion) 471 } 472 } 473 return nil 474 } 475 476 func (cli *SOpenStackClient) GetIRegions() []cloudprovider.ICloudRegion { 477 return cli.iregions 478 } 479 480 func (cli *SOpenStackClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { 481 for i := 0; i < len(cli.iregions); i++ { 482 if cli.iregions[i].GetGlobalId() == id { 483 return cli.iregions[i], nil 484 } 485 } 486 return nil, cloudprovider.ErrNotFound 487 } 488 489 func (cli *SOpenStackClient) GetRegions() []SRegion { 490 regions := make([]SRegion, len(cli.iregions)) 491 for i := 0; i < len(regions); i++ { 492 region := cli.iregions[i].(*SRegion) 493 regions[i] = *region 494 } 495 return regions 496 } 497 498 func (cli *SOpenStackClient) fetchProjects() error { 499 var err error 500 cli.projects, err = cli.GetProjects() 501 if err != nil { 502 return errors.Wrap(err, "GetProjects") 503 } 504 return nil 505 } 506 507 func (cli *SOpenStackClient) GetIProjects() ([]cloudprovider.ICloudProject, error) { 508 err := cli.fetchProjects() 509 if err != nil { 510 return nil, errors.Wrap(err, "fetchProjects") 511 } 512 iprojects := []cloudprovider.ICloudProject{} 513 for i := 0; i < len(cli.projects); i++ { 514 cli.projects[i].client = cli 515 iprojects = append(iprojects, &cli.projects[i]) 516 } 517 return iprojects, nil 518 } 519 520 func (cli *SOpenStackClient) GetProject(id string) (*SProject, error) { 521 err := cli.fetchProjects() 522 if err != nil { 523 return nil, errors.Wrap(err, "fetchProjects") 524 } 525 for i := 0; i < len(cli.projects); i++ { 526 if cli.projects[i].Id == id { 527 return &cli.projects[i], nil 528 } 529 } 530 return nil, cloudprovider.ErrNotFound 531 } 532 533 func (cli *SOpenStackClient) CreateIProject(name string) (cloudprovider.ICloudProject, error) { 534 return cli.CreateProject(name, "") 535 } 536 537 func (cli *SOpenStackClient) CreateProject(name, desc string) (*SProject, error) { 538 params := map[string]interface{}{ 539 "project": map[string]interface{}{ 540 "name": name, 541 "domain_id": cli.tokenCredential.GetProjectDomainId(), 542 "enabled": true, 543 "description": desc, 544 }, 545 } 546 resp, err := cli.iamRequest(cli.getDefaultRegionName(), httputils.POST, "/v3/projects", nil, params) 547 if err != nil { 548 return nil, errors.Wrap(err, "iamRequest") 549 } 550 project := SProject{client: cli} 551 err = resp.Unmarshal(&project, "project") 552 if err != nil { 553 return nil, errors.Wrap(err, "result.Unmarshal") 554 } 555 err = cli.AssignRoleToUserOnProject(cli.tokenCredential.GetUserId(), project.Id, "admin") 556 if err != nil { 557 return nil, errors.Wrap(err, "AssignRoleToUserOnProject") 558 } 559 return &project, nil 560 } 561 562 func (self *SOpenStackClient) GetCapabilities() []string { 563 caps := []string{ 564 cloudprovider.CLOUD_CAPABILITY_PROJECT, 565 cloudprovider.CLOUD_CAPABILITY_COMPUTE, 566 cloudprovider.CLOUD_CAPABILITY_NETWORK, 567 cloudprovider.CLOUD_CAPABILITY_EIP, 568 cloudprovider.CLOUD_CAPABILITY_LOADBALANCER, 569 cloudprovider.CLOUD_CAPABILITY_QUOTA + cloudprovider.READ_ONLY_SUFFIX, 570 // cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE, 571 // cloudprovider.CLOUD_CAPABILITY_RDS, 572 // cloudprovider.CLOUD_CAPABILITY_CACHE, 573 // cloudprovider.CLOUD_CAPABILITY_EVENT, 574 } 575 return caps 576 }