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] = &region
   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  }