diff --git a/pkg/apis/compute/regiondata.go b/pkg/apis/compute/regiondata.go index 371713244..8afb1f74b 100644 --- a/pkg/apis/compute/regiondata.go +++ b/pkg/apis/compute/regiondata.go @@ -86,6 +86,12 @@ var ( City: CITY_NEI_MENG_GU, CountryCode: COUNTRY_CODE_CN, } + RegionChangchun = cloudprovider.SGeographicInfo{ + Latitude: 43.87120919729674, + Longitude: 125.3111129463539, + City: CITY_CHANG_CHUN, + CountryCode: COUNTRY_CODE_CN, + } RegionQingdao = cloudprovider.SGeographicInfo{ Latitude: 36.067, Longitude: 120.383, @@ -182,6 +188,31 @@ var ( City: CITY_NAN_NING, CountryCode: COUNTRY_CODE_CN, } + // 郑州、苏州等周边城市已存在,下面补充部分在 ecloud 等云厂商中常用但尚未建 RegionXXX 的城市 + RegionJiNan = cloudprovider.SGeographicInfo{ + Latitude: 36.64889911073425, + Longitude: 117.11905617575435, + City: CITY_JI_NAM, + CountryCode: COUNTRY_CODE_CN, + } + RegionXiangyang = cloudprovider.SGeographicInfo{ + Latitude: 32.009075721852206, + Longitude: 112.13485327119795, + City: CITY_XIANG_YANG, + CountryCode: COUNTRY_CODE_CN, + } + RegionShijiazhuang = cloudprovider.SGeographicInfo{ + Latitude: 38.044044256466684, + Longitude: 114.50225031469532, + City: CITY_SHI_JIA_ZHUANG, + CountryCode: COUNTRY_CODE_CN, + } + RegionHuainan = cloudprovider.SGeographicInfo{ + Latitude: 32.62657438299575, + Longitude: 116.99779954519057, + City: CITY_HUAI_NAN, + CountryCode: COUNTRY_CODE_CN, + } RegionChengzhou = cloudprovider.SGeographicInfo{ Latitude: 25.777, Longitude: 112.975, @@ -212,12 +243,24 @@ var ( City: CITY_HAI_KOU, CountryCode: COUNTRY_CODE_CN, } + RegionWulumuqi = cloudprovider.SGeographicInfo{ + Latitude: 43.825, + Longitude: 87.616, + City: CITY_WU_LU_MU_QI, + CountryCode: COUNTRY_CODE_CN, + } RegionTianjin = cloudprovider.SGeographicInfo{ Latitude: 39.125, Longitude: 117.131, City: CITY_TIAN_JIN, CountryCode: COUNTRY_CODE_CN, } + RegionShenyang = cloudprovider.SGeographicInfo{ + Latitude: 41.78937667917192, + Longitude: 123.43099727316815, + City: CITY_SHEN_YANG, + CountryCode: COUNTRY_CODE_CN, + } RegionChengdu = cloudprovider.SGeographicInfo{ Latitude: 30.573, Longitude: 104.067, diff --git a/pkg/apis/compute/storage_const.go b/pkg/apis/compute/storage_const.go index 3bba04ce6..b5181a5e0 100644 --- a/pkg/apis/compute/storage_const.go +++ b/pkg/apis/compute/storage_const.go @@ -110,10 +110,21 @@ const ( STORAGE_JDCLOUD_SSD = "ssd" // SSD云硬盘 STORAGE_JDCLOUD_PHD = "premium-hdd" // HDD云硬盘 - STORAGE_ECLOUD_CAPEBS = "capebs" // 容量盘 - STORAGE_ECLOUD_EBS = "ebs" // 性能盘 - STORAGE_ECLOUD_SSD = "ssd" // 高性能盘 - STORAGE_ECLOUD_SSDEBS = "ssdebs" // 性能优化盘 + STORAGE_ECLOUD_CAPEBS = "capebs" // 容量盘 + STORAGE_ECLOUD_SSDEBS = "ssdebs" // 性能优化型 + STORAGE_ECLOUD_SSD = "ssd" // 高性能盘 + STORAGE_ECLOUD_CAPEBS_YC = "capebsyc" // 容量型-云创版 + STORAGE_ECLOUD_SSDEBS_YC = "ssdebsyc" // 性能优化型-云创版 + STORAGE_ECLOUD_SSDYC = "ssdyc" // 高性能型-云创版 + STORAGE_ECLOUD_CAPEBS_ZX = "capebszx" // 经济型 + STORAGE_ECLOUD_ESSDL1 = "essdl1" // 极速型-L1 + STORAGE_ECLOUD_ESSDL2 = "essdl2" // 极速型-L2 + STORAGE_ECLOUD_ESSDL3 = "essdl3" // 极速型-L3 + STORAGE_ECLOUD_ESSDYCL1 = "essdycl1" // 极速型-L1-云创版 + // 系统盘 + STORAGE_ECLOUD_LOCAL = "local" // 本地盘 + // 弃用 + STORAGE_ECLOUD_EBS = "ebs" // 弹性块存储 STORAGE_ECLOUD_SYSTEM = "system" // 系统盘 // volcengine storage type diff --git a/pkg/multicloud/ecloud/client.go b/pkg/multicloud/ecloud/client.go deleted file mode 100644 index 2e1292066..000000000 --- a/pkg/multicloud/ecloud/client.go +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ecloud - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "strings" - - "yunion.io/x/jsonutils" - "yunion.io/x/pkg/errors" - "yunion.io/x/pkg/util/httputils" - - api "yunion.io/x/cloudmux/pkg/apis/compute" - "yunion.io/x/cloudmux/pkg/cloudprovider" -) - -const ( - CLOUD_PROVIDER_ECLOUD = api.CLOUD_PROVIDER_ECLOUD - CLOUD_PROVIDER_ECLOUD_CN = "移动云" - CLOUD_PROVIDER_ECLOUD_EN = "Ecloud" - CLOUD_API_VERSION = "2016-12-05" - - ECLOUD_DEFAULT_REGION = "beijing-1" -) - -type SEcloudClientConfig struct { - cpcfg cloudprovider.ProviderConfig - signer ISigner - - debug bool -} - -func NewEcloudClientConfig(signer ISigner) *SEcloudClientConfig { - cfg := &SEcloudClientConfig{ - signer: signer, - } - return cfg -} - -func (cfg *SEcloudClientConfig) SetCloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *SEcloudClientConfig { - cfg.cpcfg = cpcfg - return cfg -} - -func (cfg *SEcloudClientConfig) SetDebug(debug bool) *SEcloudClientConfig { - cfg.debug = debug - return cfg -} - -type SEcloudClient struct { - *SEcloudClientConfig - - httpClient *http.Client - iregions []cloudprovider.ICloudRegion -} - -func NewEcloudClient(cfg *SEcloudClientConfig) (*SEcloudClient, error) { - httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient() - return &SEcloudClient{ - SEcloudClientConfig: cfg, - httpClient: httpClient, - }, nil -} - -func (self *SEcloudClient) GetAccessEnv() string { - return api.CLOUD_ACCESS_ENV_ECLOUD_CHINA -} - -func (ec *SEcloudClient) fetchRegions() { - regions := make([]SRegion, 0, len(regionList)) - for id, name := range regionList { - region := SRegion{} - region.ID = id - region.Name = name - region.client = ec - regions = append(regions, region) - } - iregions := make([]cloudprovider.ICloudRegion, len(regions)) - for i := range iregions { - iregions[i] = ®ions[i] - } - ec.iregions = iregions - return -} - -func (ec *SEcloudClient) TryConnect() error { - iregions, _ := ec.GetIRegions() - if len(iregions) == 0 { - return fmt.Errorf("no invalid region for ecloud") - } - _, err := iregions[0].GetIZones() - if err != nil { - return errors.Wrap(err, "try to connect failed") - } - return nil -} - -func (ec *SEcloudClient) GetIRegions() ([]cloudprovider.ICloudRegion, error) { - if ec.iregions == nil { - ec.fetchRegions() - } - return ec.iregions, nil -} - -func (ec *SEcloudClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { - iregions, err := ec.GetIRegions() - if err != nil { - return nil, err - } - for i := range iregions { - if iregions[i].GetGlobalId() == id { - return iregions[i], nil - } - } - return nil, cloudprovider.ErrNotFound -} - -func (ec *SEcloudClient) GetRegionById(id string) (*SRegion, error) { - iregions, err := ec.GetIRegions() - if err != nil { - return nil, err - } - for i := range iregions { - if iregions[i].GetId() == id { - return iregions[i].(*SRegion), nil - } - } - return nil, cloudprovider.ErrNotFound -} - -func (ec *SEcloudClient) GetCapabilities() []string { - caps := []string{ - cloudprovider.CLOUD_CAPABILITY_COMPUTE + cloudprovider.READ_ONLY_SUFFIX, - cloudprovider.CLOUD_CAPABILITY_NETWORK + cloudprovider.READ_ONLY_SUFFIX, - cloudprovider.CLOUD_CAPABILITY_EIP + cloudprovider.READ_ONLY_SUFFIX, - } - return caps -} - -func (ec *SEcloudClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { - subAccount := cloudprovider.SSubAccount{} - subAccount.Id = ec.GetAccountId() - subAccount.Name = ec.cpcfg.Name - subAccount.Account = ec.signer.GetAccessKeyId() - subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL - return []cloudprovider.SSubAccount{subAccount}, nil -} - -func (ec *SEcloudClient) GetAccountId() string { - return ec.signer.GetAccessKeyId() -} - -func (ec *SEcloudClient) GetCloudRegionExternalIdPrefix() string { - return CLOUD_PROVIDER_ECLOUD -} - -func (ec *SEcloudClient) completeSingParams(request IRequest) (err error) { - queryParams := request.GetQueryParams() - queryParams["AccessKey"] = ec.signer.GetAccessKeyId() - queryParams["Version"] = request.GetVersion() - queryParams["Timestamp"] = request.GetTimestamp() - queryParams["SignatureMethod"] = ec.signer.GetName() - queryParams["SignatureVersion"] = ec.signer.GetVersion() - queryParams["SignatureNonce"] = ec.signer.GetNonce() - return -} - -func (ec *SEcloudClient) buildStringToSign(request IRequest) string { - signParams := request.GetQueryParams() - queryString := getUrlFormedMap(signParams) - queryString = strings.Replace(queryString, "+", "%20", -1) - queryString = strings.Replace(queryString, "*", "%2A", -1) - queryString = strings.Replace(queryString, "%7E", "~", -1) - shaString := sha256.Sum256([]byte(queryString)) - summaryQuery := hex.EncodeToString(shaString[:]) - serverPath := strings.Replace(request.GetServerPath(), "/", "%2F", -1) - return fmt.Sprintf("%s\n%s\n%s", request.GetMethod(), serverPath, summaryQuery) -} - -func (ec *SEcloudClient) doGet(ctx context.Context, r IRequest, result interface{}) error { - r.SetMethod("GET") - data, err := ec.request(ctx, r) - if err != nil { - return err - } - return data.Unmarshal(result) -} - -func (ec *SEcloudClient) doList(ctx context.Context, r IRequest, result interface{}) error { - r.SetMethod("GET") - // TODO Paging query - data, err := ec.request(ctx, r) - if err != nil { - return err - } - var ( - datas *jsonutils.JSONArray - ok bool - ) - - if datas, ok = data.(*jsonutils.JSONArray); !ok { - if !data.Contains("content") { - return ErrMissKey{ - Key: "content", - Jo: data, - } - } - content, _ := data.Get("content") - datas, ok = content.(*jsonutils.JSONArray) - if !ok { - return fmt.Errorf("The return result should be an array, but:\n%s", content) - } - } - return datas.Unmarshal(result) -} - -func (ec *SEcloudClient) request(ctx context.Context, r IRequest) (jsonutils.JSONObject, error) { - jrbody, err := ec.doRequest(ctx, r) - if err != nil { - return nil, err - } - return r.ForMateResponseBody(jrbody) -} - -func (ec *SEcloudClient) doRequest(ctx context.Context, r IRequest) (jsonutils.JSONObject, error) { - // sign - ec.completeSingParams(r) - signature := ec.signer.Sign(ec.buildStringToSign(r), "BC_SIGNATURE&") - query := r.GetQueryParams() - query["Signature"] = signature - header := r.GetHeaders() - header["Content-Type"] = "application/json" - var urlStr string - port := r.GetPort() - if len(port) > 0 { - urlStr = fmt.Sprintf("%s://%s:%s%s", r.GetScheme(), r.GetEndpoint(), port, r.GetServerPath()) - } else { - urlStr = fmt.Sprintf("%s://%s%s", r.GetScheme(), r.GetEndpoint(), r.GetServerPath()) - } - queryString := getUrlFromedMapUnescaped(r.GetQueryParams()) - if len(queryString) > 0 { - urlStr = urlStr + "?" + queryString - } - resp, err := httputils.Request( - ec.httpClient, - ctx, - httputils.THttpMethod(r.GetMethod()), - urlStr, - convertHeader(header), - r.GetBodyReader(), - ec.debug, - ) - defer httputils.CloseResponse(resp) - if err != nil { - return nil, err - } - rbody, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, errors.Wrap(err, "unable to read body of response") - } - if ec.debug { - fmt.Fprintf(os.Stderr, "Response body: %s\n", string(rbody)) - } - rbody = bytes.TrimSpace(rbody) - - var jrbody jsonutils.JSONObject - if len(rbody) > 0 && (rbody[0] == '{' || rbody[0] == '[') { - var err error - jrbody, err = jsonutils.Parse(rbody) - if err != nil { - return nil, errors.Wrapf(err, "unable to parsing json: %s", rbody) - } - } - return jrbody, nil -} - -type ErrMissKey struct { - Key string - Jo jsonutils.JSONObject -} - -func (mk ErrMissKey) Error() string { - return fmt.Sprintf("The response body should contain the %q key, but it doesn't. It is:\n%s", mk.Key, mk.Jo) -} - -func convertHeader(mh map[string]string) http.Header { - header := http.Header{} - for k, v := range mh { - header.Add(k, v) - } - return header -} - -func getUrlFromedMapUnescaped(source map[string]string) string { - kvs := make([]string, 0, len(source)) - for k, v := range source { - kvs = append(kvs, fmt.Sprintf("%s=%s", k, v)) - } - return strings.Join(kvs, "&") -} - -func getUrlFormedMap(source map[string]string) (urlEncoded string) { - urlEncoder := url.Values{} - for key, value := range source { - urlEncoder.Add(key, value) - } - urlEncoded = urlEncoder.Encode() - return -} diff --git a/pkg/multicloud/ecloud/client_test.go b/pkg/multicloud/ecloud/client_test.go deleted file mode 100644 index 78494bdae..000000000 --- a/pkg/multicloud/ecloud/client_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ecloud - -import ( - "testing" -) - -type signerMock struct { - SRamRoleSigner -} - -func (s *signerMock) GetNonce() string { - return "9d81ffbeaaf7477390db5df577bb3299" -} - -type requestMock struct { - SBaseRequest -} - -func (r *requestMock) GetTimestamp() string { - return "2017-01-11T15:15:11Z" -} - -func TestBuildStringToSing(t *testing.T) { - signer := &signerMock{SRamRoleSigner: *NewRamRoleSigner("testid", "testsecret")} - client, _ := NewEcloudClient(NewEcloudClientConfig(signer)) - request := &requestMock{ - SBaseRequest: SBaseRequest{ - Method: "GET", - ServerPath: "/api/keypair", - QueryParams: map[string]string{}, - Headers: map[string]string{}, - Content: []byte{}, - }, - } - client.completeSingParams(request) - stringToSign := client.buildStringToSign(request) - want := `GET -%2Fapi%2Fkeypair -25fb697a06bc8a16a0a2549460f5cba35aded3818e15f088e4e17cd394aa07af` - if stringToSign != want { - t.Fatalf("want:\n%s, get:\n%s", want, stringToSign) - } -} diff --git a/pkg/multicloud/ecloud/create_time.go b/pkg/multicloud/ecloud/create_time.go index 5475475ed..5eb5d4f4f 100644 --- a/pkg/multicloud/ecloud/create_time.go +++ b/pkg/multicloud/ecloud/create_time.go @@ -17,7 +17,7 @@ package ecloud import "time" type SCreateTime struct { - CreatedTime string + CreatedTime string `json:"createdTime"` } func (c *SCreateTime) GetCreatedAt() time.Time { diff --git a/pkg/multicloud/ecloud/disk.go b/pkg/multicloud/ecloud/disk.go index df5aa4f8d..ba1f98829 100644 --- a/pkg/multicloud/ecloud/disk.go +++ b/pkg/multicloud/ecloud/disk.go @@ -17,8 +17,11 @@ package ecloud import ( "context" "fmt" + "strings" "time" + "yunion.io/x/pkg/errors" + billing_api "yunion.io/x/cloudmux/pkg/apis/billing" api "yunion.io/x/cloudmux/pkg/apis/compute" "yunion.io/x/cloudmux/pkg/cloudprovider" @@ -38,31 +41,31 @@ type SDisk struct { ManualAttr SDiskManualAttr // 硬盘可挂载主机类型 - AttachServerTypes []string - AvailabilityZone string - BackupId string - Description string - ID string - IsDelete bool - IsShare bool + AttachServerTypes []string `json:"attachServerTypes,omitempty"` + AvailabilityZone string `json:"region"` + BackupId string `json:"backupId,omitempty"` + Description string `json:"description,omitempty"` + ID string `json:"id"` + IsDelete bool `json:"isDelete,omitempty"` + IsShare bool `json:"isShare,omitempty"` // 磁盘所在集群的ID - Metadata string - Name string - OperationFlag string - // 硬盘挂在主机ID列表 - ServerId []string - SizeGB int `json:"size"` - SourceVolumeId string - Status string - Type string - VolumeType string - Iscsi bool - ProductType string + Metadata string `json:"metadata,omitempty"` + Name string `json:"name,omitempty"` + OperationFlag string `json:"operationFlag,omitempty"` + // 硬盘挂载主机ID列表 + ServerId []string `json:"serverIds,omitempty"` + SizeGB int `json:"size"` + SourceVolumeId string `json:"sourceVolumeId,omitempty"` + Status string `json:"status,omitempty"` + Type string `json:"type,omitempty"` + VolumeType string `json:"volumeType,omitempty"` + Iscsi bool `json:"iscsi,omitempty"` + ProductType string `json:"productType,omitempty"` } type SDiskManualAttr struct { IsVirtual bool - TempalteId string + TemplateId string ServerId string } @@ -95,7 +98,7 @@ func (d *SDisk) GetStatus() string { // TODO return "" } - switch d.Status { + switch strings.ToLower(d.Status) { case "available", "in-use": return api.DISK_READY case "attaching": @@ -165,7 +168,10 @@ func (s *SDisk) GetIsAutoDelete() bool { } func (s *SDisk) GetTemplateId() string { - return s.ManualAttr.TempalteId + if s.ManualAttr.TemplateId != "" { + return s.ManualAttr.TemplateId + } + return "" } func (s *SDisk) GetDiskType() string { @@ -200,11 +206,31 @@ func (s *SDisk) GetAccessPath() string { } func (s *SDisk) Delete(ctx context.Context) error { - return cloudprovider.ErrNotImplemented + if s.storage == nil { + return fmt.Errorf("disk not attached to storage") + } + return s.storage.zone.region.PreDeleteVolume(s.ID) } func (s *SDisk) CreateISnapshot(ctx context.Context, name string, desc string) (cloudprovider.ICloudSnapshot, error) { - return nil, cloudprovider.ErrNotImplemented + if s.storage == nil { + return nil, fmt.Errorf("disk not attached to storage") + } + r := s.storage.zone.region + snapshotId, err := r.CreateEbsSnapshot(s.ID, name, desc) + if err != nil { + return nil, err + } + snapshots, err := r.GetSnapshots(snapshotId, s.ID, false) + if err != nil || len(snapshots) == 0 { + return nil, errors.Wrapf(err, "GetSnapshots after create") + } + for i := range snapshots { + if snapshots[i].Id == snapshotId { + return &snapshots[i], nil + } + } + return &snapshots[0], nil } func (s *SDisk) GetISnapshot(id string) (cloudprovider.ICloudSnapshot, error) { @@ -239,7 +265,11 @@ func (s *SDisk) GetISnapshots() ([]cloudprovider.ICloudSnapshot, error) { } func (s *SDisk) Resize(ctx context.Context, newSizeMB int64) error { - return cloudprovider.ErrNotImplemented + if s.storage == nil { + return fmt.Errorf("disk not attached to storage") + } + newSizeGB := int64(newSizeMB / 1024) + return s.storage.zone.region.ResizeDisk(ctx, s.ID, newSizeGB) } func (s *SDisk) Reset(ctx context.Context, snapshotId string) (string, error) { @@ -251,9 +281,9 @@ func (s *SDisk) Rebuild(ctx context.Context) error { } func (s *SRegion) GetDisks() ([]SDisk, error) { - request := NewNovaRequest(NewApiRequest(s.ID, "/api/v2/volume/volume/volume/list/with/server", nil, nil)) + req := NewOpenApiEbsRequest(s.RegionId, "/api/v2/volume/volume/volume/list/with/server", nil, nil) disks := make([]SDisk, 0, 5) - err := s.client.doList(context.Background(), request, &disks) + err := s.client.doList(context.Background(), req.Base(), &disks) if err != nil { return nil, err } @@ -261,10 +291,9 @@ func (s *SRegion) GetDisks() ([]SDisk, error) { } func (s *SRegion) GetDisk(id string) (*SDisk, error) { - // TODO - request := NewNovaRequest(NewApiRequest(s.ID, fmt.Sprintf("/api/v2/volume/volume/volumeDetail/%s", id), nil, nil)) + req := NewOpenApiEbsRequest(s.RegionId, fmt.Sprintf("/api/v2/volume/volume/volumeDetail/%s", id), nil, nil) var disk SDisk - err := s.client.doGet(context.Background(), request, &disk) + err := s.client.doGet(context.Background(), req.Base(), &disk) if err != nil { return nil, err } diff --git a/pkg/multicloud/ecloud/ecloud.go b/pkg/multicloud/ecloud/ecloud.go new file mode 100644 index 000000000..113a0c6f7 --- /dev/null +++ b/pkg/multicloud/ecloud/ecloud.go @@ -0,0 +1,546 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ecloud + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strconv" + "strings" + + "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/httputils" + "yunion.io/x/pkg/util/stringutils" + + api "yunion.io/x/cloudmux/pkg/apis/compute" + "yunion.io/x/cloudmux/pkg/cloudprovider" +) + +const ( + CLOUD_PROVIDER_ECLOUD = api.CLOUD_PROVIDER_ECLOUD + CLOUD_PROVIDER_ECLOUD_CN = "移动云" + CLOUD_PROVIDER_ECLOUD_EN = "Ecloud" + CLOUD_API_VERSION = "2016-12-05" + + ECLOUD_DEFAULT_REGION = "cn-beijing-1" +) + +type SEcloudClientConfig struct { + cpcfg cloudprovider.ProviderConfig + AccessKey string + Secret string + debug bool +} + +func NewEcloudClientConfig(accessKey, secret string) *SEcloudClientConfig { + cfg := &SEcloudClientConfig{ + AccessKey: accessKey, + Secret: secret, + } + return cfg +} + +func (cfg *SEcloudClientConfig) SetCloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *SEcloudClientConfig { + cfg.cpcfg = cpcfg + return cfg +} + +func (cfg *SEcloudClientConfig) SetDebug(debug bool) *SEcloudClientConfig { + cfg.debug = debug + return cfg +} + +type SEcloudClient struct { + *SEcloudClientConfig + + httpClient *http.Client +} + +func NewEcloudClient(cfg *SEcloudClientConfig) (*SEcloudClient, error) { + httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient() + cli := &SEcloudClient{ + SEcloudClientConfig: cfg, + httpClient: httpClient, + } + return cli, nil +} + +func (self *SEcloudClient) GetAccessEnv() string { + return api.CLOUD_ACCESS_ENV_ECLOUD_CHINA +} + +func (ec *SEcloudClient) GetRegions() ([]SRegion, error) { + ctx := context.Background() + req := NewOpenApiRegionRequest(ECLOUD_DEFAULT_REGION, nil) + ret := make([]SRegion, 0) + if err := ec.doList(ctx, req.Base(), &ret); err != nil { + return nil, err + } + for i := range ret { + ret[i].client = ec + } + return ret, nil +} + +func (ec *SEcloudClient) GetIRegions() ([]cloudprovider.ICloudRegion, error) { + regions, err := ec.GetRegions() + if err != nil { + return nil, err + } + iregions := make([]cloudprovider.ICloudRegion, len(regions)) + for i := range iregions { + iregions[i] = ®ions[i] + } + return iregions, nil +} + +func (ec *SEcloudClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { + iregions, err := ec.GetIRegions() + if err != nil { + return nil, err + } + for i := range iregions { + if iregions[i].GetGlobalId() == id { + return iregions[i], nil + } + } + return nil, cloudprovider.ErrNotFound +} + +func (ec *SEcloudClient) GetRegionById(id string) (*SRegion, error) { + iregions, err := ec.GetIRegions() + if err != nil { + return nil, err + } + for i := range iregions { + if iregions[i].GetId() == id { + return iregions[i].(*SRegion), nil + } + } + return nil, cloudprovider.ErrNotFound +} + +func (ec *SEcloudClient) GetCapabilities() []string { + caps := []string{ + cloudprovider.CLOUD_CAPABILITY_COMPUTE + cloudprovider.READ_ONLY_SUFFIX, + cloudprovider.CLOUD_CAPABILITY_NETWORK + cloudprovider.READ_ONLY_SUFFIX, + cloudprovider.CLOUD_CAPABILITY_SECURITY_GROUP + cloudprovider.READ_ONLY_SUFFIX, + cloudprovider.CLOUD_CAPABILITY_EIP + cloudprovider.READ_ONLY_SUFFIX, + } + return caps +} + +func (ec *SEcloudClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) { + subAccount := cloudprovider.SSubAccount{} + subAccount.Id = ec.GetAccountId() + subAccount.Name = ec.cpcfg.Name + subAccount.Account = ec.AccessKey + subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL + return []cloudprovider.SSubAccount{subAccount}, nil +} + +func (ec *SEcloudClient) GetAccountId() string { + return ec.AccessKey +} + +// GetBalance 查询账户余额,使用 MOPC 开放接口(与 ecloudsdkmopc BalanceQueryPOST 一致)。 +func (ec *SEcloudClient) GetBalance() (*cloudprovider.SBalanceInfo, error) { + type accountInfo struct { + AccountId string `json:"accountId"` + Balance string `json:"balance"` + OweAmount string `json:"oweAmount"` + NABalance string `json:"nABalance"` + DetailName string `json:"detailName"` + DetailValue string `json:"detailValue"` + } + type accMegRsp struct { + AccountInfo []accountInfo `json:"accountInfo"` + RspCode string `json:"rspCode"` + RspDesc string `json:"rspDesc"` + } + type resultBody struct { + RspCode string `json:"rspCode"` + RspDesc string `json:"rspDesc"` + AccMegRsp accMegRsp `json:"accMegRsp"` + } + type mopcResp struct { + RespCode string `json:"respCode"` + RespDesc string `json:"respDesc"` + Result resultBody `json:"result"` + } + + ctx := context.Background() + regionId := ECLOUD_DEFAULT_REGION + req := NewOpenApiMopcBalanceRequest(regionId, ec.GetAccountId()) + base := req.Base() + base.Method = "POST" + body, err := ec.doRequestRaw(ctx, base) + if err != nil { + return nil, err + } + resp := mopcResp{} + if err := body.Unmarshal(&resp); err != nil { + return nil, errors.Wrap(err, "unmarshal mopc balance response") + } + // 顶层 respCode: "0"/"00" 视为成功 + if resp.RespCode != "" && resp.RespCode != "0" && resp.RespCode != "00" { + return nil, fmt.Errorf("balance query failed: respCode=%s respDesc=%s", resp.RespCode, resp.RespDesc) + } + // result.rspCode: "00"/"0000" 视为成功 + if resp.Result.RspCode != "" && resp.Result.RspCode != "00" && resp.Result.RspCode != "0000" { + return nil, fmt.Errorf("balance result error: rspCode=%s rspDesc=%s", resp.Result.RspCode, resp.Result.RspDesc) + } + amount := 0.0 + if len(resp.Result.AccMegRsp.AccountInfo) > 0 { + balanceStr := resp.Result.AccMegRsp.AccountInfo[0].Balance + if balanceStr != "" { + if v, err := strconv.ParseFloat(balanceStr, 64); err == nil { + amount = v + } + } + } + return &cloudprovider.SBalanceInfo{ + Currency: "CNY", + Amount: amount, + Status: "", + }, nil +} + +func (ec *SEcloudClient) GetCloudRegionExternalIdPrefix() string { + return CLOUD_PROVIDER_ECLOUD +} + +// completeSingParams 填充签名相关的公共 query 参数。 +func (ec *SEcloudClient) completeSingParams(request *SBaseRequest) (err error) { + queryParams := request.GetQueryParams() + // 每次签名前先清理旧的 Signature,避免在同一个 request 上重复签名(如分页循环)时将旧签名参与新的签名计算。 + delete(queryParams, "Signature") + queryParams["AccessKey"] = ec.AccessKey + queryParams["Version"] = request.GetVersion() + queryParams["Timestamp"] = request.GetTimestamp() + queryParams["SignatureMethod"] = "HmacSHA1" + queryParams["SignatureVersion"] = "V2.0" + queryParams["SignatureNonce"] = stringutils.UUID4() + return +} + +// buildStringToSign 生成签名字符串,兼容老版移动云签名规则。 +func (ec *SEcloudClient) buildStringToSign(request *SBaseRequest) string { + signParams := request.GetQueryParams() + queryString := getUrlFormedMap(signParams) + queryString = strings.Replace(queryString, "+", "%20", -1) + queryString = strings.Replace(queryString, "*", "%2A", -1) + queryString = strings.Replace(queryString, "%7E", "~", -1) + shaString := sha256.Sum256([]byte(queryString)) + summaryQuery := hex.EncodeToString(shaString[:]) + serverPath := strings.Replace(request.GetServerPath(), "/", "%2F", -1) + return fmt.Sprintf("%s\n%s\n%s", request.GetMethod(), serverPath, summaryQuery) +} + +func signSHA1HMAC(source, secret string) string { + key := []byte(secret) + h := hmac.New(sha1.New, key) + h.Write([]byte(source)) + signedBytes := h.Sum(nil) + return hex.EncodeToString(signedBytes) +} + +// parseBodyToList 统一从 API 返回的 body 中解析列表,兼容 content / regions 或直接为数组,避免各处重复处理。 +func parseBodyToList(body jsonutils.JSONObject) (*jsonutils.JSONArray, error) { + if body == nil { + return nil, fmt.Errorf("response body is nil") + } + if arr, ok := body.(*jsonutils.JSONArray); ok { + return arr, nil + } + if body.Contains("content") { + content, _ := body.Get("content") + if arr, ok := content.(*jsonutils.JSONArray); ok { + return arr, nil + } + // content 为 null 或 empty:true 时视为空列表 + if content == nil || (body.Contains("empty") && body.Contains("total")) { + return jsonutils.NewArray(), nil + } + } + if body.Contains("regions") { + regions, _ := body.Get("regions") + if arr, ok := regions.(*jsonutils.JSONArray); ok { + return arr, nil + } + } + if body.Contains("zones") { + zones, _ := body.Get("zones") + if arr, ok := zones.(*jsonutils.JSONArray); ok { + return arr, nil + } + } + return nil, fmt.Errorf("response body should be array or contain content/regions/zones array, got:\n%s", body) +} + +func (ec *SEcloudClient) doGet(ctx context.Context, r *SBaseRequest, result interface{}) error { + r.SetMethod("GET") + data, err := ec.request(ctx, r) + if err != nil { + return err + } + return data.Unmarshal(result) +} + +func (ec *SEcloudClient) doPost(ctx context.Context, r *SBaseRequest, result interface{}) error { + r.SetMethod("POST") + data, err := ec.request(ctx, r) + if err != nil { + return err + } + return data.Unmarshal(result) +} + +func (ec *SEcloudClient) doList(ctx context.Context, r *SBaseRequest, result interface{}) error { + r.SetMethod("GET") + // doList 会自动翻页;为避免修改调用方传入的 request,这里拷贝一份 query 参数用于翻页循环。 + query := map[string]string{} + for k, v := range r.GetQueryParams() { + query[k] = v + } + origQuery := r.QueryParams + defer func() { r.QueryParams = origQuery }() + pageStr, hasPage := query["page"] + pageSizeStr, hasPageSize := query["pageSize"] + + // 使用 page/pageSize 做简单分页聚合: + // - 若调用方未显式设置 page/pageSize,则默认 page=1,pageSize=100,并自动翻页,直至返回为空或不足一页。 + page := 1 + if hasPage { + if v, err := strconv.Atoi(pageStr); err == nil && v > 0 { + page = v + } + } + pageSize := 100 + if hasPageSize { + if v, err := strconv.Atoi(pageSizeStr); err == nil && v > 0 { + pageSize = v + } + } + + all := jsonutils.NewArray() + for { + query["page"] = strconv.Itoa(page) + query["pageSize"] = strconv.Itoa(pageSize) + r.QueryParams = query + data, err := ec.request(ctx, r) + if err != nil { + return err + } + arr, err := parseBodyToList(data) + if err != nil { + return err + } + if arr.Length() == 0 { + break + } + for i := 0; i < arr.Length(); i++ { + item, _ := arr.GetAt(i) + all.Add(item) + } + if arr.Length() < pageSize { + break + } + page++ + } + return all.Unmarshal(result) +} + +// doPostList 与 doList 类似,但使用 POST 方法,适配新的 OpenAPI 列表接口。 +func (ec *SEcloudClient) doPostList(ctx context.Context, r *SBaseRequest, result interface{}) error { + r.SetMethod("POST") + + // POST 列表接口分页参数可能在: + // - 最外层:{"page":1,"pageSize":100,...} + // 这里自动翻页聚合:若未显式设置,则默认 page=1,pageSize=100。 + origContent := r.Content + defer func() { r.Content = origContent }() + // doPostList 会自动翻页;为避免修改调用方传入的 request,这里基于 Content/默认值构造并循环写回 r.Content。 + var reqBody jsonutils.JSONObject + if len(r.Content) > 0 { + if jb, err := jsonutils.Parse(r.Content); err == nil { + reqBody = jb + } + } + if reqBody == nil { + reqBody = jsonutils.NewDict() + } + // 注意:这里的 dict 是从 Content parse 出来的独立对象,不会影响调用方原始 JSON 对象。 + dict, ok := reqBody.(*jsonutils.JSONDict) + if !ok { + return errors.Errorf("doPostList request body should be JSON object, got: %s", reqBody) + } + + page := 1 + pageSize := 100 + + // 优先读取最外层 page/pageSize + if v, err := dict.Int("page"); err == nil && v > 0 { + page = int(v) + } + if v, err := dict.Int("pageSize"); err == nil && v > 0 { + pageSize = int(v) + } + + // 写回分页参数:统一更新/写入最外层 page/pageSize。 + setPage := func(p, ps int) { + dict.Set("page", jsonutils.NewInt(int64(p))) + dict.Set("pageSize", jsonutils.NewInt(int64(ps))) + } + + // 兜底:确保 pageSize 合法 + if pageSize <= 0 { + pageSize = 100 + } + if page <= 0 { + page = 1 + } + + all := jsonutils.NewArray() + for { + setPage(page, pageSize) + r.Content = []byte(dict.String()) + + data, err := ec.request(ctx, r) + if err != nil { + return err + } + arr, err := parseBodyToList(data) + if err != nil { + return err + } + if arr.Length() == 0 { + break + } + for i := 0; i < arr.Length(); i++ { + item, _ := arr.GetAt(i) + all.Add(item) + } + if arr.Length() < pageSize { + break + } + page++ + } + return all.Unmarshal(result) +} + +func (ec *SEcloudClient) request(ctx context.Context, r *SBaseRequest) (jsonutils.JSONObject, error) { + jrbody, err := ec.doRequest(ctx, r) + if err != nil { + return nil, err + } + return r.ForMateResponseBody(jrbody) +} + +// doRequestRaw 返回原始响应 body(不经过 ForMateResponseBody),用于 MOPC 等返回格式与 state/body 不同的接口。 +func (ec *SEcloudClient) doRequestRaw(ctx context.Context, r *SBaseRequest) (jsonutils.JSONObject, error) { + return ec.doRequest(ctx, r) +} + +func (ec *SEcloudClient) doRequest(ctx context.Context, r *SBaseRequest) (jsonutils.JSONObject, error) { + // sign + ec.completeSingParams(r) + stringToSign := ec.buildStringToSign(r) + secret := "BC_SIGNATURE&" + ec.Secret + signature := signSHA1HMAC(stringToSign, secret) + query := r.GetQueryParams() + query["Signature"] = signature + header := r.GetHeaders() + header["Content-Type"] = "application/json" + var urlStr string + port := r.GetPort() + if len(port) > 0 { + urlStr = fmt.Sprintf("https://%s:%s%s", r.GetEndpoint(), port, r.GetServerPath()) + } else { + urlStr = fmt.Sprintf("https://%s%s", r.GetEndpoint(), r.GetServerPath()) + } + // 注意:URL query 需要与签名参数一致并进行标准转义,避免出现特殊字符解析/签名不一致问题。 + queryString := getUrlFormedMap(r.GetQueryParams()) + if len(queryString) > 0 { + urlStr = urlStr + "?" + queryString + } + resp, err := httputils.Request( + ec.httpClient, + ctx, + httputils.THttpMethod(r.GetMethod()), + urlStr, + convertHeader(header), + r.GetBodyReader(), + ec.debug, + ) + defer httputils.CloseResponse(resp) + if err != nil { + return nil, err + } + rbody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "unable to read body of response") + } + if ec.debug { + fmt.Fprintf(os.Stderr, "Response body: %s\n", string(rbody)) + } + rbody = bytes.TrimSpace(rbody) + + var jrbody jsonutils.JSONObject + if len(rbody) > 0 && (rbody[0] == '{' || rbody[0] == '[') { + var err error + jrbody, err = jsonutils.Parse(rbody) + if err != nil { + return nil, errors.Wrapf(err, "unable to parsing json: %s", rbody) + } + } + return jrbody, nil +} + +type ErrMissKey struct { + Key string + Jo jsonutils.JSONObject +} + +func (mk ErrMissKey) Error() string { + return fmt.Sprintf("The response body should contain the %q key, but it doesn't. It is:\n%s", mk.Key, mk.Jo) +} + +func convertHeader(mh map[string]string) http.Header { + header := http.Header{} + for k, v := range mh { + header.Add(k, v) + } + return header +} + +func getUrlFormedMap(source map[string]string) (urlEncoded string) { + urlEncoder := url.Values{} + for key, value := range source { + urlEncoder.Add(key, value) + } + urlEncoded = urlEncoder.Encode() + return +} diff --git a/pkg/multicloud/ecloud/ecloudcli/main.go b/pkg/multicloud/ecloud/ecloudcli/main.go index b893da574..65f2856be 100644 --- a/pkg/multicloud/ecloud/ecloudcli/main.go +++ b/pkg/multicloud/ecloud/ecloudcli/main.go @@ -22,6 +22,7 @@ import ( "golang.org/x/net/http/httpproxy" + "yunion.io/x/pkg/errors" "yunion.io/x/pkg/util/shellutils" "yunion.io/x/structarg" @@ -67,6 +68,16 @@ func showErrorAndExit(e error) { os.Exit(1) } +// isClientOnlyCommand 仅依赖 client、不依赖 region 资源的命令,无有效 region 时可用 PlaceholderRegion 执行 +func isClientOnlyCommand(subcommand string) bool { + switch subcommand { + case "region-list", "balance-show", "metric-list": + return true + default: + return false + } +} + func newClient(options *Options) (*ecloud.SRegion, error) { if len(options.AccessKey) == 0 { return nil, fmt.Errorf("Missing access key") @@ -87,9 +98,8 @@ func newClient(options *Options) (*ecloud.SRegion, error) { } cli, err := ecloud.NewEcloudClient( - ecloud.NewEcloudClientConfig( - ecloud.NewRamRoleSigner(options.AccessKey, options.AccessSecret), - ).SetDebug(options.Debug). + ecloud.NewEcloudClientConfig(options.AccessKey, options.AccessSecret). + SetDebug(options.Debug). SetCloudproviderConfig( cloudprovider.ProviderConfig{ ProxyFunc: proxyFunc, @@ -102,6 +112,10 @@ func newClient(options *Options) (*ecloud.SRegion, error) { region, err := cli.GetRegionById(options.RegionId) if err != nil { + // 无 region 时仍允许执行仅需 client 的命令(region-list/balance-show/metric-list),便于验证 + if errors.Cause(err) == cloudprovider.ErrNotFound && isClientOnlyCommand(options.SUBCOMMAND) { + return ecloud.NewPlaceholderRegion(cli, options.RegionId), nil + } return nil, err } diff --git a/pkg/multicloud/ecloud/eip.go b/pkg/multicloud/ecloud/eip.go index 2f62c8faf..ecb2a9dd7 100644 --- a/pkg/multicloud/ecloud/eip.go +++ b/pkg/multicloud/ecloud/eip.go @@ -30,28 +30,30 @@ type SEip struct { multicloud.SEipBase EcloudTags - BandWidthMbSize int - BandWidthType string - BindType string + BandWidthMbSize int `json:"bandwidthMbSize,omitempty"` + BandWidthType string `json:"bandwidthType,omitempty"` + BindType string `json:"bindType,omitempty"` // 使用,未使用 - Bound bool + Bound bool `json:"bound,omitempty"` // bandwidthCharge, trafficCharge - ChargeModeEnum string - CreateTime time.Time - Frozen bool + ChargeModeEnum string `json:"chargeModeEnum,omitempty"` + CreateTime time.Time `json:"-"` + // OpenAPI 返回的创建时间字符串 + CreatedTimeStr string `json:"createdTime,omitempty"` + Frozen bool `json:"frozen,omitempty"` // 备案状态 - IcpStatus string - Id string - IpType string - Ipv6 string - Name string //公网IPv4地址 - NicName string - PortNetworkId string - Region string - ResourceId string - RouterId string + IcpStatus string `json:"icpStatus,omitempty"` + Id string `json:"id,omitempty"` + IpType string `json:"ipType,omitempty"` + Ipv6 string `json:"ipv6,omitempty"` + Name string `json:"name,omitempty"` // 公网IPv4地址 + NicName string `json:"nicName,omitempty"` + PortNetworkId string `json:"portNetworkId,omitempty"` + Region string `json:"region,omitempty"` + ResourceId string `json:"resourceId,omitempty"` + RouterId string `json:"routerId,omitempty"` // BINDING, UNBOUND, FROZEN - Status string + Status string `json:"status,omitempty"` } func (e *SEip) GetId() string { @@ -113,6 +115,14 @@ func (e *SEip) GetBillingType() string { } func (e *SEip) GetCreatedAt() time.Time { + if len(e.CreatedTimeStr) > 0 { + if t, err := time.Parse("2006-01-02 15:04:05", e.CreatedTimeStr); err == nil { + return t + } + if t, err := time.Parse(time.RFC3339, e.CreatedTimeStr); err == nil { + return t + } + } return e.CreateTime } @@ -129,7 +139,7 @@ func (e *SEip) GetBandwidth() int { } func (e *SEip) GetINetworkId() string { - return e.PortNetworkId + return "" } func (e *SEip) GetInternetChargeType() string { @@ -162,10 +172,27 @@ func (e *SEip) GetProjectId() string { func (r *SRegion) GetEipById(id string) (*SEip, error) { var eip SEip - request := NewConsoleRequest(r.ID, fmt.Sprintf("/api/v2/floatingIp/getRespWithBw/%s", id), nil, nil) - err := r.client.doGet(context.Background(), request, &eip) + // 使用 OpenAPI EIP 详情:GET /api/openapi-eip/acl/v3/floatingip/getRespWithBw/{ipId} + req := NewOpenApiEbsRequest(r.RegionId, fmt.Sprintf("/api/openapi-eip/acl/v3/floatingip/getRespWithBw/%s", id), nil, nil) + err := r.client.doGet(context.Background(), req.Base(), &eip) if err != nil { return nil, err } + eip.region = r + return &eip, nil +} + +// GetEipByAddr 使用 OpenAPI EIP 详情查询(按地址): +// GET /api/openapi-eip/acl/v3/floatingip/apiDetail?ipAddr={addr} +func (r *SRegion) GetEipByAddr(addr string) (*SEip, error) { + var eip SEip + params := map[string]string{ + "ipAddress": addr, + } + req := NewOpenApiEbsRequest(r.RegionId, "/api/openapi-eip/acl/v3/floatingip/apiDetail", params, nil) + if err := r.client.doGet(context.Background(), req.Base(), &eip); err != nil { + return nil, err + } + eip.region = r return &eip, nil } diff --git a/pkg/multicloud/ecloud/host.go b/pkg/multicloud/ecloud/host.go index 477135fde..dc6dbfdef 100644 --- a/pkg/multicloud/ecloud/host.go +++ b/pkg/multicloud/ecloud/host.go @@ -55,8 +55,7 @@ func (h *SHost) IsEmulated() bool { } func (h *SHost) GetIVMs() ([]cloudprovider.ICloudVM, error) { - zoneRegion := h.zone.Region - vms, err := h.zone.region.GetInstances(zoneRegion) + vms, err := h.zone.region.GetInstances(h.zone.ZoneId, "") if err != nil { return nil, errors.Wrap(err, "SHost.GetVMs") } @@ -69,7 +68,7 @@ func (h *SHost) GetIVMs() ([]cloudprovider.ICloudVM, error) { } func (h *SHost) GetIVMById(id string) (cloudprovider.ICloudVM, error) { - vm, err := h.zone.region.GetInstanceById(id) + vm, err := h.zone.region.GetInstance(id) if err != nil { return nil, err } @@ -156,7 +155,11 @@ func (h *SHost) CreateVM(desc *cloudprovider.SManagedVMCreateConfig) (cloudprovi } func (h *SHost) GetIHostNics() ([]cloudprovider.ICloudHostNetInterface, error) { - return nil, cloudprovider.ErrNotImplemented + wires, err := h.zone.GetIWires() + if err != nil { + return nil, errors.Wrap(err, "GetIWires") + } + return cloudprovider.GetHostNetifs(h, wires), nil } func (h *SRegion) GetVMs() ([]SInstance, error) { diff --git a/pkg/multicloud/ecloud/image.go b/pkg/multicloud/ecloud/image.go index 5140fa4d0..a5f08e411 100644 --- a/pkg/multicloud/ecloud/image.go +++ b/pkg/multicloud/ecloud/image.go @@ -35,31 +35,35 @@ type SImage struct { storageCache *SStoragecache imgInfo *imagetools.ImageInfo - ImageId string - ServerId string - ImageAlias string - Name string - Url string - SrourceImageId string - Status string - SizeMb int `json:"size"` - IsPublic int - CreateTime time.Time - Note string - OsType string - MinDiskGB int `json:"minDisk"` - ImageType string - PublicImageType string - BackupType string - BackupWay string - SnapshotId string - OsName string + // 字段与 IMS OpenAPI listImageRespV2/getImageRespV2 对齐 + ImageId string `json:"imageId,omitempty"` + ServerId string `json:"serverId,omitempty"` + ImageAlias string `json:"imageAlias,omitempty"` + Name string `json:"name,omitempty"` + Url string `json:"url,omitempty"` + SrourceImageId string `json:"sourceImageId,omitempty"` + Status string `json:"status,omitempty"` + SizeMb int `json:"size"` // IMS 返回 size,按 MB 处理 + IsPublic int `json:"isPublic,omitempty"` + ImageSource string `json:"imageSource,omitempty"` // 通过 imageSource 判断是否公共镜像 + CreateTime time.Time `json:"-"` // 旧接口可能直接反序列化为 time.Time + CreateTimeStr string `json:"createTime,omitempty"` + Note string `json:"note,omitempty"` + OsType string `json:"osType,omitempty"` + MinDiskGB int `json:"minDisk,omitempty"` + ImageType string `json:"imageType,omitempty"` + PublicImageType string `json:"publicImageType,omitempty"` + BackupType string `json:"backupType,omitempty"` + BackupWay string `json:"backupWay,omitempty"` + SnapshotId string `json:"snapshotId,omitempty"` + OsName string `json:"osName,omitempty"` } func (r *SRegion) GetImage(imageId string) (*SImage, error) { - request := NewNovaRequest(NewApiRequest(r.ID, fmt.Sprintf("/api/v2/image/%s", imageId), nil, nil)) + // 使用 IMS OpenAPI 镜像详情:GET /api/openapi-ims/user/v5/image/{imageId} + req := NewOpenApiEbsRequest(r.RegionId, fmt.Sprintf("/api/openapi-ims/user/v5/image/%s", imageId), nil, nil) var image SImage - err := r.client.doGet(context.Background(), request, &image) + err := r.client.doGet(context.Background(), req.Base(), &image) if err != nil { return nil, err } @@ -67,19 +71,40 @@ func (r *SRegion) GetImage(imageId string) (*SImage, error) { } func (r *SRegion) GetImages(isPublic bool) ([]SImage, error) { + // IMS 接口要求 region(资源池 ID),否则报 CSLOPENSTACK_COMPUTE_IMAGE_PARAM_INVALID + poolId := regionIdToPoolId[r.RegionId] + if poolId == "" { + poolId = r.RegionId + } + // IMS OpenAPI ListImageRespV2 + // GET /api/openapi-ims/user/v5/image?region=CIDC-RP-xx&imageSource=PUBLIC|PRIVATE + query := map[string]string{ + "region": poolId, + } if isPublic { - return nil, cloudprovider.ErrNotImplemented + query["imageSource"] = "PUBLIC" + } else { + query["imageSource"] = "PRIVATE" } - request := NewNovaRequest(NewApiRequest(r.ID, "/api/v2/image", nil, nil)) - images := make([]SImage, 0, 5) - err := r.client.doList(context.Background(), request, &images) - if err != nil { + req := NewOpenApiEbsRequest(r.RegionId, "/api/openapi-ims/user/v5/image", query, nil) + images := make([]SImage, 0, 20) + if err := r.client.doList(context.Background(), req.Base(), &images); err != nil { return nil, err } return images, nil } func (i *SImage) GetCreatedAt() time.Time { + if len(i.CreateTimeStr) > 0 { + // IMS 接口时间格式一般为 "2006-01-02 15:04:05" + if t, err := time.Parse("2006-01-02 15:04:05", i.CreateTimeStr); err == nil { + return t + } + // 兼容 RFC3339 + if t, err := time.Parse(time.RFC3339, i.CreateTimeStr); err == nil { + return t + } + } return i.CreateTime } diff --git a/pkg/multicloud/ecloud/instance.go b/pkg/multicloud/ecloud/instance.go index 38bea5a48..4ff3faea4 100644 --- a/pkg/multicloud/ecloud/instance.go +++ b/pkg/multicloud/ecloud/instance.go @@ -19,9 +19,11 @@ import ( "fmt" "time" + "yunion.io/x/jsonutils" + "yunion.io/x/log" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/util/billing" - "yunion.io/x/pkg/util/sets" + "yunion.io/x/pkg/util/imagetools" billing_api "yunion.io/x/cloudmux/pkg/apis/billing" api "yunion.io/x/cloudmux/pkg/apis/compute" @@ -29,23 +31,6 @@ import ( "yunion.io/x/cloudmux/pkg/multicloud" ) -type SNovaRequest struct { - SApiRequest -} - -func NewNovaRequest(ar *SApiRequest) *SNovaRequest { - return &SNovaRequest{ - SApiRequest: *ar, - } -} - -func (nr *SNovaRequest) GetPort() string { - if nr.RegionId == "guangzhou-2" { - return "" - } - return nr.SApiRequest.GetPort() -} - type SInstance struct { multicloud.SInstanceBase EcloudTags @@ -53,48 +38,117 @@ type SInstance struct { SZoneRegionBase SCreateTime - nicComplete bool - host *SHost - image *SImage - - sysDisk cloudprovider.ICloudDisk - dataDisks []cloudprovider.ICloudDisk - - Id string - Name string - Vcpu int - Vmemory int - KeyName string - ImageRef string - ImageName string - ImageOsType string - FlavorRef string - SystemDiskSizeGB int `json:"vdisk"` - SystemDiskId string - ServerType string - ServerVmType string - EcStatus string - BootVolumeType string - Deleted int - Visible bool - Region string - PortDetail []SInstanceNic + host *SHost + + region *SRegion + + billInfoFetched bool + billInfo *SInstanceBillInfo + + // OpenAPI v4/list/describe-instances 返回字段 + ActualSystemImage string `json:"actualSystemImage"` + AdminPaused bool `json:"adminPaused"` + AdminPausedTip string `json:"adminPausedTip"` + + ZoneId string `json:"zoneId"` + + CredibleStatus string `json:"credibleStatus"` + + CrossRegionMigrate bool `json:"crossRegionMigrate"` + + Description string `json:"description"` + + GroupId string `json:"groupId"` + + InstanceId string `json:"instanceId"` + InstanceName string `json:"instanceName"` + + ImageId string `json:"imageId"` + ImageName string `json:"imageName"` + ImageOsType string `json:"imageOsType"` + + KeyName string `json:"keyName"` + + MainPortId string `json:"mainPortId"` + MaxPorts int `json:"maxPorts"` + MaxVolumes int `json:"maxVolumes"` + + AvailableZone string `json:"availableZone"` + + ProductType string `json:"productType"` + + Recycle bool `json:"recycle"` + RecycleCount int `json:"recycleCount"` + RecycleStatus string `json:"recycleStatus"` + RecycleTime string `json:"recycleTime"` + + ReleaseProtect bool `json:"releaseProtect"` + + FlavorName string `json:"flavorName"` + ServerType string `json:"serverType"` + ServerVmType string `json:"serverVmType"` + + StoppedMode bool `json:"stoppedMode"` + + SupportVolumeMount string `json:"supportVolumeMount"` + + UserName string `json:"userName"` + + Vcpu int `json:"vcpu"` + Vmemory int `json:"vmemory"` + + Vdisk int `json:"vdisk"` + SystemDiskId string `json:"systemDiskId"` + Status string `json:"status"` + BootVolumeType string `json:"bootVolumeType"` } func (i *SInstance) GetBillingType() string { + region := i.getRegion() + if region != nil && !i.billInfoFetched { + i.billInfoFetched = true + infos, err := region.GetInsatnceBillInfo(context.Background(), []string{i.GetId()}) + if err != nil { + log.Debugf("ecloud GetInsatnceBillInfo(%s) error: %v", i.GetId(), err) + } else { + for idx := range infos { + if infos[idx].InstanceId == i.GetId() { + i.billInfo = &infos[idx] + break + } + } + } + } + if i.billInfo != nil { + // chargingMode: 1=包周期(预付费) 2=按需(后付费) + switch i.billInfo.ChargingMode { + case 1: + return billing_api.BILLING_TYPE_PREPAID + case 2: + return billing_api.BILLING_TYPE_POSTPAID + } + } + // 查询失败或无返回时兜底按后付费处理 return billing_api.BILLING_TYPE_POSTPAID } +func (i *SInstance) getRegion() *SRegion { + if i.host != nil && i.host.zone != nil && i.host.zone.region != nil { + return i.host.zone.region + } + return i.region +} + func (i *SInstance) GetExpiredAt() time.Time { return time.Time{} } func (i *SInstance) GetId() string { - return i.Id + return i.InstanceId } func (i *SInstance) GetName() string { - return i.Name + return i.InstanceName } func (i *SInstance) GetHostname() string { @@ -106,7 +160,7 @@ func (i *SInstance) GetGlobalId() string { } func (i *SInstance) GetStatus() string { - switch i.EcStatus { + switch i.Status { case "active": return api.VM_RUNNING case "suspended", "paused": @@ -127,12 +181,11 @@ func (i *SInstance) GetStatus() string { } func (i *SInstance) Refresh() error { - // TODO - return nil -} - -func (i *SInstance) IsEmulated() bool { - return false + vm, err := i.host.zone.region.GetInstance(i.InstanceId) + if err != nil { + return err + } + return jsonutils.Update(i, vm) } func (self *SInstance) GetBootOrder() string { @@ -147,68 +200,35 @@ func (self *SInstance) GetVdi() string { return "vnc" } -func (i *SInstance) GetImage() (*SImage, error) { - if i.image != nil { - return i.image, nil - } - image, err := i.host.zone.region.GetImage(i.ImageRef) - if err != nil { - return nil, err - } - i.image = image - return i.image, nil -} - func (i *SInstance) GetOsType() cloudprovider.TOsType { return cloudprovider.TOsType(i.ImageOsType) } func (i *SInstance) GetFullOsName() string { - image, err := i.GetImage() - if err != nil { - return "" - } - return image.GetFullOsName() + return i.ImageName } func (i *SInstance) GetBios() cloudprovider.TBiosType { - image, err := i.GetImage() - if err != nil { - return cloudprovider.BIOS - } - return image.GetBios() + return cloudprovider.BIOS } func (i *SInstance) GetOsArch() string { - image, err := i.GetImage() - if err != nil { - return "" - } - return image.GetOsArch() + return "" } func (i *SInstance) GetOsDist() string { - image, err := i.GetImage() - if err != nil { - return "" - } - return image.GetOsDist() + osInfo := imagetools.NormalizeImageInfo(i.ImageName, "", i.ImageOsType, "", "") + return osInfo.OsDistro } func (i *SInstance) GetOsVersion() string { - image, err := i.GetImage() - if err != nil { - return "" - } - return image.GetOsVersion() + osInfo := imagetools.NormalizeImageInfo(i.ImageName, "", i.ImageOsType, "", "") + return osInfo.OsVersion } func (i *SInstance) GetOsLang() string { - image, err := i.GetImage() - if err != nil { - return "" - } - return image.GetOsLang() + osInfo := imagetools.NormalizeImageInfo(i.ImageName, "", i.ImageOsType, "", "") + return osInfo.OsLang } func (i *SInstance) GetMachine() string { @@ -216,7 +236,7 @@ func (i *SInstance) GetMachine() string { } func (i *SInstance) GetInstanceType() string { - return i.FlavorRef + return i.FlavorName } func (in *SInstance) GetProjectId() string { @@ -228,70 +248,64 @@ func (in *SInstance) GetIHost() cloudprovider.ICloudHost { } func (in *SInstance) GetIDisks() ([]cloudprovider.ICloudDisk, error) { - if in.sysDisk == nil { - in.fetchSysDisk() + sysDisk, err := in.GetSysDisk() + if err != nil { + return nil, err } - if in.dataDisks == nil { - err := in.fetchDataDisks() - if err != nil { - return nil, err - } + dataDisks, err := in.GetDataDisks() + if err != nil { + return nil, err } - return append([]cloudprovider.ICloudDisk{in.sysDisk}, in.dataDisks...), nil + return append([]cloudprovider.ICloudDisk{sysDisk}, dataDisks...), nil } func (in *SInstance) GetINics() ([]cloudprovider.ICloudNic, error) { - if !in.nicComplete { - err := in.makeNicComplete() - if err != nil { - return nil, errors.Wrap(err, "unable to make nics complete") - } - in.nicComplete = true + region := in.getRegion() + if region == nil { + return []cloudprovider.ICloudNic{}, nil + } + nics, err := region.GetInstanceNics(context.Background(), in.GetId()) + if err != nil { + return nil, err } - inics := make([]cloudprovider.ICloudNic, len(in.PortDetail)) - for i := range in.PortDetail { - in.PortDetail[i].instance = in - inics[i] = &in.PortDetail[i] + ret := make([]cloudprovider.ICloudNic, len(nics)) + for i := range nics { + nics[i].instance = in + ret[i] = &nics[i] } - return inics, nil + return ret, nil } func (in *SInstance) GetIEIP() (cloudprovider.ICloudEIP, error) { - if !in.nicComplete { - err := in.makeNicComplete() - if err != nil { - return nil, errors.Wrap(err, "unable to make nics complete") - } - in.nicComplete = true + nics, err := in.host.zone.region.GetInstanceNics(context.Background(), in.GetId()) + if err != nil { + return nil, err } - var eipId string - for i := range in.PortDetail { - if len(in.PortDetail[i].IpId) > 0 { - eipId = in.PortDetail[i].IpId - break + for i := range nics { + if len(nics[i].FipAddress) == 0 { + continue } + eip, err := in.getRegion().GetEipByAddr(nics[i].FipAddress) + if err != nil { + return nil, err + } + return eip, nil } - if len(eipId) == 0 { - return nil, nil - } - return in.host.zone.region.GetEipById(eipId) + return nil, cloudprovider.ErrNotFound } func (in *SInstance) GetSecurityGroupIds() ([]string, error) { - if !in.nicComplete { - err := in.makeNicComplete() - if err != nil { - return nil, errors.Wrap(err, "unable to make nics complete") - } - in.nicComplete = true + nics, err := in.host.zone.region.GetInstanceNics(context.Background(), in.GetId()) + if err != nil { + return nil, err } - ret := sets.NewString() - for i := range in.PortDetail { - for _, group := range in.PortDetail[i].SecurityGroups { - ret.Insert(group.Id) + securityGroupIds := make([]string, 0) + for i := range nics { + for _, sg := range nics[i].SecurityGroups { + securityGroupIds = append(securityGroupIds, sg.Id) } } - return ret.UnsortedList(), nil + return securityGroupIds, nil } func (in *SInstance) GetVcpuCount() int { @@ -311,15 +325,27 @@ func (in *SInstance) GetHypervisor() string { } func (in *SInstance) StartVM(ctx context.Context) error { - return cloudprovider.ErrNotImplemented + region := in.getRegion() + if region == nil { + return errors.Wrap(cloudprovider.ErrNotImplemented, "missing region") + } + return region.StartInstance(ctx, in.GetId()) } func (self *SInstance) StopVM(ctx context.Context, opts *cloudprovider.ServerStopOptions) error { - return cloudprovider.ErrNotImplemented + region := self.getRegion() + if region == nil { + return errors.Wrap(cloudprovider.ErrNotImplemented, "missing region") + } + return region.StopInstance(ctx, self.GetId()) } func (self *SInstance) DeleteVM(ctx context.Context) error { - return cloudprovider.ErrNotImplemented + region := self.getRegion() + if region == nil { + return errors.Wrap(cloudprovider.ErrNotImplemented, "missing region") + } + return region.DeleteInstance(ctx, self.GetId(), false, false) } func (self *SInstance) UpdateVM(ctx context.Context, input cloudprovider.SInstanceUpdateOptions) error { @@ -372,153 +398,295 @@ func (self *SInstance) GetError() error { return nil } -func (in *SInstance) fetchSysDisk() { - storage, _ := in.host.zone.getStorageByType(api.STORAGE_ECLOUD_SYSTEM) - disk := SDisk{ +func (in *SInstance) GetSysDisk() (cloudprovider.ICloudDisk, error) { + storage, err := in.host.zone.GetStorageByType(storageTypeConstMap[in.BootVolumeType]) + if err != nil { + return nil, errors.Wrapf(err, "GetStorageByType(%s)", in.BootVolumeType) + } + disk := &SDisk{ storage: storage, ManualAttr: SDiskManualAttr{ IsVirtual: true, - TempalteId: in.ImageRef, - ServerId: in.Id, + TemplateId: in.ImageId, + ServerId: in.InstanceId, }, SCreateTime: in.SCreateTime, SZoneRegionBase: in.SZoneRegionBase, - ServerId: []string{in.Id}, + ServerId: []string{in.InstanceId}, IsShare: false, IsDelete: false, - SizeGB: in.SystemDiskSizeGB, + SizeGB: in.Vdisk, ID: in.SystemDiskId, - Name: fmt.Sprintf("%s-root", in.Name), + Name: fmt.Sprintf("%s-root", in.InstanceName), Status: "in-use", Type: api.STORAGE_ECLOUD_SYSTEM, } - in.sysDisk = &disk - return + return disk, nil } -func (in *SInstance) fetchDataDisks() error { - request := NewNovaRequest(NewApiRequest(in.host.zone.region.ID, "/api/v2/volume/volume/mount/list", - map[string]string{"serverId": in.Id}, nil)) - disks := make([]SDisk, 0, 5) - err := in.host.zone.region.client.doList(context.Background(), request, &disks) +func (region *SRegion) GetDataDisks(serverId string) ([]SDisk, error) { + request := NewOpenApiEbsRequest(region.RegionId, "/api/ebs/acl/v3/volume/mount/list", map[string]string{"serverId": serverId}, nil) + var ret []SDisk + err := region.client.doList(context.Background(), request.Base(), &ret) if err != nil { - return err + return nil, errors.Wrapf(err, "GetDataDisks") + } + return ret, nil +} + +func (in *SInstance) GetDataDisks() ([]cloudprovider.ICloudDisk, error) { + disks, err := in.host.zone.region.GetDataDisks(in.InstanceId) + if err != nil { + return nil, err } idisks := make([]cloudprovider.ICloudDisk, len(disks)) - for i := range idisks { - storageType := disks[i].Type - storage, err := in.host.zone.getStorageByType(storageType) + for i := range disks { + storage, err := in.host.zone.GetStorageByType(disks[i].Type) if err != nil { - return errors.Wrapf(err, "unable to fetch storage with stoageType %s", storageType) + return nil, errors.Wrapf(err, "GetStorageByType(%s)", disks[i].Type) } disks[i].storage = storage idisks[i] = &disks[i] } - in.dataDisks = idisks - return nil + return idisks, nil } -func (in *SInstance) makeNicComplete() error { - routerIds := sets.NewString() - nics := make(map[string]*SInstanceNic, len(in.PortDetail)) - for i := range in.PortDetail { - nic := &in.PortDetail[i] - routerIds.Insert(nic.RouterId) - nics[nic.PortId] = nic - } - for _, routerId := range routerIds.UnsortedList() { - request := NewConsoleRequest(in.host.zone.region.ID, fmt.Sprintf("/api/vpc/%s/nic", routerId), - map[string]string{ - "resourceId": in.Id, - }, nil, - ) - completeNics := make([]SInstanceNic, 0, len(nics)/2) - err := in.host.zone.region.client.doList(context.Background(), request, &completeNics) - if err != nil { - return errors.Wrapf(err, "unable to get nics with instance %s in vpc %s", in.Id, routerId) - } - for i := range completeNics { - id := completeNics[i].Id - nic, ok := nics[id] - if !ok { - continue - } - nic.SInstanceNicDetail = completeNics[i].SInstanceNicDetail - } +func (r *SRegion) GetInstances(zoneId string, serverId string) ([]SInstance, error) { + body := jsonutils.NewDict() + if len(zoneId) > 0 { + body.Add(jsonutils.NewString(zoneId), "zoneId") } - return nil -} - -func (r *SRegion) findHost(zoneRegion string) (*SHost, error) { - zone, err := r.FindZone(zoneRegion) + if len(serverId) > 0 { + body.Add(jsonutils.NewString(serverId), "instanceId") + } + req := NewOpenApiInstanceRequest(r.RegionId, body) + ret := make([]SInstance, 0) + err := r.client.doPostList(context.Background(), req.Base(), &ret) if err != nil { return nil, err } - return &SHost{ - zone: zone, - }, nil + for i := range ret { + ret[i].region = r + } + return ret, nil } -func (r *SRegion) GetInstancesWithHost(zoneRegion string) ([]SInstance, error) { - instances, err := r.GetInstances(zoneRegion) +func (r *SRegion) GetInstance(id string) (*SInstance, error) { + instances, err := r.GetInstances("", id) if err != nil { return nil, err } for i := range instances { - host, _ := r.findHost(instances[i].Region) - instances[i].host = host + if instances[i].InstanceId == id { + instances[i].region = r + return &instances[i], nil + } + } + return nil, cloudprovider.ErrNotFound +} + +func (r *SRegion) GetInstanceVNCUrl(instanceId string) (string, error) { + req := NewOpenApiInstanceActionRequest(r.RegionId, "/api/openapi-instance/v4/vnc-url", nil) + base := req.Base() + base.SetMethod("GET") + base.GetQueryParams()["instanceId"] = instanceId + resp := struct { + VncUrl string `json:"vncUrl"` + }{} + if err := r.client.doGet(context.Background(), base, &resp); err != nil { + return "", err } - return instances, nil + return resp.VncUrl, nil } -func (r *SRegion) GetInstances(zoneRegion string) ([]SInstance, error) { - return r.getInstances(zoneRegion, "") +type sInstanceBatchResult struct { + Result bool `json:"result"` + InstanceId string `json:"instanceId"` + Message string `json:"message"` } -func (r *SRegion) getInstances(zoneRegion string, serverId string) ([]SInstance, error) { - query := map[string]string{ - "serverTypes": "VM", - "productTypes": "NORMAL,AUTOSCALING,VO,CDN,PAAS_MASTER,PAAS_SLAVE,VCPE,EMR,LOGAUDIT", - //"productTypes": "NORMAL", - "visible": "true", +type sInstanceBatchResp struct { + InstanceBatchResult []sInstanceBatchResult `json:"instanceBatchResult"` +} + +func (r *SRegion) StartInstance(ctx context.Context, instanceId string) error { + body := jsonutils.NewDict() + body.Set("batchStartInstancesBody", jsonutils.Marshal(map[string][]string{ + "instanceIds": {instanceId}, + })) + req := NewOpenApiInstanceActionRequest(r.RegionId, "/api/openapi-instance/v4/batch-start-instances", body) + resp := sInstanceBatchResp{} + if err := r.client.doPost(ctx, req.Base(), &resp); err != nil { + return err } - if len(serverId) > 0 { - query["serverId"] = serverId + for i := range resp.InstanceBatchResult { + if resp.InstanceBatchResult[i].InstanceId != instanceId { + continue + } + if !resp.InstanceBatchResult[i].Result { + return errors.Errorf("start instance %s failed: %s", instanceId, resp.InstanceBatchResult[i].Message) + } + return nil } - if len(zoneRegion) > 0 { - query["region"] = zoneRegion + return nil +} + +func (r *SRegion) StopInstance(ctx context.Context, instanceId string) error { + body := jsonutils.NewDict() + body.Set("batchStopInstancesBody", jsonutils.Marshal(map[string][]string{ + "instanceIds": {instanceId}, + })) + req := NewOpenApiInstanceActionRequest(r.RegionId, "/api/openapi-instance/v4/batch-stop-instances", body) + resp := sInstanceBatchResp{} + if err := r.client.doPost(ctx, req.Base(), &resp); err != nil { + return err } - request := NewNovaRequest(NewApiRequest(r.ID, "/api/v2/server/web/with/network", query, nil)) - var instances []SInstance - err := r.client.doList(context.Background(), request, &instances) - if err != nil { - return nil, err + for i := range resp.InstanceBatchResult { + if resp.InstanceBatchResult[i].InstanceId != instanceId { + continue + } + if !resp.InstanceBatchResult[i].Result { + return errors.Errorf("stop instance %s failed: %s", instanceId, resp.InstanceBatchResult[i].Message) + } + return nil } - return instances, nil + return nil } -func (r *SRegion) GetInstanceById(id string) (*SInstance, error) { - instances, err := r.getInstances("", id) - if err != nil { - return nil, err +func (r *SRegion) DeleteInstance(ctx context.Context, instanceId string, deletePublicNetwork, deleteDataVolumes bool) error { + body := jsonutils.NewDict() + body.Set("deleteInstancesBody", jsonutils.Marshal(map[string]interface{}{ + "instanceIds": []string{instanceId}, + "deletePublicNetwork": deletePublicNetwork, + "deleteDataVolumes": deleteDataVolumes, + "dryRun": false, + })) + req := NewOpenApiInstanceActionRequest(r.RegionId, "/api/openapi-instance/v4/delete-instances", body) + resp := sInstanceBatchResp{} + if err := r.client.doPost(ctx, req.Base(), &resp); err != nil { + return err } - if len(instances) == 0 { - return nil, cloudprovider.ErrNotFound + for i := range resp.InstanceBatchResult { + if resp.InstanceBatchResult[i].InstanceId != instanceId { + continue + } + if !resp.InstanceBatchResult[i].Result { + return errors.Errorf("delete instance %s failed: %s", instanceId, resp.InstanceBatchResult[i].Message) + } + return nil + } + return nil +} + +type sDescribeVirtualNetworksResp struct { + MacAddress string `json:"macAddress"` + Name string `json:"name"` + BandwidthSize int `json:"bandwidthSize"` + FixedIpResps []struct { + SubnetId string `json:"subnetId"` + SubnetCidr string `json:"subnetCidr"` + VpcName string `json:"vpcName"` + IpVersion int32 `json:"ipVersion"` + VpcId string `json:"vpcId"` + IpAddress string `json:"ipAddress"` + SubnetName string `json:"subnetName"` + } `json:"fixedIpResps"` + CreatedTime string `json:"createdTime"` + PublicIp string `json:"publicIp"` + Id string `json:"id"` + SubnetName string `json:"subnetName"` + SecurityGroupResps []struct { + Name string `json:"name"` + CreatedTime string `json:"createdTime"` + Description string `json:"description"` + Id string `json:"id"` + } `json:"securityGroupResps"` +} + +// GetInstanceNics 查询实例绑定的网卡详情列表: +// GET /api/openapi-instance/v4/virtual-networks?instanceId=xxx&page=1&pageSize=100 +func (r *SRegion) GetInstanceNics(ctx context.Context, instanceId string) ([]SInstanceNic, error) { + req := NewOpenApiInstanceActionRequest(r.RegionId, "/api/openapi-instance/v4/virtual-networks", nil) + query := req.Base().GetQueryParams() + query["instanceId"] = instanceId + nets := make([]sDescribeVirtualNetworksResp, 0) + if err := r.client.doList(ctx, req.Base(), &nets); err != nil { + return nil, err } - instance := &instances[0] - host, err := r.findHost(instance.Region) - if err == nil { - instance.host = host + ret := make([]SInstanceNic, 0, len(nets)) + for i := range nets { + n := nets[i] + nic := SInstanceNic{ + Id: n.Id, + PortId: n.Id, + PortName: n.Name, + PrivateIp: "", + FipAddress: n.PublicIp, + FipBandwidthSize: n.BandwidthSize, + } + nic.MacAddress = n.MacAddress + nic.PublicIp = n.PublicIp + for j := range n.SecurityGroupResps { + sg := n.SecurityGroupResps[j] + nic.SecurityGroups = append(nic.SecurityGroups, SSecurityGroupRef{ + Id: sg.Id, + Name: sg.Name, + }) + } + for j := range n.FixedIpResps { + ip := n.FixedIpResps[j] + if nic.PrivateIp == "" && ip.IpAddress != "" { + nic.PrivateIp = ip.IpAddress + } + if nic.NetworkId == "" && ip.SubnetId != "" { + nic.NetworkId = ip.SubnetId + } + nic.FixedIpDetails = append(nic.FixedIpDetails, SFixedIpDetail{ + IpAddress: ip.IpAddress, + IpVersion: fmt.Sprintf("%d", ip.IpVersion), + SubnetId: ip.SubnetId, + SubnetName: func() string { + if ip.SubnetName != "" { + return ip.SubnetName + } + return n.SubnetName + }(), + }) + } + ret = append(ret, nic) } - return instance, nil + return ret, nil } -func (r *SRegion) GetInstanceVNCUrl(instanceId string) (string, error) { - request := NewNovaRequest(NewApiRequest(r.ID, fmt.Sprintf("/api/server/%s/vnc", instanceId), nil, nil)) - var url string - err := r.client.doGet(context.Background(), request, &url) - if err != nil { - return "", err +type SInstanceBillInfo struct { + AutoEndTime string `json:"autoEndTime"` + PriceUnit string `json:"priceUnit"` + ChargingMode int32 `json:"chargingMode"` + RespDesc string `json:"respDesc"` + InstanceId string `json:"instanceId"` + AutoRenew string `json:"autoRenew"` + EndTime string `json:"endTime"` +} + +// GetInstanceBillInfo 查询实例计费信息: +// POST /api/openapi-instance/v4/batch-query-price-info +func (r *SRegion) GetInstanceBillInfo(ctx context.Context, instanceIds []string) ([]SInstanceBillInfo, error) { + if len(instanceIds) == 0 { + return []SInstanceBillInfo{}, nil + } + body := jsonutils.NewDict() + body.Set("batchQueryPriceInfoBody", jsonutils.Marshal(map[string][]string{ + "instanceIds": instanceIds, + })) + req := NewOpenApiInstanceActionRequest(r.RegionId, "/api/openapi-instance/v4/batch-query-price-info", body) + ret := make([]SInstanceBillInfo, 0) + if err := r.client.doPost(ctx, req.Base(), &ret); err != nil { + return nil, err } - return url, nil + return ret, nil +} + +// GetInsatnceBillInfo 保持兼容(历史拼写错误) +func (r *SRegion) GetInsatnceBillInfo(ctx context.Context, instanceIds []string) ([]SInstanceBillInfo, error) { + return r.GetInstanceBillInfo(ctx, instanceIds) } diff --git a/pkg/multicloud/ecloud/instancenic.go b/pkg/multicloud/ecloud/instancenic.go index 1d2d3569d..061351568 100644 --- a/pkg/multicloud/ecloud/instancenic.go +++ b/pkg/multicloud/ecloud/instancenic.go @@ -26,40 +26,44 @@ type SInstanceNic struct { instance *SInstance - Id string - PrivateIp string - FipAddress string - FipBandwidthSize int - PortId string - PortName string - FixedIpDetails []SFixedIpDetail - RouterId string + Id string `json:"id,omitempty"` // 网卡ID(等同 PortId) + PrivateIp string `json:"-"` + FipAddress string `json:"-"` + FipBandwidthSize int `json:"-"` + PortId string `json:"portId,omitempty"` // 兼容旧字段 + PortName string `json:"portName,omitempty"` // 兼容旧字段 + FixedIpDetails []SFixedIpDetail `json:"-"` + RouterId string `json:"routerId,omitempty"` } type SInstanceNicDetail struct { - MacAddress string - SecurityGroups []SSecurityGroupRef - Status int - ResourceId string - CreaetTime time.Time - PublicIp string - IpId string - NetworkId string + MacAddress string `json:"macAddress,omitempty"` + SecurityGroups []SSecurityGroupRef `json:"securityGroups,omitempty"` + Status int `json:"status,omitempty"` + ResourceId string `json:"resourceId,omitempty"` + + // CreateTime 正确拼写;保留旧字段 CreaetTime 以兼容历史引用 + CreateTime time.Time `json:"createTime,omitempty"` + CreaetTime time.Time `json:"-"` + + PublicIp string `json:"publicIp,omitempty"` + IpId string `json:"ipId,omitempty"` + NetworkId string `json:"networkId,omitempty"` } type SSecurityGroupRef struct { - Id string - Name string + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` } type SFixedIpDetail struct { - IpAddress string - IpVersion string - PublicIp string - BandWidthSize int - BandWidthType string - SubnetId string - SubnetName string + IpAddress string `json:"ipAddress,omitempty"` + IpVersion string `json:"ipVersion,omitempty"` + PublicIp string `json:"publicIp,omitempty"` + BandWidthSize int `json:"bandWidthSize,omitempty"` + BandWidthType string `json:"bandWidthType,omitempty"` + SubnetId string `json:"subnetId,omitempty"` + SubnetName string `json:"subnetName,omitempty"` } func (in *SInstanceNic) GetIP() string { diff --git a/pkg/multicloud/ecloud/latitud_and_longitude.go b/pkg/multicloud/ecloud/latitud_and_longitude.go index b35ca5f16..af6e4178a 100644 --- a/pkg/multicloud/ecloud/latitud_and_longitude.go +++ b/pkg/multicloud/ecloud/latitud_and_longitude.go @@ -20,32 +20,32 @@ import ( ) var LatitudeAndLongitude = map[string]cloudprovider.SGeographicInfo{ - "guangzhou-2": {Latitude: 23.129110, Longitude: 113.264381, City: api.CITY_GUANG_ZHOU, CountryCode: api.COUNTRY_CODE_CN}, - "beijing-1": {Latitude: 39.904202, Longitude: 116.407394, City: api.CITY_BEI_JING, CountryCode: api.COUNTRY_CODE_CN}, - "hunan-1": {Latitude: 28.2277765095, Longitude: 112.9388453666, City: api.CITY_CHANG_SHA, CountryCode: api.COUNTRY_CODE_CN}, - "wuxi-1": {Latitude: 31.2983479333, Longitude: 120.5831894861, City: api.CITY_SU_ZHOU, CountryCode: api.COUNTRY_CODE_CN}, - "dongguan-1": api.RegionSuzhou, - "yaan-1": {Latitude: 30.572815, Longitude: 104.066803, City: api.CITY_CHENG_DU, CountryCode: api.COUNTRY_CODE_CN}, - "zhengzhou-1": {Latitude: 34.7533581487, Longitude: 113.6313915479, City: api.CITY_ZHENG_ZHOU, CountryCode: api.COUNTRY_CODE_CN}, - "beijing-2": {Latitude: 39.904202, Longitude: 116.407394, City: api.CITY_BEI_JING, CountryCode: api.COUNTRY_CODE_CN}, - "zhuzhou-1": {Latitude: 28.2277765095, Longitude: 112.9388453666, City: api.CITY_CHANG_SHA, CountryCode: api.COUNTRY_CODE_CN}, - "jinan-1": {Latitude: 36.64889911073425, Longitude: 117.11905617575435, City: api.CITY_JI_NAM, CountryCode: api.COUNTRY_CODE_CN}, - "xian-1": {Latitude: 34.3412614674, Longitude: 108.9398165260, City: api.CITY_XI_AN, CountryCode: api.COUNTRY_CODE_CN}, - "shanghai-1": {Latitude: 31.210344, Longitude: 121.455364, City: api.CITY_SHANG_HAI, CountryCode: api.COUNTRY_CODE_CN}, - "chongqing-1": {Latitude: 29.431585, Longitude: 106.912254, City: api.CITY_CHONG_QING, CountryCode: api.COUNTRY_CODE_CN}, - "ningbo-1": {Latitude: 30.274084, Longitude: 120.155067, City: api.CITY_HANG_ZHOU, CountryCode: api.COUNTRY_CODE_CN}, - "tianjin-1": {Latitude: 39.0850853357, Longitude: 117.1993482089, City: api.CITY_TIAN_JIN, CountryCode: api.COUNTRY_CODE_CN}, - "jilin-1": {Latitude: 43.87120919729674, Longitude: 125.3111129463539, City: api.CITY_CHANG_CHUN, CountryCode: api.COUNTRY_CODE_CN}, - "hubei-1": {Latitude: 32.009075721852206, Longitude: 112.13485327119795, City: api.CITY_XIANG_YANG, CountryCode: api.COUNTRY_CODE_CN}, - "jiangxi-1": {Latitude: 28.66278309381472, Longitude: 115.82816199879247, City: api.CITY_NAN_CHANG, CountryCode: api.COUNTRY_CODE_CN}, - "gansu-1": {Latitude: 36.0613769373, Longitude: 103.8341600069, City: api.CITY_LAN_ZHOU, CountryCode: api.COUNTRY_CODE_CN}, - "shanxi-1": {Latitude: 37.8705857132, Longitude: 112.5506634865, City: api.CITY_TAI_YUAN, CountryCode: api.COUNTRY_CODE_CN}, - "liaoning-1": {Latitude: 41.78937667917192, Longitude: 123.43099727316815, City: api.CITY_SHEN_YANG, CountryCode: api.COUNTRY_CODE_CN}, - "yunnan-2": {Latitude: 24.8796595146, Longitude: 102.8332118852, City: api.CITY_KUN_MING, CountryCode: api.COUNTRY_CODE_CN}, - "hebei-1": {Latitude: 38.044044256466684, Longitude: 114.50225031469532, City: api.CITY_SHI_JIA_ZHUANG, CountryCode: api.COUNTRY_CODE_CN}, - "fujian-1": {Latitude: 24.478556505708365, Longitude: 118.0875755539503, City: api.CITY_XIA_MEN, CountryCode: api.COUNTRY_CODE_CN}, - "guangxi-1": {Latitude: 22.8167372565, Longitude: 108.3669005333, City: api.CITY_NAN_NING, CountryCode: api.COUNTRY_CODE_CN}, - "anhui-1": {Latitude: 32.62657438299575, Longitude: 116.99779954519057, City: api.CITY_HUAI_NAN, CountryCode: api.COUNTRY_CODE_CN}, - "huhehaote-1": {Latitude: 40.842358, Longitude: 111.749992, City: api.CITY_HU_HE_HAO_TE, CountryCode: api.COUNTRY_CODE_CN}, - "guiyang-1": {Latitude: 26.6470035286, Longitude: 106.6302113880, City: api.CITY_GUI_YANG, CountryCode: api.COUNTRY_CODE_CN}, + // 对齐最新 RegionId(cn- 前缀),与 regionIdToPoolId 保持一致;尽量复用 compute.RegionXXX + "cn-beijing-1": api.RegionBeijing, + "cn-jiangsu-1": api.RegionSuzhou, // 江苏(无锡附近)复用苏州坐标 + "cn-guangdong-1": api.RegionGuangzhou, // 广东(东莞附近)复用广州坐标 + "cn-sichuan-1": api.RegionChengdu, // 四川(雅安/成都附近)复用成都坐标 + "cn-henan-1": api.RegionZhengzhou, // 河南(郑州) + "cn-hunan-1": api.RegionChangsha, // 湖南(株洲/长沙) + "cn-shandong-1": api.RegionJiNan, // 山东(济南) + "cn-shaanxi-1": api.RegionXian, // 陕西(西安) + "cn-shanghai-1": api.RegionShanghai, // 上海 + "cn-chongqing-1": api.RegionChongqing, // 重庆 + "cn-zhejiang-1": api.RegionHangzhou, // 浙江(宁波/杭州) + "cn-tianjin-1": api.RegionTianjin, // 天津 + "cn-jilin-1": api.RegionChangchun, // 吉林(长春) + "cn-hubei-2": api.RegionXiangyang, // 湖北(襄阳) + "cn-jiangxi-1": api.RegionJiangxi, // 江西(南昌) + "cn-gansu-1": api.RegionLanzhou, // 甘肃(兰州) + "cn-shangxi-1": api.RegionJinzhong, // 山西(太原附近),复用晋中 + "cn-liaoning-1": api.RegionShenyang, // 辽宁(沈阳) + "cn-yunnan-1": api.RegionKunming, // 云南(昆明) + "cn-hebei-1": api.RegionShijiazhuang, // 河北(石家庄) + "cn-fujian-1": api.RegionFujian, // 福建(厦门附近) + "cn-guangxi-1": api.RegionNanning, // 广西(南宁) + "cn-anhui-1": api.RegionHuainan, // 安徽(淮南) + "cn-neimenggu-1": api.RegionHuhehaote, // 内蒙古(呼和浩特) + "cn-guzhou-1": api.RegionGuiyang, // 贵州(贵阳) + "cn-hainan-1": api.RegionHaikou, // 海南(海口) + "cn-xinjiang-1": api.RegionWulumuqi, // 新疆(乌鲁木齐) } diff --git a/pkg/multicloud/ecloud/monitor.go b/pkg/multicloud/ecloud/monitor.go index 85aeb8f09..cabc1c294 100644 --- a/pkg/multicloud/ecloud/monitor.go +++ b/pkg/multicloud/ecloud/monitor.go @@ -16,39 +16,33 @@ package ecloud import ( "context" + "fmt" "strconv" "time" "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" "yunion.io/x/pkg/util/timeutils" - "yunion.io/x/pkg/utils" "yunion.io/x/cloudmux/pkg/cloudprovider" ) -const ( - MONITOR_FETCH_REQUEST_ACTION = "v1.dawn.monitor.fetch" - MONITOR_PRODUCT_REQUEST_ACTION = "v1.dawn.monitor.products" - - MONITOR_FETCH_SERVER_PATH = "/api/edw/edw/api" - - REQUEST_SUCCESS_CODE = "000000" -) - -var ( - noMetricRegion = []string{"guangzhou-2", "beijing-1", "hunan-1"} - - portRegionMap = map[string][]string{ - "8443": {"wuxi-1", "dongguan-1", "yaan-1", "zhengzhou-1", "beijing-2", "zhuzhou-1", "jinan-1", - "xian-1", "shanghai-1", "chongqing-1", "ningbo-1"}, - "18080": {"tianjin-1", "jilin-1", "hubei-1", "jiangxi-1", "gansu-1", "shanxi-1", "liaoning-1", - "yunnan-2", "hebei-1", "fujian-1", "guangxi-1", "anhui-1", "huhehaote-1", "guiyang-1"}, +func getPoolIdByRegionId(regionId string) (string, bool) { + if regionId == "" { + return "", false } -) + // regionIdToPoolId 使用 cn-* 作为 key,这里兼容传入不带 cn- 的情况 + if poolID := regionIdToPoolId[regionId]; poolID != "" { + return poolID, true + } + if poolID := regionIdToPoolId["cn-"+regionId]; poolID != "" { + return poolID, true + } + return "", false +} type Metric struct { - Name string `json:"MetricName"` + Name string `json:"metricName"` } type MetricData struct { @@ -74,26 +68,22 @@ type Entity struct { type Datapoint []string type SMonitorRequest struct { - SApiRequest + SJSONRequest } +// NewMonitorRequest 监控 OpenAPI 统一走 ecloud.10086.cn(443)。 func NewMonitorRequest(regionId string, serverPath string, query map[string]string, data jsonutils.JSONObject) *SMonitorRequest { - apiRequest := NewApiRequest(regionId, serverPath, query, data) - return &SMonitorRequest{*apiRequest} + r := SMonitorRequest{SJSONRequest: newBaseJSONRequest(regionId, "ecloud.10086.cn", "", serverPath, query, data)} + return &r } -func (rr *SMonitorRequest) GetPort() string { - for port, regions := range portRegionMap { - if utils.IsInStringArray(rr.GetRegionId(), regions) { - return port - } - } - return "8443" +func (r *SMonitorRequest) Base() *SBaseRequest { + return &r.SJSONRequest.SBaseRequest } func (br *SMonitorRequest) ForMateResponseBody(jrbody jsonutils.JSONObject) (jsonutils.JSONObject, error) { code, _ := jrbody.GetString("code") - if code != REQUEST_SUCCESS_CODE { + if code != "000000" { message, _ := jrbody.(*jsonutils.JSONDict).GetString("message") return nil, errors.Errorf("rep body code is :%s, message:%s,body:%v", code, message, jrbody) } @@ -111,34 +101,60 @@ func (self *SEcloudClient) DescribeMetricList(regionId, productType string, metr metricData := MetricData{ Entitys: make([]Entity, 0), } + // 新接口要求 query.poolId(资源池 ID) + poolID, ok := getPoolIdByRegionId(regionId) + if !ok { + return metricData, fmt.Errorf("missing poolId mapping for regionId %q", regionId) + } params := map[string]string{ - "eAction": MONITOR_FETCH_REQUEST_ACTION, + "poolId": poolID, } getBody := jsonutils.NewDict() - getBody.Set("startTime", jsonutils.NewString(since.Format(timeutils.MysqlTimeFormat))) - getBody.Set("endTime", jsonutils.NewString(until.Format(timeutils.MysqlTimeFormat))) + sh, _ := time.LoadLocation("Asia/Shanghai") + getBody.Set("startTime", jsonutils.NewString(since.In(sh).Format(timeutils.MysqlTimeFormat))) + getBody.Set("endTime", jsonutils.NewString(until.In(sh).Format(timeutils.MysqlTimeFormat))) getBody.Set("productType", jsonutils.NewString(productType)) getBody.Set("resourceId", jsonutils.NewString(resourceId)) getBody.Set("metrics", jsonutils.Marshal(&metrics)) - request := NewMonitorRequest(regionId, MONITOR_FETCH_SERVER_PATH, params, getBody) - err := self.doGet(context.Background(), request, &metricData) + request := NewMonitorRequest(regionId, "/api/edw/openapi/version2/v1/dawn/monitor/distribute/fetch", params, getBody) + // 新接口为 POST,且返回不遵循通用 state/body,需用 SMonitorRequest.ForMateResponseBody 校验/取数 + base := request.Base() + base.SetMethod("POST") + jrbody, err := self.doRequest(context.Background(), base) if err != nil { - return metricData, errors.Wrap(err, "client doGet error") + return metricData, errors.Wrap(err, "client doRequest error") + } + body, err := request.ForMateResponseBody(jrbody) + if err != nil { + return metricData, err + } + if err := body.Unmarshal(&metricData); err != nil { + return metricData, errors.Wrap(err, "unmarshal metric data") } return metricData, nil } -func (r *SRegion) GetProductTypes() (jsonutils.JSONObject, error) { +func (r *SRegion) GetMetricTypes() (jsonutils.JSONObject, error) { + poolID, ok := getPoolIdByRegionId(r.RegionId) + if !ok { + return nil, fmt.Errorf("missing poolId mapping for regionId %q", r.RegionId) + } params := map[string]string{ - "eAction": MONITOR_PRODUCT_REQUEST_ACTION, + "poolId": poolID, + "productType": "vm", + } + request := NewMonitorRequest(r.RegionId, "/api/edw/openapi/version2/v1/dawn/monitor/distribute/metricindicators", params, nil) + base := request.Base() + base.SetMethod("GET") + jrbody, err := r.client.doRequest(context.Background(), base) + if err != nil { + return nil, errors.Wrap(err, "client doRequest error") } - request := NewMonitorRequest(r.ID, MONITOR_FETCH_SERVER_PATH, params, nil) - rtn := jsonutils.NewDict() - err := r.client.doGet(context.Background(), request, rtn) + body, err := request.ForMateResponseBody(jrbody) if err != nil { - return nil, errors.Wrap(err, "client doGet error") + return nil, err } - return rtn, nil + return body, nil } func (self *SEcloudClient) GetMetrics(opts *cloudprovider.MetricListOptions) ([]cloudprovider.MetricValues, error) { @@ -152,24 +168,20 @@ func (self *SEcloudClient) GetMetrics(opts *cloudprovider.MetricListOptions) ([] func (self *SEcloudClient) GetEcsMetrics(opts *cloudprovider.MetricListOptions) ([]cloudprovider.MetricValues, error) { metrics := map[string]cloudprovider.TMetricType{ - "cpu_util": cloudprovider.VM_METRIC_TYPE_CPU_USAGE, - "memory.util": cloudprovider.VM_METRIC_TYPE_MEM_USAGE, - "disk.device.read.requests.rate": cloudprovider.VM_METRIC_TYPE_DISK_IO_READ_IOPS, - "disk.device.write.requests.rate": cloudprovider.VM_METRIC_TYPE_DISK_IO_WRITE_IOPS, - "disk.device.read.bytes.rate": cloudprovider.VM_METRIC_TYPE_DISK_IO_READ_BPS, - "disk.device.write.bytes.rate": cloudprovider.VM_METRIC_TYPE_DISK_IO_WRITE_BPS, - "network.incoming.bytes": cloudprovider.VM_METRIC_TYPE_NET_BPS_RX, - "network.outgoing.bytes": cloudprovider.VM_METRIC_TYPE_NET_BPS_TX, + "vm_realtime_cpu_avg_util_percent": cloudprovider.VM_METRIC_TYPE_CPU_USAGE, + "vm_realtime_mem_avg_util_percent": cloudprovider.VM_METRIC_TYPE_MEM_USAGE, + "vm_disk_read_bytes_rate": cloudprovider.VM_METRIC_TYPE_DISK_IO_READ_IOPS, + "vm_disk_write_bytes_rate": cloudprovider.VM_METRIC_TYPE_DISK_IO_WRITE_IOPS, + "vm_realtime_allnetwork_incoming_rate": cloudprovider.VM_METRIC_TYPE_NET_BPS_RX, + "vm_realtime_allnetwork_outgoing_rate": cloudprovider.VM_METRIC_TYPE_NET_BPS_TX, } + metricNames := []Metric{} for metric := range metrics { metricNames = append(metricNames, Metric{ Name: metric, }) } - if utils.IsInStringArray(opts.RegionExtId, noMetricRegion) { - return []cloudprovider.MetricValues{}, nil - } data, err := self.DescribeMetricList(opts.RegionExtId, "vm", metricNames, opts.ResourceId, opts.StartTime, opts.EndTime) if err != nil { return nil, errors.Wrapf(err, "DescribeMetricList") diff --git a/pkg/multicloud/ecloud/network.go b/pkg/multicloud/ecloud/network.go index 78703fe9a..848b05e81 100644 --- a/pkg/multicloud/ecloud/network.go +++ b/pkg/multicloud/ecloud/network.go @@ -16,7 +16,9 @@ package ecloud import ( "context" + "fmt" + "yunion.io/x/pkg/errors" "yunion.io/x/pkg/util/netutils" "yunion.io/x/pkg/util/rbacscope" @@ -32,23 +34,20 @@ type SNetwork struct { wire *SWire - Id string - Name string - Shared bool - Enabled bool - EcStatus string - Subnets []SSubnet -} - -type SSubnet struct { - Id string - Name string - NetworkId string - Region string - GatewayIp string - EnableDHCP bool - Cidr string - IpVersion string + // 与 ecloudsdkvpc ListSubnetsResponseContent 对齐 + VpoolId string `json:"vpoolId,omitempty"` + GatewayIp string `json:"gatewayIp,omitempty"` + Vaz string `json:"vaz,omitempty"` + Edge bool `json:"edge,omitempty"` + Deleted bool `json:"deleted,omitempty"` + IpVersion int `json:"ipVersion,omitempty"` + Name string `json:"name,omitempty"` + CreatedTime string `json:"createdTime,omitempty"` + CidrBlock string `json:"cidr,omitempty"` + NetworkId string `json:"networkId,omitempty"` + Id string `json:"id,omitempty"` + NetworkType string `json:"networkType,omitempty"` + Region string `json:"region,omitempty"` } func (n *SNetwork) GetId() string { @@ -64,27 +63,15 @@ func (n *SNetwork) GetGlobalId() string { } func (n *SNetwork) GetStatus() string { - switch n.EcStatus { - case "ACTIVE": - return api.NETWORK_STATUS_AVAILABLE - case "DOWN", "BUILD", "ERROR": - return api.NETWORK_STATUS_UNAVAILABLE - case "PENDING_DELETE": + // 子网接口未直接返回状态,这里视未删除的子网为可用 + if n.Deleted { return api.NETWORK_STATUS_DELETING - case "PENDING_CREATE", "PENDING_UPDATE": - return api.NETWORK_STATUS_PENDING - default: - return api.NETWORK_STATUS_UNKNOWN } + return api.NETWORK_STATUS_AVAILABLE } func (n *SNetwork) Refresh() error { return nil - // nn, err := n.wire.vpc.region.GetNetworkById(n.wire.vpc.RouterId, n.wire.zone.Region, n.GetId()) - // if err != nil { - // return err - // } - // return jsonutils.Update(n, nn) } func (n *SNetwork) IsEmulated() bool { @@ -100,7 +87,11 @@ func (n *SNetwork) GetIWire() cloudprovider.ICloudWire { } func (n *SNetwork) GetIpStart() string { - pref, _ := netutils.NewIPV4Prefix(n.Cidr()) + cidr := n.Cidr() + if cidr == "" { + return "" + } + pref, _ := netutils.NewIPV4Prefix(cidr) startIp := pref.Address.NetAddr(pref.MaskLen) // 0 startIp = startIp.StepUp() // 1 startIp = startIp.StepUp() // 2 @@ -108,23 +99,31 @@ func (n *SNetwork) GetIpStart() string { } func (n *SNetwork) GetIpEnd() string { - pref, _ := netutils.NewIPV4Prefix(n.Cidr()) + cidr := n.Cidr() + if cidr == "" { + return "" + } + pref, _ := netutils.NewIPV4Prefix(cidr) endIp := pref.Address.BroadcastAddr(pref.MaskLen) // 255 endIp = endIp.StepDown() // 254 return endIp.String() } func (n *SNetwork) Cidr() string { - return n.Subnets[0].Cidr + return n.CidrBlock } func (n *SNetwork) GetIpMask() int8 { - pref, _ := netutils.NewIPV4Prefix(n.Cidr()) + cidr := n.Cidr() + if cidr == "" { + return 0 + } + pref, _ := netutils.NewIPV4Prefix(cidr) return pref.MaskLen } func (n *SNetwork) GetGateway() string { - return n.Subnets[0].GatewayIp + return n.GatewayIp } func (self *SNetwork) GetServerType() string { @@ -140,51 +139,44 @@ func (self *SNetwork) GetPublicScope() rbacscope.TRbacScope { } func (self *SNetwork) Delete() error { - return cloudprovider.ErrNotImplemented + return self.wire.vpc.region.DeleteNetwork(self.Id) } func (self *SNetwork) GetAllocTimeoutSeconds() int { return 120 } -func (r *SRegion) GetNetworks(routerId, zoneRegion string) ([]SNetwork, error) { - queryParams := map[string]string{ - "networkTypeEnum": "VM", +// GetNetworks 使用 OpenAPI VPC 子网列表:GET /api/openapi-vpc/customer/v3/subnet(替代旧的 network/NetworkResps 接口) +func (r *SRegion) GetNetworks(vpcId, zoneCode string) ([]SNetwork, error) { + query := map[string]string{"networkTypeEnum": "VM"} + if vpcId != "" { + query["vpcId"] = vpcId } - if len(routerId) > 0 { - queryParams["routerId"] = routerId + if zoneCode != "" { + query["region"] = zoneCode } - if len(zoneRegion) > 0 { - queryParams["region"] = zoneRegion - } - request := NewConsoleRequest(r.ID, "/api/v2/netcenter/network/NetworkResps", queryParams, nil) + req := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/subnet", query, nil) networks := make([]SNetwork, 0) - err := r.client.doList(context.Background(), request, &networks) + err := r.client.doList(context.Background(), req.Base(), &networks) if err != nil { return nil, err } return networks, nil } -func (r *SRegion) GetNetworkById(routerId, zoneRegion, netId string) (*SNetwork, error) { - queryParams := map[string]string{ - "rangeInNetworkIds": netId, - "networkTypeEnum": "VM", - } - if len(routerId) > 0 { - queryParams["routerId"] = routerId - } - if len(zoneRegion) > 0 { - queryParams["region"] = zoneRegion - } - request := NewConsoleRequest(r.ID, "/api/v2/netcenter/network/NetworkResps", queryParams, nil) - networks := make([]SNetwork, 0, 1) - err := r.client.doList(context.Background(), request, &networks) +// GetNetwork 使用 OpenAPI VPC 子网详情:GET /api/openapi-vpc/customer/v3/subnet/subnetId/{subnetId}/SubnetDetailResp +func (r *SRegion) GetNetwork(netId string) (*SNetwork, error) { + base := NewOpenApiVpcRequest(r.RegionId, + fmt.Sprintf("/api/openapi-vpc/customer/v3/subnet/subnetId/%s/SubnetDetailResp", netId), + nil, nil).Base() + base.SetMethod("GET") + data, err := r.client.request(context.Background(), base) if err != nil { return nil, err } - if len(networks) == 0 { - return nil, cloudprovider.ErrNotFound + net := SNetwork{} + if err := data.Unmarshal(&net); err != nil { + return nil, errors.Wrap(err, "Unmarshal subnet detail") } - return &networks[0], nil + return &net, nil } diff --git a/pkg/multicloud/ecloud/provider/provider.go b/pkg/multicloud/ecloud/provider/provider.go index 87b0f5d92..df90722cf 100644 --- a/pkg/multicloud/ecloud/provider/provider.go +++ b/pkg/multicloud/ecloud/provider/provider.go @@ -82,14 +82,13 @@ func (f *SEcloudProviderFactory) GetProvider(cfg cloudprovider.ProviderConfig) ( } client, err := ecloud.NewEcloudClient( - ecloud.NewEcloudClientConfig( - ecloud.NewRamRoleSigner(account, cfg.Secret), - ).SetCloudproviderConfig(cfg), + ecloud.NewEcloudClientConfig(account, cfg.Secret). + SetCloudproviderConfig(cfg), ) if err != nil { return nil, err } - err = client.TryConnect() + _, err = client.GetRegions() if err != nil { return nil, err } @@ -154,11 +153,17 @@ func (p *SEcloudProvider) GetIRegionById(id string) (cloudprovider.ICloudRegion, } func (p *SEcloudProvider) GetBalance() (*cloudprovider.SBalanceInfo, error) { - return &cloudprovider.SBalanceInfo{ - Amount: 0.0, - Currency: "CNY", - Status: api.CLOUD_PROVIDER_HEALTH_NORMAL, - }, cloudprovider.ErrNotSupported + balance, err := p.client.GetBalance() + if err != nil { + return nil, err + } + balance.Status = api.CLOUD_PROVIDER_HEALTH_NORMAL + if balance.Amount < 0 { + balance.Status = api.CLOUD_PROVIDER_HEALTH_ARREARS + } else if balance.Amount < 400 { + balance.Status = api.CLOUD_PROVIDER_HEALTH_INSUFFICIENT + } + return balance, nil } func (p *SEcloudProvider) GetIProjects() ([]cloudprovider.ICloudProject, error) { diff --git a/pkg/multicloud/ecloud/region.go b/pkg/multicloud/ecloud/region.go index 8a72b525b..89f8d17e7 100644 --- a/pkg/multicloud/ecloud/region.go +++ b/pkg/multicloud/ecloud/region.go @@ -17,7 +17,11 @@ package ecloud import ( "context" "fmt" + "strconv" + "strings" + "time" + "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" api "yunion.io/x/cloudmux/pkg/apis/compute" @@ -25,62 +29,30 @@ import ( "yunion.io/x/cloudmux/pkg/multicloud" ) -var regionList = map[string]string{ - "guangzhou-2": "华南-广州2", - "beijing-1": "华北-北京1", - "hunan-1": "华中-长沙1", - "wuxi-1": "华东-苏州", - "dongguan-1": "华南-广州3", - "yaan-1": "西南-成都", - "zhengzhou-1": "华中-郑州", - "beijing-2": "华北-北京3", - "zhuzhou-1": "华中-长沙2", - "jinan-1": "华东-济南", - "xian-1": "西北-西安", - "shanghai-1": "华东-上海1", - "chongqing-1": "西南-重庆", - "ningbo-1": "华东-杭州", - "tianjin-1": "天津-天津", - "jilin-1": "吉林-长春", - "hubei-1": "湖北-襄阳", - "jiangxi-1": "江西-南昌", - "gansu-1": "甘肃-兰州", - "shanxi-1": "山西-太原", - "liaoning-1": "辽宁-沈阳", - "yunnan-2": "云南-昆明2", - "hebei-1": "河北-石家庄", - "fujian-1": "福建-厦门", - "guangxi-1": "广西-南宁", - "anhui-1": "安徽-淮南", - "huhehaote-1": "华北-呼和浩特", - "guiyang-1": "西南-贵阳", -} - +// SOpenApiRegion 用于接收 OpenAPI 区域列表返回的单个区域结构 type SRegion struct { cloudprovider.SFakeOnPremiseRegion multicloud.SRegion multicloud.SNoObjectStorageRegion - client *SEcloudClient - storageCache *SStoragecache - - ID string `json:"id"` - Name string `json:"Name"` + client *SEcloudClient - izones []cloudprovider.ICloudZone - ivpcs []cloudprovider.ICloudVpc + RegionId string `json:"regionId"` + RegionCode string `json:"regionCode"` + RegionName string `json:"regionName"` + RegionArea string `json:"regionArea"` } func (r *SRegion) GetId() string { - return r.ID + return r.RegionId } func (r *SRegion) GetName() string { - return r.Name + return r.RegionName } func (r *SRegion) GetGlobalId() string { - return fmt.Sprintf("%s/%s", r.client.GetAccessEnv(), r.ID) + return fmt.Sprintf("%s/%s", r.client.GetAccessEnv(), r.RegionId) } func (r *SRegion) GetStatus() string { @@ -88,11 +60,6 @@ func (r *SRegion) GetStatus() string { } func (r *SRegion) Refresh() error { - // err := r.fetchZones() - // if err != nil { - // return err - // } - // return r.fetchVpcs() return nil } @@ -101,115 +68,266 @@ func (r *SRegion) IsEmulated() bool { } func (r *SRegion) GetI18n() cloudprovider.SModelI18nTable { - en := fmt.Sprintf("%s %s", CLOUD_PROVIDER_ECLOUD_EN, r.Name) table := cloudprovider.SModelI18nTable{} - table["name"] = cloudprovider.NewSModelI18nEntry(r.GetName()).CN(r.GetName()).EN(en) return table } // GetLatitude() float32 // GetLongitude() float32 func (r *SRegion) GetGeographicInfo() cloudprovider.SGeographicInfo { - if info, ok := LatitudeAndLongitude[r.ID]; ok { + if info, ok := LatitudeAndLongitude[r.RegionId]; ok { return info } return cloudprovider.SGeographicInfo{} } func (r *SRegion) GetIZones() ([]cloudprovider.ICloudZone, error) { - if r.izones == nil { - err := r.fetchZones() - if err != nil { - return nil, err - } - } - return r.izones, nil -} - -func (r *SRegion) fetchZones() error { - request := NewNovaRequest(NewApiRequest(r.ID, "/api/v2/region", - map[string]string{"component": "NOVA"}, nil)) - zones := make([]SZone, 0) - err := r.client.doList(context.Background(), request, &zones) + zones, err := r.GetZones() if err != nil { - return err + return nil, err } izones := make([]cloudprovider.ICloudZone, len(zones)) for i := range zones { - zones[i].region = r - zones[i].host = &SHost{ - zone: &zones[i], - } izones[i] = &zones[i] } - r.izones = izones - return nil + return izones, nil } -func (r *SRegion) fetchVpcs() error { - vpcs, err := r.getVpcs() - if err != nil { - return err +func (r *SRegion) GetZones() ([]SZone, error) { + zones := make([]SZone, 0) + req := NewOpenApiZoneRequest(r.RegionId, nil) + if err := r.client.doList(context.Background(), req.Base(), &zones); err != nil { + return nil, errors.Wrap(err, "GetZones") } - ivpcs := make([]cloudprovider.ICloudVpc, len(vpcs)) - for i := range vpcs { - ivpcs[i] = &vpcs[i] + for i := range zones { + zones[i].region = r } - r.ivpcs = ivpcs - return nil + return zones, nil } -func (r *SRegion) getVpcs() ([]SVpc, error) { - request := NewConsoleRequest(r.ID, "/api/v2/netcenter/vpc", nil, nil) +func (r *SRegion) GetVpcs() ([]SVpc, error) { + req := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/vpc", nil, nil) vpcs := make([]SVpc, 0) - err := r.client.doList(context.Background(), request, &vpcs) - if err != nil { + if err := r.client.doList(context.Background(), req.Base(), &vpcs); err != nil { return nil, err } for i := range vpcs { vpcs[i].region = r } - return vpcs, err + return vpcs, nil } -func (r *SRegion) getVpcById(id string) (*SVpc, error) { - request := NewConsoleRequest(r.ID, fmt.Sprintf("/api/v2/netcenter/vpc/%s", id), nil, nil) - vpc := SVpc{} - err := r.client.doGet(context.Background(), request, &vpc) +func (r *SRegion) GetVpc(id string) (*SVpc, error) { + base := NewOpenApiVpcRequest(r.RegionId, fmt.Sprintf("/api/openapi-vpc/customer/v3/vpc/%s", id), nil, nil).Base() + base.SetMethod("GET") + resp, err := r.client.request(context.Background(), base) if err != nil { - return nil, err + // 可能传入的是 routerId,再试详情接口 by routerId + return r.getVpcByRouterId(id) + } + vpc := SVpc{} + if err := resp.Unmarshal(&vpc); err != nil { + return nil, errors.Wrap(err, "Unmarshal vpc") } vpc.region = r - return &vpc, err + return &vpc, nil } -func (r *SRegion) getVpcByRouterId(id string) (*SVpc, error) { - request := NewConsoleRequest(r.ID, fmt.Sprintf("/api/v2/netcenter/vpc/router/%s", id), nil, nil) - vpc := SVpc{} - err := r.client.doGet(context.Background(), request, &vpc) +func (r *SRegion) getVpcByRouterId(routerId string) (*SVpc, error) { + base := NewOpenApiVpcRequest(r.RegionId, fmt.Sprintf("/api/openapi-vpc/customer/v3/vpc/router/%s", routerId), nil, nil).Base() + base.SetMethod("GET") + resp, err := r.client.request(context.Background(), base) if err != nil { return nil, err } + vpc := SVpc{} + if err := resp.Unmarshal(&vpc); err != nil { + return nil, errors.Wrap(err, "Unmarshal vpc") + } vpc.region = r - return &vpc, err + return &vpc, nil } -func (r *SRegion) GetIVpcs() ([]cloudprovider.ICloudVpc, error) { - if r.ivpcs == nil { - err := r.fetchVpcs() +// CreateVpc 创建 VPC,OpenAPI POST /api/openapi-vpc/customer/v3/order/create/vpc。 +// name/networkName 须 5-22 位、字母开头;body 使用 vpcOrderCreateBody 包装以兼容网关。 +func (r *SRegion) CreateVpc(opts *cloudprovider.VpcCreateOptions) (*SVpc, error) { + name := opts.NAME + if name == "" { + name = "vpc-default" + } + // 规范:必须以字母开头,长度 5-22(数字、字母、下划线) + if name[0] < 'a' || name[0] > 'z' { + if name[0] < 'A' || name[0] > 'Z' { + name = "v" + name + } + } + if len(name) < 5 { + pad := 5 - len(name) + if pad > 4 { + pad = 4 + } + name = name + "xxxx"[:pad] + } + if len(name) > 22 { + name = name[:22] + } + cidr := opts.CIDR + if cidr == "" { + cidr = "192.168.0.0/16" + } + networkName := name + "-subnet1" + if len(networkName) > 22 { + networkName = name + "-s1" + if len(networkName) > 22 { + networkName = name[:20] + "-s1" + } + } + poolID := regionIdToPoolId[r.RegionId] + if poolID == "" { + poolID = r.RegionId + } + inner := jsonutils.NewDict() + inner.Set("name", jsonutils.NewString(name)) + inner.Set("cidr", jsonutils.NewString(cidr)) + inner.Set("networkName", jsonutils.NewString(networkName)) + inner.Set("region", jsonutils.NewString(poolID)) + inner.Set("specs", jsonutils.NewString("high")) + inner.Set("networkTypeEnum", jsonutils.NewString("VM")) + if opts.Desc != "" { + inner.Set("description", jsonutils.NewString(opts.Desc)) + } + body := jsonutils.NewDict() + body.Set("vpcOrderCreateBody", inner) + base := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/order/create/vpc", nil, body).Base() + base.SetMethod("POST") + _, err := r.client.request(context.Background(), base) + if err != nil { + if strings.Contains(err.Error(), "该可用区暂无可用的网络资源") || strings.Contains(err.Error(), "THIS_AZ_MUST_AUTH") { + return nil, errors.Wrap(err, "当前区域/可用区暂无可用网络资源或需开通权限,请尝试其他区域或联系移动云") + } + return nil, errors.Wrap(err, "CreateVpc") + } + // 创建为订购接口,返回 orderId;轮询列表按名称查找新 VPC + for retry := 0; retry < 3; retry++ { + if retry > 0 { + time.Sleep(time.Duration(3+retry*2) * time.Second) + } + vpcs, err := r.GetVpcs() if err != nil { - return nil, err + continue + } + for i := range vpcs { + if vpcs[i].Name == name { + vpcs[i].region = r + return &vpcs[i], nil + } + } + } + return nil, errors.Wrap(errors.ErrNotFound, "created vpc not found in list yet") +} + +// DeleteVpc 退订 VPC,OpenAPI POST /api/openapi-vpc/customer/v3/order/delete。 +// 参数 idOrRouterId 可为 vpc Id 或 routerId;若为 vpc Id 会先查详情取 routerId 再退订。 +func (r *SRegion) DeleteVpc(idOrRouterId string) error { + routerId := idOrRouterId + if vpc, err := r.GetVpc(idOrRouterId); err == nil { + routerId = vpc.RouterId + } + commonBody := jsonutils.NewDict() + commonBody.Set("resourceId", jsonutils.NewString(routerId)) + commonBody.Set("productType", jsonutils.NewString("router")) + reqBody := jsonutils.NewDict() + reqBody.Set("commonMopOrderDeleteVpcBody", commonBody) + base := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/order/delete", nil, reqBody).Base() + base.SetMethod("POST") + _, err := r.client.request(context.Background(), base) + return errors.Wrap(err, "DeleteVpc") +} + +// DeleteNetwork 删除 VPC 下网络(子网),与 ecloudsdkvpc DeleteNetwork 一致。 +func (r *SRegion) DeleteNetwork(networkId string) error { + base := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/network/"+networkId, nil, nil).Base() + base.SetMethod("DELETE") + _, err := r.client.request(context.Background(), base) + return errors.Wrap(err, "DeleteNetwork") +} + +// CreateNetwork 在 VPC 下创建网络(子网),与 ecloudsdkvpc CreateNetwork 一致。 +// networkName 须 5-22 位、字母开头;cidr 如 192.168.1.0/24。 +func (r *SRegion) CreateNetwork(routerId, regionPoolId, networkName, cidr string) (*SNetwork, error) { + body := jsonutils.NewDict() + inner := jsonutils.NewDict() + inner.Set("routerId", jsonutils.NewString(routerId)) + inner.Set("networkName", jsonutils.NewString(networkName)) + inner.Set("networkTypeEnum", jsonutils.NewString("VM")) + if regionPoolId != "" { + inner.Set("availabilityZoneHints", jsonutils.NewString(regionPoolId)) + } + subnet := jsonutils.NewDict() + subnet.Set("cidr", jsonutils.NewString(cidr)) + subnet.Set("ipVersion", jsonutils.NewString("4")) + inner.Set("subnets", jsonutils.NewArray(subnet)) + body.Set("createNetworkBody", inner) + base := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/network", nil, body).Base() + base.SetMethod("POST") + data, err := r.client.request(context.Background(), base) + if err != nil { + return nil, errors.Wrap(err, "CreateNetwork") + } + // 响应可能含 body(网络 id)或直接返回网络信息 + var networkId string + if data.Contains("body") { + networkId, _ = data.GetString("body") + } + if networkId == "" && data.Contains("id") { + networkId, _ = data.GetString("id") + } + if networkId == "" { + // 通过名称再查一次 + networks, listErr := r.GetNetworks(routerId, "") + if listErr != nil { + return nil, errors.Wrapf(listErr, "CreateNetwork succeeded but list networks failed") + } + for i := range networks { + if networks[i].Name == networkName { + return &networks[i], nil + } } + return nil, errors.Errorf("CreateNetwork succeeded but could not find created network by name %q", networkName) + } + return r.GetNetwork(networkId) +} + +func (r *SRegion) GetIVpcs() ([]cloudprovider.ICloudVpc, error) { + vpcs, err := r.GetVpcs() + if err != nil { + return nil, err + } + ivpcs := make([]cloudprovider.ICloudVpc, len(vpcs)) + for i := range vpcs { + ivpcs[i] = &vpcs[i] } - return r.ivpcs, nil + return ivpcs, nil } func (r *SRegion) GetIEips() ([]cloudprovider.ICloudEIP, error) { - return nil, cloudprovider.ErrNotSupported + // 使用 OpenAPI EIP 公网 IP 列表(含带宽信息): + // GET /api/openapi-eip/acl/v3/floatingip/listWithBw + base := NewOpenApiEbsRequest(r.RegionId, "/api/openapi-eip/acl/v3/floatingip/listWithBw", nil, nil).Base() + eips := make([]SEip, 0, 20) + if err := r.client.doList(context.Background(), base, &eips); err != nil { + return nil, err + } + ret := make([]cloudprovider.ICloudEIP, len(eips)) + for i := range eips { + eips[i].region = r + ret[i] = &eips[i] + } + return ret, nil } func (r *SRegion) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) { - vpc, err := r.getVpcById(id) + vpc, err := r.GetVpc(id) if err != nil { return nil, err } @@ -217,14 +335,22 @@ func (r *SRegion) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) { return vpc, nil } +func (r *SRegion) CreateIVpc(opts *cloudprovider.VpcCreateOptions) (cloudprovider.ICloudVpc, error) { + vpc, err := r.CreateVpc(opts) + if err != nil { + return nil, err + } + return vpc, nil +} + func (r *SRegion) GetIZoneById(id string) (cloudprovider.ICloudZone, error) { - izones, err := r.GetIZones() + zones, err := r.GetZones() if err != nil { return nil, err } - for i := 0; i < len(izones); i += 1 { - if izones[i].GetGlobalId() == id { - return izones[i], nil + for i := 0; i < len(zones); i += 1 { + if zones[i].GetGlobalId() == id || zones[i].GetId() == id { + return &zones[i], nil } } return nil, cloudprovider.ErrNotFound @@ -235,24 +361,93 @@ func (r *SRegion) GetIEipById(id string) (cloudprovider.ICloudEIP, error) { } func (r *SRegion) GetIVMById(id string) (cloudprovider.ICloudVM, error) { - vm, err := r.GetInstanceById(id) + vm, err := r.GetInstance(id) if err != nil { return nil, err } - zone, err := r.FindZone(vm.Region) + zones, err := r.GetZones() if err != nil { return nil, err } - vm.host = &SHost{ - zone: zone, + for i := range zones { + if zones[i].ZoneId == vm.ZoneId { + vm.host = &SHost{ + zone: &zones[i], + } + return vm, nil + } } - return vm, nil + return nil, cloudprovider.ErrNotFound } func (r *SRegion) GetIDiskById(id string) (cloudprovider.ICloudDisk, error) { return r.GetDisk(id) } +// ResizeDisk 扩容云盘,使用 OpenAPI /api/v2/volume/volume/change/ebs,与 ecloudsdkebs ChangeVolume 一致。 +// newSizeGB 为目标容量(GB),只做直通调用,参数合法性由上层调用方保证。 +func (r *SRegion) ResizeDisk(ctx context.Context, diskId string, newSizeGB int64) error { + body := jsonutils.NewDict() + body.Set("size", jsonutils.NewInt(newSizeGB)) + body.Set("changeType", jsonutils.NewString("CHANGE")) + body.Set("volumeId", jsonutils.NewString(diskId)) + reqBody := jsonutils.NewDict() + reqBody.Set("changeVolumeBody", body) + base := NewOpenApiEbsRequest(r.RegionId, "/api/v2/volume/volume/change/ebs", nil, reqBody).Base() + base.SetMethod("POST") + _, err := r.client.request(ctx, base) + return err +} + +// PreDeleteVolume 退订/删除云盘(预删除),与 ecloudsdkebs PreDeleteResources 一致。 +func (r *SRegion) PreDeleteVolume(volumeId string) error { + body := jsonutils.NewDict() + body.Set("resourceId", jsonutils.NewString(volumeId)) + body.Set("resourceType", jsonutils.NewString("VOLUME")) + reqBody := jsonutils.NewDict() + reqBody.Set("preDeleteResourcesBody", body) + base := NewOpenApiEbsRequest(r.RegionId, "/api/ebs/acl/v3/common/resource/preDelete", nil, reqBody).Base() + base.SetMethod("POST") + _, err := r.client.request(context.Background(), base) + return errors.Wrap(err, "PreDeleteVolume") +} + +// CreateEbsSnapshot 创建云盘快照,与 ecloudsdkebs CreateSnapshot 一致。 +func (r *SRegion) CreateEbsSnapshot(volumeId, name, description string) (string, error) { + snapBody := jsonutils.NewDict() + snapBody.Set("volumeId", jsonutils.NewString(volumeId)) + snapBody.Set("name", jsonutils.NewString(name)) + if description != "" { + snapBody.Set("description", jsonutils.NewString(description)) + } + reqBody := jsonutils.NewDict() + reqBody.Set("createSnapshotBody", snapBody) + base := NewOpenApiEbsRequest(r.RegionId, "/api/v2/volume/openApi/volumeSnapshot/create", nil, reqBody).Base() + base.SetMethod("POST") + data, err := r.client.request(context.Background(), base) + if err != nil { + return "", errors.Wrap(err, "CreateEbsSnapshot") + } + // 响应可能含 snapshotId 或 id + if data.Contains("snapshotId") { + id, _ := data.GetString("snapshotId") + return id, nil + } + if data.Contains("id") { + id, _ := data.GetString("id") + return id, nil + } + return "", errors.Errorf("CreateEbsSnapshot response has no snapshotId/id: %s", data) +} + +// DeleteEbsSnapshot 删除云盘快照,与 ecloudsdkebs Deletes 一致。 +func (r *SRegion) DeleteEbsSnapshot(snapshotId string) error { + base := NewOpenApiEbsRequest(r.RegionId, "/api/ebs/acl/v3/openApi/volumeSnapshot/"+snapshotId, nil, nil).Base() + base.SetMethod("DELETE") + _, err := r.client.request(context.Background(), base) + return errors.Wrap(err, "DeleteEbsSnapshot") +} + func (r *SRegion) GetIHosts() ([]cloudprovider.ICloudHost, error) { izones, err := r.GetIZones() if err != nil { @@ -325,6 +520,73 @@ func (r *SRegion) GetIStoragecacheById(id string) (cloudprovider.ICloudStorageca return nil, cloudprovider.ErrNotFound } +type SPoolZoneInfo struct { + ZoneId string `json:"zoneId"` + ZoneName string `json:"zoneName"` + ZoneCode string `json:"zoneCode"` +} +type SPoolInfo struct { + ZoneInfo []SPoolZoneInfo `json:"zoneInfo"` + PoolId string `json:"poolId"` + PoolArea string `json:"poolArea"` + ProductType string `json:"productType"` + PoolName string `json:"poolName"` +} +type SPoolInfosRespBody struct { + PoolList []SPoolInfo `json:"poolList"` +} + +func (r *SRegion) GetPoolInfo(productType string) ([]SPoolInfo, error) { + query := map[string]string{ + "productType": productType, + } + base := NewOpenApiEbsRequest(r.RegionId, "/api/ebs/acl/v3/mop/common/getPoolInfo", query, nil).Base() + base.SetMethod("GET") + data, err := r.client.request(context.Background(), base) + if err != nil { + return nil, err + } + resp := SPoolInfosRespBody{} + if err := data.Unmarshal(&resp); err != nil { + return nil, err + } + return resp.PoolList, nil +} + +// GetStorages 返回当前区域(可选指定可用区)的可用存储列表。 +// 若 zoneCode 为空,则返回所有可用区;否则仅返回指定可用区的存储。 +func (r *SRegion) GetStorages(zoneCode string) ([]SStorage, error) { + volumeConfig, err := r.GetVolumeConfig() + if err != nil { + return nil, err + } + ret := make([]SStorage, 0) + for _, config := range volumeConfig { + if config.Region == zoneCode { + ret = append(ret, config) + } + } + ret = append(ret, SStorage{ + StorageType: api.STORAGE_ECLOUD_LOCAL, + Region: zoneCode, + }) + return ret, nil +} + +func (r *SRegion) GetVolumeConfig() ([]SStorage, error) { + base := NewOpenApiEbsRequest(r.RegionId, "/api/ebs/customer/v3/volume/volumeType/list", nil, nil).Base() + base.SetMethod("GET") + data, err := r.client.request(context.Background(), base) + if err != nil { + return nil, errors.Wrap(err, "GetVolumeConfigs") + } + resp := []SStorage{} + if err := data.Unmarshal(&resp); err != nil { + return nil, errors.Wrap(err, "Unmarshal volume config") + } + return resp, nil +} + func (r *SRegion) GetProvider() string { return api.CLOUD_PROVIDER_ECLOUD } @@ -337,25 +599,26 @@ func (r *SRegion) GetClient() *SEcloudClient { return r.client } -func (r *SRegion) FindZone(zoneRegion string) (*SZone, error) { - izones, err := r.GetIZones() +// NewPlaceholderRegion 返回仅持有 client 的 SRegion,用于无有效 region 时仍可执行 region-list(如 OpenAPI 失败导致列表为空)。 +func NewPlaceholderRegion(client *SEcloudClient, regionId string) *SRegion { + return &SRegion{RegionId: regionId, client: client} +} + +func (r *SRegion) FindZone(zoneCode string) (*SZone, error) { + zones, err := r.GetZones() if err != nil { return nil, errors.Wrap(err, "unable to GetZones") } - findZone := func(zoneRegion string) *SZone { - for i := range izones { - zone := izones[i].(*SZone) - if zone.Region == zoneRegion { - return zone - } + for i := range zones { + if zones[i].ZoneCode == zoneCode { + return &zones[i], nil } - return nil } - return findZone(zoneRegion), nil + return nil, cloudprovider.ErrNotFound } func (region *SRegion) GetIVMs() ([]cloudprovider.ICloudVM, error) { - vms, err := region.GetInstances(region.ID) + vms, err := region.GetInstances("", "") if err != nil { return nil, errors.Wrap(err, "GetVMs") } @@ -365,3 +628,200 @@ func (region *SRegion) GetIVMs() ([]cloudprovider.ICloudVM, error) { } return ivms, nil } + +func (r *SRegion) GetSecurityGroups() ([]SSecurityGroup, error) { + // --- 安全组 OpenAPI(ecloudsdkvpc 路径,使用 console 主机)--- + query := map[string]string{ + "region": r.RegionId, + } + req := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/SecurityGroup", query, nil) + sgs := make([]SSecurityGroup, 0, 8) + if err := r.client.doList(context.Background(), req.Base(), &sgs); err != nil { + return nil, err + } + for i := range sgs { + sgs[i].region = r + } + return sgs, nil +} + +// GetSecurityGroup 按 ID 获取安全组,返回底层 SSecurityGroup 结构。 +func (r *SRegion) GetSecurityGroup(id string) (*SSecurityGroup, error) { + base := NewOpenApiVpcRequest(r.RegionId, fmt.Sprintf("/api/openapi-vpc/customer/v3/SecurityGroup/%s", id), nil, nil).Base() + base.SetMethod("GET") + resp, err := r.client.request(context.Background(), base) + if err != nil { + return nil, err + } + sg := SSecurityGroup{} + if err := resp.Unmarshal(&sg); err != nil { + return nil, errors.Wrap(err, "Unmarshal security group") + } + sg.region = r + return &sg, nil +} + +func (r *SRegion) CreateSecurityGroup(opts *cloudprovider.SecurityGroupCreateInput) (*SSecurityGroup, error) { + name := opts.Name + if name == "" { + name = "sg-default" + } + poolID := regionIdToPoolId[r.RegionId] + if poolID == "" { + poolID = r.RegionId + } + // SDK 发送的 Body 为 createSecurityGroupBody 内层,与 VPC 一致 + body := jsonutils.NewDict() + body.Set("name", jsonutils.NewString(name)) + body.Set("region", jsonutils.NewString(poolID)) + body.Set("type", jsonutils.NewString("VM")) + if opts.Desc != "" { + body.Set("description", jsonutils.NewString(opts.Desc)) + } + base := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/SecurityGroup", nil, body).Base() + base.SetMethod("POST") + resp, err := r.client.request(context.Background(), base) + if err != nil { + return nil, errors.Wrap(err, "CreateSecurityGroup") + } + sg := SSecurityGroup{} + if err := resp.Unmarshal(&sg); err != nil { + return nil, errors.Wrap(err, "Unmarshal created security group") + } + sg.region = r + return &sg, nil +} + +func (r *SRegion) DeleteSecurityGroup(id string) error { + base := NewOpenApiVpcRequest(r.RegionId, fmt.Sprintf("/api/openapi-vpc/customer/v3/SecurityGroup/%s", id), nil, nil).Base() + base.SetMethod("DELETE") + _, err := r.client.request(context.Background(), base) + return errors.Wrap(err, "DeleteSecurityGroup") +} + +func (r *SRegion) GetSecurityGroupRules(sgId string) ([]SSecurityGroupRule, error) { + query := map[string]string{"securityGroupId": sgId, "page": "1", "pageSize": "100"} + req := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/SecurityGroupRule", query, nil) + rules := make([]SSecurityGroupRule, 0) + if err := r.client.doList(context.Background(), req.Base(), &rules); err != nil { + return nil, err + } + for i := range rules { + rules[i].region = r + rules[i].SecgroupId = sgId + } + return rules, nil +} + +func (r *SRegion) CreateSecurityGroupRule(sgId string, opts *cloudprovider.SecurityGroupRuleCreateOptions) (*SSecurityGroupRule, error) { + body := jsonutils.NewDict() + body.Set("securityGroupId", jsonutils.NewString(sgId)) + body.Set("remoteType", jsonutils.NewString("cidr")) + body.Set("direction", jsonutils.NewString(string(opts.Direction))) + proto := strings.ToUpper(opts.Protocol) + if proto == "" || proto == "ANY" { + proto = "ANY" + } + body.Set("protocol", jsonutils.NewString(proto)) + if opts.CIDR != "" { + body.Set("remoteIpPrefix", jsonutils.NewString(opts.CIDR)) + } + if opts.Desc != "" { + body.Set("description", jsonutils.NewString(opts.Desc)) + } + minPort, maxPort := parsePorts(opts.Ports) + if minPort >= 0 { + body.Set("minPortRange", jsonutils.NewInt(int64(minPort))) + } + if maxPort >= 0 { + body.Set("maxPortRange", jsonutils.NewInt(int64(maxPort))) + } + base := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/SecurityGroupRule", nil, body).Base() + base.SetMethod("POST") + resp, err := r.client.request(context.Background(), base) + if err != nil { + return nil, errors.Wrap(err, "CreateSecurityGroupRule") + } + rule := SSecurityGroupRule{} + if err := resp.Unmarshal(&rule); err != nil { + return nil, errors.Wrap(err, "Unmarshal created rule") + } + rule.region = r + rule.SecgroupId = sgId + return &rule, nil +} + +func (r *SRegion) DeleteSecurityGroupRule(ruleId string) error { + base := NewOpenApiVpcRequest(r.RegionId, fmt.Sprintf("/api/openapi-vpc/customer/v3/SecurityGroupRule/%s", ruleId), nil, nil).Base() + base.SetMethod("DELETE") + _, err := r.client.request(context.Background(), base) + return errors.Wrap(err, "DeleteSecurityGroupRule") +} + +func (r *SRegion) UpdatePortSecurityGroups(portId string, securityGroupIds []string) error { + body := jsonutils.NewDict() + body.Set("id", jsonutils.NewString(portId)) + arr := jsonutils.NewArray() + for _, id := range securityGroupIds { + arr.Add(jsonutils.NewString(id)) + } + body.Set("securityGroupIds", arr) + reqBody := jsonutils.NewDict() + reqBody.Set("updatePortSecurityGroupsBody", body) + base := NewOpenApiVpcRequest(r.RegionId, "/api/openapi-vpc/customer/v3/port/portSecurityGroups", nil, reqBody).Base() + base.SetMethod("PUT") + _, err := r.client.request(context.Background(), base) + return errors.Wrap(err, "UpdatePortSecurityGroups") +} + +func (r *SRegion) GetISecurityGroups() ([]cloudprovider.ICloudSecurityGroup, error) { + sgs, err := r.GetSecurityGroups() + if err != nil { + return nil, err + } + ret := make([]cloudprovider.ICloudSecurityGroup, len(sgs)) + for i := range sgs { + ret[i] = &sgs[i] + } + return ret, nil +} + +func (r *SRegion) GetISecurityGroupById(id string) (cloudprovider.ICloudSecurityGroup, error) { + sg, err := r.GetSecurityGroup(id) + if err != nil { + return nil, err + } + return sg, nil +} + +func (r *SRegion) CreateISecurityGroup(opts *cloudprovider.SecurityGroupCreateInput) (cloudprovider.ICloudSecurityGroup, error) { + sg, err := r.CreateSecurityGroup(opts) + if err != nil { + return nil, err + } + return sg, nil +} + +// parsePorts 解析 "80" 或 "80-443" 为 min, max;无法解析返回 -1,-1。 +func parsePorts(ports string) (minPort, maxPort int32) { + if ports == "" { + return -1, -1 + } + parts := strings.Split(ports, "-") + if len(parts) == 1 { + p, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 32) + if err != nil { + return -1, -1 + } + return int32(p), int32(p) + } + if len(parts) == 2 { + p1, e1 := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 32) + p2, e2 := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 32) + if e1 != nil || e2 != nil { + return -1, -1 + } + return int32(p1), int32(p2) + } + return -1, -1 +} diff --git a/pkg/multicloud/ecloud/request.go b/pkg/multicloud/ecloud/request.go index 9c1039808..dcddc4108 100644 --- a/pkg/multicloud/ecloud/request.go +++ b/pkg/multicloud/ecloud/request.go @@ -16,7 +16,6 @@ package ecloud import ( "bytes" - "fmt" "io" "strings" "time" @@ -28,27 +27,6 @@ import ( "yunion.io/x/cloudmux/pkg/cloudprovider" ) -type IRequest interface { - GetScheme() string - GetMethod() string - SetMethod(string) - GetEndpoint() string - GetServerPath() string - GetPort() string - GetRegionId() string - GetHeaders() map[string]string - GetQueryParams() map[string]string - GetBodyReader() io.Reader - GetVersion() string - GetTimestamp() string - GetReadTimeout() time.Duration - GetConnectTimeout() time.Duration - GetHTTPSInsecure() bool - SetHTTPSInsecure(bool) - GetUserAgent() map[string]string - ForMateResponseBody(jrbody jsonutils.JSONObject) (jsonutils.JSONObject, error) -} - type SJSONRequest struct { SBaseRequest Data jsonutils.JSONObject @@ -64,6 +42,19 @@ func NewJSONRequest(data jsonutils.JSONObject) *SJSONRequest { return jr } +func newBaseJSONRequest(regionId string, endpoint string, port string, serverPath string, query map[string]string, data jsonutils.JSONObject) SJSONRequest { + req := *NewJSONRequest(data) + req.SBaseRequest.Endpoint = endpoint + req.SBaseRequest.Port = port + req.SBaseRequest.RegionId = regionId + req.SBaseRequest.ServerPath = serverPath + if data != nil { + req.SBaseRequest.Content = []byte(data.String()) + } + mergeMap(req.GetQueryParams(), query) + return req +} + func (jr *SJSONRequest) GetBodyReader() io.Reader { if jr.Data == nil { return nil @@ -71,58 +62,357 @@ func (jr *SJSONRequest) GetBodyReader() io.Reader { return strings.NewReader(jr.Data.String()) } -type SApiRequest struct { +// SOpenApiInstanceRequest 用于调用新的 OpenAPI 云主机实例列表接口: +// 走区域 endpoint (api-xxx.cmecloud.cn:8443),路径为 /api/openapi-instance/v4/list/describe-instances +type SOpenApiInstanceRequest struct { SJSONRequest RegionId string } -func NewApiRequest(regionId string, serverPath string, query map[string]string, data jsonutils.JSONObject) *SApiRequest { - r := SApiRequest{ - RegionId: regionId, +// SOpenApiInstanceActionRequest 用于 OpenAPI Instance 区域接口(与实例列表一致走 api-*.cmecloud.cn:8443)。 +// 可用于除 list/describe-instances 之外的其他 instance openapi path。 +type SOpenApiInstanceActionRequest struct { + SJSONRequest + RegionId string + ServerPath string +} + +func NewOpenApiInstanceRequest(regionId string, data jsonutils.JSONObject) *SOpenApiInstanceRequest { + host := openApiRegionHost(regionId) + r := SOpenApiInstanceRequest{RegionId: regionId} + r.SJSONRequest = newBaseJSONRequest(regionId, host, "8443", "/api/openapi-instance/v4/list/describe-instances", nil, data) + return &r +} + +func (rr *SOpenApiInstanceRequest) GetPort() string { + return "8443" +} + +func (rr *SOpenApiInstanceRequest) GetEndpoint() string { + return openApiRegionHost(rr.RegionId) +} + +func (rr *SOpenApiInstanceRequest) GetHeaders() map[string]string { + h := rr.SJSONRequest.GetHeaders() + h["Region-Id"] = rr.RegionId + // OpenAPI 示例中带有 User-Agent,但不是必须字段,这里不强制设置 + return h +} + +func NewOpenApiInstanceActionRequest(regionId string, serverPath string, data jsonutils.JSONObject) *SOpenApiInstanceActionRequest { + host := openApiRegionHost(regionId) + r := SOpenApiInstanceActionRequest{RegionId: regionId, ServerPath: serverPath} + r.SJSONRequest = newBaseJSONRequest(regionId, host, "8443", serverPath, nil, data) + return &r +} + +func (rr *SOpenApiInstanceActionRequest) GetPort() string { return "8443" } +func (rr *SOpenApiInstanceActionRequest) GetEndpoint() string { return openApiRegionHost(rr.RegionId) } +func (rr *SOpenApiInstanceActionRequest) GetServerPath() string { return rr.ServerPath } +func (rr *SOpenApiInstanceActionRequest) GetHeaders() map[string]string { + h := rr.SJSONRequest.GetHeaders() + if len(rr.RegionId) > 0 { + h["Region-Id"] = rr.RegionId + } + return h +} + +func (r *SOpenApiInstanceActionRequest) Base() *SBaseRequest { return &r.SJSONRequest.SBaseRequest } + +// openApiRegionHostFallback 与官方 SDK 不一致的 regionId -> 主机名(省/区名与机房城市名不同时需单独映射),先查表再走拼接。 +var openApiRegionHostFallback = map[string]string{ + "cn-beijing-1": "api-beijing-2.cmecloud.cn", // 华北北京3 + "cn-jiangsu-1": "api-wuxi-1.cmecloud.cn", // 华东苏州 + "cn-guangdong-1": "api-dongguan-1.cmecloud.cn", // 华南广州3 + "cn-sichuan-1": "api-yaan-1.cmecloud.cn", // 西南成都 + "cn-henan-1": "api-zhengzhou-1.cmecloud.cn", // 华中郑州 + "cn-hunan-1": "api-zhuzhou-1.cmecloud.cn", // 华中长沙2 + "cn-shandong-1": "api-jinan-1.cmecloud.cn", // 华东济南 + "cn-shaanxi-1": "api-xian-1.cmecloud.cn", // 西北西安(陕 vs 山) + "cn-shangxi-1": "api-shanxi-1.cmecloud.cn", // 山西太原 + "cn-zhejiang-1": "api-ningbo-1.cmecloud.cn", // 华东杭州 + "cn-yunnan-1": "api-yunnan-2.cmecloud.cn", // 云南昆明2 + "cn-neimenggu-1": "api-huhehaote-1.cmecloud.cn", // 华北呼和浩特 + "cn-guzhou-1": "api-guiyang-1.cmecloud.cn", // 西南贵阳(贵州->贵阳) + "cn-hubei-2": "api-wuhan-1.cmecloud.cn", // 湖北武汉 +} + +// openApiRegionHost 由 regionId 得到区域 API 主机:先查 fallback(与官方 SDK 一致的特殊项),再按 api-{suffix}.cmecloud.cn 拼接,避免 API 返回新 region 拿不到 endpoint。 +func openApiRegionHost(regionId string) string { + if host := openApiRegionHostFallback[regionId]; host != "" { + return host + } + suffix := strings.TrimPrefix(regionId, "cn-") + if suffix == "" { + suffix = regionId + } + return "api-" + suffix + ".cmecloud.cn" +} + +// openApiVpcConsoleHost 用于 OpenAPI VPC:ecloudsdkvpc 使用 console-*.cmecloud.cn 且会正确转发 POST body,api-* 网关可能不转 body 导致“参数为空”。 +var openApiVpcConsoleHostFallback = map[string]string{ + "cn-beijing-1": "console-beijing-2.cmecloud.cn", + "cn-jiangsu-1": "console-wuxi-1.cmecloud.cn", + "cn-guangdong-1": "console-dongguan-1.cmecloud.cn", + "cn-sichuan-1": "console-yaan-1.cmecloud.cn", + "cn-henan-1": "console-zhengzhou-1.cmecloud.cn", + "cn-hunan-1": "console-zhuzhou-1.cmecloud.cn", + "cn-shandong-1": "console-jinan-1.cmecloud.cn", + "cn-shaanxi-1": "console-xian-1.cmecloud.cn", + "cn-shanghai-1": "console-shanghai-1.cmecloud.cn", + "cn-chongqing-1": "console-chongqing-1.cmecloud.cn", + "cn-zhejiang-1": "console-ningbo-1.cmecloud.cn", + "cn-tianjin-1": "console-tianjin-1.cmecloud.cn", + "cn-jilin-1": "console-jilin-1.cmecloud.cn", + "cn-hubei-2": "console-hubei-1.cmecloud.cn", + "cn-jiangxi-1": "console-jiangxi-1.cmecloud.cn", + "cn-gansu-1": "console-gansu-1.cmecloud.cn", + "cn-shangxi-1": "console-shanxi-1.cmecloud.cn", + "cn-liaoning-1": "console-liaoning-1.cmecloud.cn", + "cn-yunnan-1": "console-yunnan-2.cmecloud.cn", + "cn-hebei-1": "console-hebei-1.cmecloud.cn", + "cn-fujian-1": "console-fujian-1.cmecloud.cn", + "cn-guangxi-1": "console-guangxi-1.cmecloud.cn", + "cn-anhui-1": "console-anhui-1.cmecloud.cn", + "cn-neimenggu-1": "console-huhehaote-1.cmecloud.cn", + "cn-guzhou-1": "console-guiyang-1.cmecloud.cn", + "cn-hainan-1": "console-hainan-1.cmecloud.cn", + "cn-xinjiang-1": "console-xinjiang-1.cmecloud.cn", +} + +func openApiVpcConsoleHost(regionId string) string { + if host := openApiVpcConsoleHostFallback[regionId]; host != "" { + return host + } + suffix := strings.TrimPrefix(regionId, "cn-") + if suffix == "" { + suffix = regionId + } + return "console-" + suffix + ".cmecloud.cn" +} + +// SOpenApiRegionRequest 用于调用新的 OpenAPI 区域列表接口: +// 需走区域 endpoint (api-xxx.cmecloud.cn:8443),与 yunion.io/x/ecloud 成功样例一致。 +type SOpenApiRegionRequest struct { + SJSONRequest + RegionId string +} + +func NewOpenApiRegionRequest(regionId string, data jsonutils.JSONObject) *SOpenApiRegionRequest { + host := openApiRegionHost(regionId) + r := SOpenApiRegionRequest{ SJSONRequest: *NewJSONRequest(data), + RegionId: regionId, } - r.ServerPath = serverPath - mergeMap(r.GetQueryParams(), query) + r.SBaseRequest.Port = "8443" + r.SBaseRequest.Endpoint = host + r.SBaseRequest.RegionId = regionId + r.ServerPath = "/api/openapi-instance/v4/region/describe-regions" return &r } -func (rr *SApiRequest) GetScheme() string { - return "https" +func (rr *SOpenApiRegionRequest) GetPort() string { + return "8443" } -func (rr *SApiRequest) GetPort() string { - return "8443" +func (rr *SOpenApiRegionRequest) GetEndpoint() string { + return openApiRegionHost(rr.RegionId) } -func (rr *SApiRequest) GetEndpoint() string { - return fmt.Sprintf("api-%s.cmecloud.cn", rr.RegionId) +func (rr *SOpenApiRegionRequest) GetHeaders() map[string]string { + h := rr.SJSONRequest.GetHeaders() + if len(rr.RegionId) > 0 { + h["Region-Id"] = rr.RegionId + } + return h } -type SConsoleRequest struct { +// SOpenApiZoneRequest 用于 OpenAPI 可用区列表:GET 区域 endpoint 上的 describe-zones,与 describe-regions 同机房子网。 +type SOpenApiZoneRequest struct { SJSONRequest RegionId string } -func NewConsoleRequest(regionId string, serverPath string, query map[string]string, data jsonutils.JSONObject) *SConsoleRequest { - r := SConsoleRequest{ - RegionId: regionId, +func NewOpenApiZoneRequest(regionId string, data jsonutils.JSONObject) *SOpenApiZoneRequest { + host := openApiRegionHost(regionId) + r := SOpenApiZoneRequest{ SJSONRequest: *NewJSONRequest(data), + RegionId: regionId, } - r.ServerPath = serverPath - mergeMap(r.GetQueryParams(), query) + r.SBaseRequest.Port = "8443" + r.SBaseRequest.Endpoint = host + r.SBaseRequest.RegionId = regionId + r.ServerPath = "/api/openapi-instance/v4/region/describe-zones" return &r } -func (rr *SConsoleRequest) GetScheme() string { - return "https" +func (rr *SOpenApiZoneRequest) GetPort() string { return "8443" } +func (rr *SOpenApiZoneRequest) GetEndpoint() string { return openApiRegionHost(rr.RegionId) } +func (rr *SOpenApiZoneRequest) GetHeaders() map[string]string { + h := rr.SJSONRequest.GetHeaders() + if len(rr.RegionId) > 0 { + h["Region-Id"] = rr.RegionId + } + return h } -func (rr *SConsoleRequest) GetPort() string { - return "8443" +func (r *SOpenApiZoneRequest) Base() *SBaseRequest { + return &r.SJSONRequest.SBaseRequest +} + +// SOpenApiRequest 通用 OpenAPI 请求,用于 ecloud.10086.cn 上任意 path(GET/POST)。 +// 新接口迁移时可直接使用,无需再新增专用 Request 类型。 +type SOpenApiRequest struct { + SJSONRequest + RegionId string + ServerPath string +} + +func NewOpenApiRequest(regionId string, serverPath string, data jsonutils.JSONObject) *SOpenApiRequest { + r := SOpenApiRequest{RegionId: regionId, ServerPath: serverPath} + r.SJSONRequest = newBaseJSONRequest(regionId, "ecloud.10086.cn", "", serverPath, nil, data) + return &r +} + +func (rr *SOpenApiRequest) GetPort() string { return "" } +func (rr *SOpenApiRequest) GetEndpoint() string { + return "ecloud.10086.cn" +} +func (rr *SOpenApiRequest) GetServerPath() string { return rr.ServerPath } +func (rr *SOpenApiRequest) GetRegionId() string { return rr.RegionId } +func (rr *SOpenApiRequest) GetHeaders() map[string]string { + h := rr.SJSONRequest.GetHeaders() + if len(rr.RegionId) > 0 { + h["Region-Id"] = rr.RegionId + } + return h +} + +// regionIdToPoolId 创建 VPC 时 body.region 需为资源池 ID(与 ecloudsdkvpc initRegions 一致) +var regionIdToPoolId = map[string]string{ + "cn-beijing-1": "CIDC-RP-29", // 北京2 + "cn-jiangsu-1": "CIDC-RP-25", // 无锡 + "cn-guangdong-1": "CIDC-RP-26", // 东莞 + "cn-sichuan-1": "CIDC-RP-27", // 雅安 + "cn-henan-1": "CIDC-RP-28", // 郑州 + "cn-hunan-1": "CIDC-RP-30", // 株洲 + "cn-shandong-1": "CIDC-RP-31", // 济南 + "cn-shaanxi-1": "CIDC-RP-32", // 西安 + "cn-shanghai-1": "CIDC-RP-33", + "cn-chongqing-1": "CIDC-RP-34", + "cn-zhejiang-1": "CIDC-RP-35", // 宁波 + "cn-tianjin-1": "CIDC-RP-36", + "cn-jilin-1": "CIDC-RP-37", + "cn-hubei-2": "CIDC-RP-38", // 湖北 + "cn-jiangxi-1": "CIDC-RP-39", + "cn-gansu-1": "CIDC-RP-40", + "cn-shangxi-1": "CIDC-RP-41", // 山西 + "cn-liaoning-1": "CIDC-RP-42", + "cn-yunnan-1": "CIDC-RP-43", + "cn-hebei-1": "CIDC-RP-44", + "cn-fujian-1": "CIDC-RP-45", + "cn-guangxi-1": "CIDC-RP-46", + "cn-anhui-1": "CIDC-RP-47", + "cn-neimenggu-1": "CIDC-RP-48", // 呼和浩特 + "cn-guzhou-1": "CIDC-RP-49", // 贵阳 + "cn-hainan-1": "CIDC-RP-53", + "cn-xinjiang-1": "CIDC-RP-54", +} + +// SOpenApiVpcRequest 用于 OpenAPI VPC 接口:与 ecloudsdkvpc 一致使用 console-*.cmecloud.cn:8443,否则 api-* 网关可能不转发 POST body 导致“可用区region不能为空”。 +type SOpenApiVpcRequest struct { + SJSONRequest + RegionId string + ServerPath string +} + +func (rr *SOpenApiVpcRequest) GetHeaders() map[string]string { + h := rr.SJSONRequest.GetHeaders() + if len(rr.RegionId) > 0 { + h["Region-Id"] = rr.RegionId + } + return h } -func (rr *SConsoleRequest) GetEndpoint() string { - return fmt.Sprintf("console-%s.cmecloud.cn", rr.RegionId) +func newOpenApiConsoleRequest(regionId string, serverPath string, query map[string]string, data jsonutils.JSONObject) (host string, req SJSONRequest) { + host = openApiVpcConsoleHost(regionId) + req = newBaseJSONRequest(regionId, host, "8443", serverPath, query, data) + return host, req +} + +func NewOpenApiVpcRequest(regionId string, serverPath string, query map[string]string, data jsonutils.JSONObject) *SOpenApiVpcRequest { + _, req := newOpenApiConsoleRequest(regionId, serverPath, query, data) + r := SOpenApiVpcRequest{ + SJSONRequest: req, + RegionId: regionId, + ServerPath: serverPath, + } + return &r +} + +func (rr *SOpenApiVpcRequest) GetPort() string { return "8443" } +func (rr *SOpenApiVpcRequest) GetEndpoint() string { return openApiVpcConsoleHost(rr.RegionId) } +func (rr *SOpenApiVpcRequest) Base() *SBaseRequest { return &rr.SJSONRequest.SBaseRequest } + +// SOpenApiEbsRequest 用于 EBS/磁盘/快照 OpenAPI:与 ecloudsdkebs 一致使用 console-*.cmecloud.cn:8443。 +type SOpenApiEbsRequest struct { + SJSONRequest + RegionId string + ServerPath string +} + +func NewOpenApiEbsRequest(regionId string, serverPath string, query map[string]string, data jsonutils.JSONObject) *SOpenApiEbsRequest { + _, req := newOpenApiConsoleRequest(regionId, serverPath, query, data) + r := SOpenApiEbsRequest{ + SJSONRequest: req, + RegionId: regionId, + ServerPath: serverPath, + } + return &r +} + +func (rr *SOpenApiEbsRequest) GetPort() string { return "8443" } +func (rr *SOpenApiEbsRequest) GetEndpoint() string { return openApiVpcConsoleHost(rr.RegionId) } +func (rr *SOpenApiEbsRequest) Base() *SBaseRequest { return &rr.SJSONRequest.SBaseRequest } +func (rr *SOpenApiEbsRequest) GetHeaders() map[string]string { + h := rr.SJSONRequest.GetHeaders() + if len(rr.RegionId) > 0 { + h["Region-Id"] = rr.RegionId + } + return h +} + +// SOpenApiMopcRequest 用于 MOPC 开放接口(如账户余额查询)。ecloudsdkmopc 中 CIDC-CORE-00 对应 ecloud.10086.cn,/api/openapi-mop/ 仅在该网关可路由,console-* 会报 M02C002 gateway cannot route。 +type SOpenApiMopcRequest struct { + SJSONRequest + RegionId string + ServerPath string +} + +const mopcBalanceQueryPath = "/api/openapi-mop/openapi" + +func NewOpenApiMopcBalanceRequest(regionId string, userId string) *SOpenApiMopcRequest { + body := jsonutils.Marshal(map[string]interface{}{ + "balanceQueryPOSTBody": map[string]string{"userId": userId}, + "balanceQueryPOSTHeader": map[string]string{}, + }) + r := SOpenApiMopcRequest{RegionId: regionId, ServerPath: mopcBalanceQueryPath} + r.SJSONRequest = newBaseJSONRequest(regionId, "ecloud.10086.cn", "", mopcBalanceQueryPath, nil, body) + r.SBaseRequest.QueryParams["method"] = "SYAN_UNHT_balancequeryOpen" + r.SBaseRequest.QueryParams["format"] = "json" + r.SBaseRequest.QueryParams["status"] = "1" + return &r +} + +func (rr *SOpenApiMopcRequest) GetPort() string { return "" } +func (rr *SOpenApiMopcRequest) GetEndpoint() string { return "ecloud.10086.cn" } +func (rr *SOpenApiMopcRequest) Base() *SBaseRequest { return &rr.SJSONRequest.SBaseRequest } +func (rr *SOpenApiMopcRequest) GetHeaders() map[string]string { + h := rr.SJSONRequest.GetHeaders() + if len(rr.RegionId) > 0 { + h["Region-Id"] = rr.RegionId + } + return h } func mergeMap(m1, m2 map[string]string) { @@ -135,7 +425,6 @@ func mergeMap(m1, m2 map[string]string) { } type SBaseRequest struct { - Scheme string Method string Endpoint string ServerPath string @@ -157,10 +446,6 @@ func NewBaseRequest() *SBaseRequest { } } -func (br *SBaseRequest) GetScheme() string { - return br.Scheme -} - func (br *SBaseRequest) GetMethod() string { return br.Method } @@ -232,6 +517,23 @@ func (br *SBaseRequest) GetUserAgent() map[string]string { return nil } +// Base 返回底层的 SBaseRequest 指针,便于客户端统一处理。 +func (jr *SJSONRequest) Base() *SBaseRequest { + return &jr.SBaseRequest +} + +func (r *SOpenApiInstanceRequest) Base() *SBaseRequest { + return &r.SJSONRequest.SBaseRequest +} + +func (r *SOpenApiRegionRequest) Base() *SBaseRequest { + return &r.SJSONRequest.SBaseRequest +} + +func (r *SOpenApiRequest) Base() *SBaseRequest { + return &r.SJSONRequest.SBaseRequest +} + func (br *SBaseRequest) ForMateResponseBody(jrbody jsonutils.JSONObject) (jsonutils.JSONObject, error) { if jrbody == nil || !jrbody.Contains("state") { return nil, ErrMissKey{ @@ -243,10 +545,8 @@ func (br *SBaseRequest) ForMateResponseBody(jrbody jsonutils.JSONObject) (jsonut switch state { case "OK": if !jrbody.Contains("body") { - return nil, ErrMissKey{ - Key: "body", - Jo: jrbody, - } + // 部分接口(如 DELETE)仅返回 state:OK,无 body + return jsonutils.NewDict(), nil } body, _ := jrbody.Get("body") return body, nil diff --git a/pkg/multicloud/ecloud/secgrouprule.go b/pkg/multicloud/ecloud/secgrouprule.go new file mode 100644 index 000000000..c1ab72a79 --- /dev/null +++ b/pkg/multicloud/ecloud/secgrouprule.go @@ -0,0 +1,90 @@ +package ecloud + +import ( + "fmt" + "strings" + + "yunion.io/x/pkg/errors" + + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/cloudmux/pkg/multicloud" + "yunion.io/x/pkg/util/secrules" +) + +// SSecurityGroupRule 与 ecloudsdkvpc ListSecurityGroupRuleResponseContent 字段对应 +type SSecurityGroupRule struct { + multicloud.SResourceBase + + region *SRegion + SecgroupId string `json:"secgroupId"` + + Id string `json:"id"` + Direction string `json:"direction"` + Protocol string `json:"protocol"` + MinPortRange *int32 `json:"minPortRange,omitempty"` + MaxPortRange *int32 `json:"maxPortRange,omitempty"` + RemoteIpPrefix string `json:"remoteIpPrefix"` + Description string `json:"description"` + EtherType string `json:"etherType,omitempty"` + AimSgid *string `json:"aimSgid,omitempty"` + DefaultRule *bool `json:"defaultRule,omitempty"` + CreatedTime string `json:"createdTime,omitempty"` + Status *int32 `json:"status,omitempty"` +} + +func (r *SSecurityGroupRule) GetGlobalId() string { + return r.Id +} + +func (r *SSecurityGroupRule) GetDirection() secrules.TSecurityRuleDirection { + if r.Direction == "ingress" { + return secrules.DIR_IN + } + return secrules.DIR_OUT +} + +func (r *SSecurityGroupRule) GetPriority() int { + return 0 +} + +func (r *SSecurityGroupRule) GetAction() secrules.TSecurityRuleAction { + return secrules.SecurityRuleAllow +} + +func (r *SSecurityGroupRule) GetProtocol() string { + if r.Protocol == "" || r.Protocol == "ANY" { + return secrules.PROTO_ANY + } + return strings.ToLower(r.Protocol) +} + +func (r *SSecurityGroupRule) GetPorts() string { + if r.MinPortRange != nil && r.MaxPortRange != nil { + min, max := *r.MinPortRange, *r.MaxPortRange + if min == max { + return fmt.Sprintf("%d", min) + } + return fmt.Sprintf("%d-%d", min, max) + } + return "" +} + +func (r *SSecurityGroupRule) GetDescription() string { + return r.Description +} + +func (r *SSecurityGroupRule) GetCIDRs() []string { + if r.RemoteIpPrefix == "" { + return nil + } + return []string{r.RemoteIpPrefix} +} + +func (r *SSecurityGroupRule) Update(opts *cloudprovider.SecurityGroupRuleUpdateOptions) error { + return errors.ErrNotSupported +} + +func (r *SSecurityGroupRule) Delete() error { + return r.region.DeleteSecurityGroupRule(r.Id) +} + diff --git a/pkg/multicloud/ecloud/securitygroup.go b/pkg/multicloud/ecloud/securitygroup.go new file mode 100644 index 000000000..2b3ac053d --- /dev/null +++ b/pkg/multicloud/ecloud/securitygroup.go @@ -0,0 +1,104 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ecloud + +import ( + "yunion.io/x/jsonutils" + + api "yunion.io/x/cloudmux/pkg/apis/compute" + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/cloudmux/pkg/multicloud" +) + +// SSecurityGroup 与 ecloudsdkvpc ListSecGroupResponseContent 字段对应 +type SSecurityGroup struct { + multicloud.SSecurityGroup + EcloudTags + + region *SRegion + + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Region string `json:"region"` + Type string `json:"type"` + CreatedTime string `json:"createdTime"` + Defaulted *bool `json:"defaulted,omitempty"` + Stateful *bool `json:"stateful,omitempty"` + Scale string `json:"scale,omitempty"` + VpoolId *string `json:"vpoolId,omitempty"` + Vaz *string `json:"vaz,omitempty"` +} + +func (sg *SSecurityGroup) GetId() string { + return sg.Id +} + +func (sg *SSecurityGroup) GetName() string { + return sg.Name +} + +func (sg *SSecurityGroup) GetGlobalId() string { + return sg.Id +} + +func (sg *SSecurityGroup) GetDescription() string { + return sg.Description +} + +func (sg *SSecurityGroup) GetStatus() string { + return api.SECGROUP_STATUS_READY +} + +func (sg *SSecurityGroup) GetVpcId() string { + // 移动云安全组为 region 维度,非 VPC 维度 + return "" +} + +func (sg *SSecurityGroup) Refresh() error { + latest, err := sg.region.GetSecurityGroup(sg.Id) + if err != nil { + return err + } + return jsonutils.Update(sg, latest) +} + +func (sg *SSecurityGroup) GetRules() ([]cloudprovider.ISecurityGroupRule, error) { + rules, err := sg.region.GetSecurityGroupRules(sg.Id) + if err != nil { + return nil, err + } + ret := make([]cloudprovider.ISecurityGroupRule, len(rules)) + for i := range rules { + ret[i] = &rules[i] + } + return ret, nil +} + +func (sg *SSecurityGroup) CreateRule(opts *cloudprovider.SecurityGroupRuleCreateOptions) (cloudprovider.ISecurityGroupRule, error) { + rule, err := sg.region.CreateSecurityGroupRule(sg.Id, opts) + if err != nil { + return nil, err + } + return rule, nil +} + +func (sg *SSecurityGroup) GetReferences() ([]cloudprovider.SecurityGroupReference, error) { + return nil, nil +} + +func (sg *SSecurityGroup) Delete() error { + return sg.region.DeleteSecurityGroup(sg.Id) +} diff --git a/pkg/multicloud/ecloud/shell/balance.go b/pkg/multicloud/ecloud/shell/balance.go new file mode 100644 index 000000000..3c8557f93 --- /dev/null +++ b/pkg/multicloud/ecloud/shell/balance.go @@ -0,0 +1,20 @@ +package shell + +import ( + "yunion.io/x/pkg/util/shellutils" + + "yunion.io/x/cloudmux/pkg/multicloud/ecloud" +) + +func init() { + type BalanceOptions struct { + } + shellutils.R(&BalanceOptions{}, "balance-show", "Show account balance (MOPC)", func(cli *ecloud.SRegion, args *BalanceOptions) error { + balance, err := cli.GetClient().GetBalance() + if err != nil { + return err + } + printObject(balance) + return nil + }) +} diff --git a/pkg/multicloud/ecloud/shell/disk.go b/pkg/multicloud/ecloud/shell/disk.go index 38d9c4b3d..bd06e43d3 100644 --- a/pkg/multicloud/ecloud/shell/disk.go +++ b/pkg/multicloud/ecloud/shell/disk.go @@ -15,6 +15,7 @@ package shell import ( + "context" "yunion.io/x/pkg/util/shellutils" "yunion.io/x/cloudmux/pkg/multicloud/ecloud" @@ -24,11 +25,38 @@ func init() { type VDiskListOptions struct { } shellutils.R(&VDiskListOptions{}, "disk-list", "List disks", func(cli *ecloud.SRegion, args *VDiskListOptions) error { - disks, e := cli.GetDisks() - if e != nil { - return e + disks, err := cli.GetDisks() + if err != nil { + return err } - printList(disks, 0, 0, 0, nil) + printList(disks) return nil }) + + type DiskShowOptions struct { + ID string `help:"Disk ID"` + } + shellutils.R(&DiskShowOptions{}, "disk-show", "Show disk detail", func(cli *ecloud.SRegion, args *DiskShowOptions) error { + disk, err := cli.GetDisk(args.ID) + if err != nil { + return err + } + printObject(disk) + return nil + }) + + type DiskDeleteOptions struct { + ID string `help:"Disk ID"` + } + shellutils.R(&DiskDeleteOptions{}, "disk-delete", "Delete disk (pre-delete)", func(cli *ecloud.SRegion, args *DiskDeleteOptions) error { + return cli.PreDeleteVolume(args.ID) + }) + + type DiskResizeOptions struct { + ID string `help:"Disk ID"` + SIZE int64 `help:"New disk size GB (must be >= current)"` + } + shellutils.R(&DiskResizeOptions{}, "disk-resize", "Resize disk", func(cli *ecloud.SRegion, args *DiskResizeOptions) error { + return cli.ResizeDisk(context.Background(), args.ID, args.SIZE) + }) } diff --git a/pkg/multicloud/ecloud/shell/eip.go b/pkg/multicloud/ecloud/shell/eip.go new file mode 100644 index 000000000..83c43d19d --- /dev/null +++ b/pkg/multicloud/ecloud/shell/eip.go @@ -0,0 +1,45 @@ +package shell + +import ( + "yunion.io/x/pkg/util/shellutils" + + "yunion.io/x/cloudmux/pkg/multicloud/ecloud" +) + +func init() { + type EipListOptions struct { + } + shellutils.R(&EipListOptions{}, "eip-list", "List eips", func(cli *ecloud.SRegion, args *EipListOptions) error { + eips, err := cli.GetIEips() + if err != nil { + return err + } + printList(eips) + return nil + }) + + type EipShowOptions struct { + ID string `help:"EIP ID"` + } + shellutils.R(&EipShowOptions{}, "eip-show", "Show eip detail", func(cli *ecloud.SRegion, args *EipShowOptions) error { + eip, err := cli.GetEipById(args.ID) + if err != nil { + return err + } + printObject(eip) + return nil + }) + + type EipShowByAddrOptions struct { + Addr string `help:"EIP address"` + } + shellutils.R(&EipShowByAddrOptions{}, "eip-show-by-addr", "Show eip detail by address", func(cli *ecloud.SRegion, args *EipShowByAddrOptions) error { + eip, err := cli.GetEipByAddr(args.Addr) + if err != nil { + return err + } + printObject(eip) + return nil + }) +} + diff --git a/pkg/multicloud/ecloud/shell/image.go b/pkg/multicloud/ecloud/shell/image.go index ac4de1257..e11e2bef5 100644 --- a/pkg/multicloud/ecloud/shell/image.go +++ b/pkg/multicloud/ecloud/shell/image.go @@ -22,14 +22,26 @@ import ( func init() { type VImageListOptions struct { - Public bool + Public bool `help:"List public images"` } shellutils.R(&VImageListOptions{}, "image-list", "List images", func(cli *ecloud.SRegion, args *VImageListOptions) error { images, e := cli.GetImages(args.Public) if e != nil { return e } - printList(images, 0, 0, 0, nil) + printList(images) + return nil + }) + + type ImageShowOptions struct { + ID string `help:"Image ID"` + } + shellutils.R(&ImageShowOptions{}, "image-show", "Show image detail", func(cli *ecloud.SRegion, args *ImageShowOptions) error { + img, err := cli.GetImage(args.ID) + if err != nil { + return err + } + printObject(img) return nil }) } diff --git a/pkg/multicloud/ecloud/shell/instance.go b/pkg/multicloud/ecloud/shell/instance.go index 7e2d4bb91..b22cdc109 100644 --- a/pkg/multicloud/ecloud/shell/instance.go +++ b/pkg/multicloud/ecloud/shell/instance.go @@ -15,6 +15,9 @@ package shell import ( + "context" + "fmt" + "yunion.io/x/pkg/util/shellutils" "yunion.io/x/cloudmux/pkg/multicloud/ecloud" @@ -22,48 +25,68 @@ import ( func init() { type InstanceListOptions struct { + ZoneId string + ServerId string } shellutils.R(&InstanceListOptions{}, "instance-list", "List intances", func(cli *ecloud.SRegion, args *InstanceListOptions) error { - instances, e := cli.GetInstancesWithHost("") - if e != nil { - return e + instances, err := cli.GetInstances(args.ZoneId, args.ServerId) + if err != nil { + return err } - printList(instances, 0, 0, 0, []string{}) + printList(instances) return nil }) type InstanceShowOptions struct { ID string } shellutils.R(&InstanceShowOptions{}, "instance-show", "Show intances", func(cli *ecloud.SRegion, args *InstanceShowOptions) error { - instance, e := cli.GetInstanceById(args.ID) - if e != nil { - return e + instance, err := cli.GetInstance(args.ID) + if err != nil { + return err } printObject(instance) return nil }) shellutils.R(&InstanceShowOptions{}, "instance-nic-list", "List intance nics", func(cli *ecloud.SRegion, args *InstanceShowOptions) error { - instance, e := cli.GetInstanceById(args.ID) - if e != nil { - return e - } - nics, err := instance.GetINics() + nics, err := cli.GetInstanceNics(context.Background(), args.ID) if err != nil { return err } - printList(nics, 0, 0, 0, []string{}) + printList(nics) return nil }) shellutils.R(&InstanceShowOptions{}, "instance-disk-list", "List intance disks", func(cli *ecloud.SRegion, args *InstanceShowOptions) error { - instance, e := cli.GetInstanceById(args.ID) - if e != nil { - return e + disks, err := cli.GetDataDisks(args.ID) + if err != nil { + return err } - disks, err := instance.GetIDisks() + printList(disks) + return nil + }) + + shellutils.R(&InstanceShowOptions{}, "instance-vnc-url", "Get instance VNC url", func(cli *ecloud.SRegion, args *InstanceShowOptions) error { + url, err := cli.GetInstanceVNCUrl(args.ID) if err != nil { return err } - printList(disks, 0, 0, 0, []string{}) + fmt.Println(url) return nil }) + + shellutils.R(&InstanceShowOptions{}, "instance-start", "Start instance", func(cli *ecloud.SRegion, args *InstanceShowOptions) error { + return cli.StartInstance(context.Background(), args.ID) + }) + + shellutils.R(&InstanceShowOptions{}, "instance-stop", "Stop instance", func(cli *ecloud.SRegion, args *InstanceShowOptions) error { + return cli.StopInstance(context.Background(), args.ID) + }) + + type InstanceDeleteOptions struct { + ID string `help:"Instance ID"` + DeletePublicNetwork bool `help:"Delete associated public network (EIP)" default:"false"` + DeleteDataVolumes bool `help:"Delete associated data volumes" default:"false"` + } + shellutils.R(&InstanceDeleteOptions{}, "instance-delete", "Delete instance", func(cli *ecloud.SRegion, args *InstanceDeleteOptions) error { + return cli.DeleteInstance(context.Background(), args.ID, args.DeletePublicNetwork, args.DeleteDataVolumes) + }) } diff --git a/pkg/multicloud/ecloud/shell/monitor.go b/pkg/multicloud/ecloud/shell/monitor.go index 5a23416ba..c45a89f39 100644 --- a/pkg/multicloud/ecloud/shell/monitor.go +++ b/pkg/multicloud/ecloud/shell/monitor.go @@ -15,6 +15,8 @@ package shell import ( + "time" + "yunion.io/x/log" "yunion.io/x/pkg/util/shellutils" @@ -23,26 +25,31 @@ import ( ) func init() { - type PListOptions struct { + type SMetricTypeListOptions struct { } - shellutils.R(&PListOptions{}, "server-producttype-list", "List productTypes", func(cli *ecloud.SRegion, - args *PListOptions) error { - prod, e := cli.GetProductTypes() - if e != nil { - return e + shellutils.R(&SMetricTypeListOptions{}, "metric-type-list", "List metric types", func(cli *ecloud.SRegion, args *SMetricTypeListOptions) error { + metricTypes, err := cli.GetMetricTypes() + if err != nil { + return err } - printObject(prod) + printObject(metricTypes) return nil }) shellutils.R(&cloudprovider.MetricListOptions{}, "metric-list", "List metrics", func(cli *ecloud.SRegion, args *cloudprovider.MetricListOptions) error { + if args.StartTime.IsZero() { + args.StartTime = time.Now().Add(time.Minute * -20) + } + if args.EndTime.IsZero() { + args.EndTime = time.Now() + } metrics, err := cli.GetClient().GetMetrics(args) if err != nil { return err } for i := range metrics { log.Infof("metric: %s %s %s", metrics[i].Id, metrics[i].MetricType, metrics[i].Unit) - printList(metrics[i].Values, len(metrics[i].Values), 0, 0, []string{}) + printList(metrics[i].Values) } return nil }) diff --git a/pkg/multicloud/ecloud/shell/network.go b/pkg/multicloud/ecloud/shell/network.go index 698481f03..03a462b45 100644 --- a/pkg/multicloud/ecloud/shell/network.go +++ b/pkg/multicloud/ecloud/shell/network.go @@ -17,24 +17,67 @@ package shell import ( "yunion.io/x/pkg/util/shellutils" + "yunion.io/x/cloudmux/pkg/cloudprovider" "yunion.io/x/cloudmux/pkg/multicloud/ecloud" ) func init() { type VNetworkListOptions struct { - VpcId string `help:"Vpc ID"` + RouteId string `help:"VPC ID or router ID"` } - shellutils.R(&VNetworkListOptions{}, "subnet-list", "List subnets", func(cli *ecloud.SRegion, args *VNetworkListOptions) error { + shellutils.R(&VNetworkListOptions{}, "network-list", "List networks", func(cli *ecloud.SRegion, args *VNetworkListOptions) error { + networks, err := cli.GetNetworks(args.RouteId, "") + if err != nil { + return err + } + printList(networks) + return nil + }) + + type NetworkShowOptions struct { + ID string `help:"Subnet ID"` + } + shellutils.R(&NetworkShowOptions{}, "network-show", "Show network detail", func(cli *ecloud.SRegion, args *NetworkShowOptions) error { + // VPCId 当前未被 OpenAPI 使用,这里仅保留参数以兼容调用习惯 + net, err := cli.GetNetwork(args.ID) + if err != nil { + return err + } + printObject(net) + return nil + }) + + type NetworkCreateOptions struct { + VpcId string `help:"VPC ID or router ID"` + Name string `help:"Network name (5-22 chars, letter first)"` + Cidr string `help:"CIDR, e.g. 192.168.1.0/24" default:"192.168.0.0/24"` + } + shellutils.R(&NetworkCreateOptions{}, "network-create", "Create network", func(cli *ecloud.SRegion, args *NetworkCreateOptions) error { ivpc, err := cli.GetIVpcById(args.VpcId) if err != nil { return err } vpc := ivpc.(*ecloud.SVpc) - networks, e := cli.GetNetworks(vpc.RouterId, "") - if e != nil { - return e + iwires, err := vpc.GetIWires() + if err != nil || len(iwires) == 0 { + return err + } + wire := iwires[0].(*ecloud.SWire) + inet, err := wire.CreateINetwork(&cloudprovider.SNetworkCreateOptions{ + Name: args.Name, + Cidr: args.Cidr, + }) + if err != nil { + return err } - printList(networks, 0, 0, 0, nil) + printObject(inet) return nil }) + + type NetworkDeleteOptions struct { + NetworkId string `help:"Network ID"` + } + shellutils.R(&NetworkDeleteOptions{}, "network-delete", "Delete network", func(cli *ecloud.SRegion, args *NetworkDeleteOptions) error { + return cli.DeleteNetwork(args.NetworkId) + }) } diff --git a/pkg/multicloud/ecloud/shell/printutils.go b/pkg/multicloud/ecloud/shell/printutils.go index a04ad1223..8b62b9e86 100644 --- a/pkg/multicloud/ecloud/shell/printutils.go +++ b/pkg/multicloud/ecloud/shell/printutils.go @@ -16,8 +16,8 @@ package shell import "yunion.io/x/pkg/util/printutils" -func printList(data interface{}, total, offset, limit int, columns []string) { - printutils.PrintInterfaceList(data, total, offset, limit, columns) +func printList(data interface{}) { + printutils.PrintInterfaceList(data, 0, 0, 0, nil) } func printObject(obj interface{}) { diff --git a/pkg/multicloud/ecloud/shell/region.go b/pkg/multicloud/ecloud/shell/region.go index 58be1355f..66b01565f 100644 --- a/pkg/multicloud/ecloud/shell/region.go +++ b/pkg/multicloud/ecloud/shell/region.go @@ -28,7 +28,7 @@ func init() { if err != nil { return err } - printList(regions, 0, 0, 0, nil) + printList(regions) return nil }) shellutils.R(&RegionListOptions{}, "zone-list", "List zones", func(cli *ecloud.SRegion, args *RegionListOptions) error { @@ -36,7 +36,7 @@ func init() { if err != nil { return err } - printList(zones, 0, 0, 0, nil) + printList(zones) return nil }) } diff --git a/pkg/multicloud/ecloud/shell/securitygroup.go b/pkg/multicloud/ecloud/shell/securitygroup.go new file mode 100644 index 000000000..5a844243c --- /dev/null +++ b/pkg/multicloud/ecloud/shell/securitygroup.go @@ -0,0 +1,117 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shell + +import ( + "yunion.io/x/pkg/util/secrules" + "yunion.io/x/pkg/util/shellutils" + + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/cloudmux/pkg/multicloud/ecloud" +) + +func init() { + type SecgroupListOptions struct { + } + shellutils.R(&SecgroupListOptions{}, "secgroup-list", "List security groups", func(cli *ecloud.SRegion, args *SecgroupListOptions) error { + groups, err := cli.GetISecurityGroups() + if err != nil { + return err + } + printList(groups) + return nil + }) + + type SecgroupCreateOptions struct { + Name string `help:"Security group name"` + Desc string `help:"Description" optional:"true"` + } + shellutils.R(&SecgroupCreateOptions{}, "secgroup-create", "Create security group", func(cli *ecloud.SRegion, args *SecgroupCreateOptions) error { + opts := &cloudprovider.SecurityGroupCreateInput{ + Name: args.Name, + Desc: args.Desc, + } + group, err := cli.CreateSecurityGroup(opts) + if err != nil { + return err + } + printObject(group) + return nil + }) + + type SecgroupIdOptions struct { + ID string `help:"Security group id"` + } + shellutils.R(&SecgroupIdOptions{}, "secgroup-show", "Show security group detail", func(cli *ecloud.SRegion, args *SecgroupIdOptions) error { + group, err := cli.GetISecurityGroupById(args.ID) + if err != nil { + return err + } + printObject(group) + return nil + }) + + shellutils.R(&SecgroupIdOptions{}, "secgroup-delete", "Delete security group", func(cli *ecloud.SRegion, args *SecgroupIdOptions) error { + group, err := cli.GetISecurityGroupById(args.ID) + if err != nil { + return err + } + return group.Delete() + }) + + type SecgroupRuleListOptions struct { + ID string `help:"Security group ID"` + } + shellutils.R(&SecgroupRuleListOptions{}, "secgroup-rule-list", "List security group rules", func(cli *ecloud.SRegion, args *SecgroupRuleListOptions) error { + rules, err := cli.GetSecurityGroupRules(args.ID) + if err != nil { + return err + } + printList(rules) + return nil + }) + + type SecgroupRuleCreateOptions struct { + SecgroupId string `help:"Security group ID"` + Direction string `help:"Direction: in|out" choices:"in|out"` + Protocol string `help:"Protocol: tcp|udp|icmp|ANY" default:"ANY"` + Ports string `help:"Port or range, e.g. 80 or 80-443" optional:"true"` + CIDR string `help:"Remote CIDR, e.g. 0.0.0.0/0" optional:"true"` + Desc string `help:"Description" optional:"true"` + } + shellutils.R(&SecgroupRuleCreateOptions{}, "secgroup-rule-create", "Create security group rule", func(cli *ecloud.SRegion, args *SecgroupRuleCreateOptions) error { + opts := &cloudprovider.SecurityGroupRuleCreateOptions{ + Direction: secrules.TSecurityRuleDirection(args.Direction), + Protocol: args.Protocol, + Ports: args.Ports, + CIDR: args.CIDR, + Desc: args.Desc, + Action: secrules.SecurityRuleAllow, + } + rule, err := cli.CreateSecurityGroupRule(args.SecgroupId, opts) + if err != nil { + return err + } + printObject(rule) + return nil + }) + + type SecgroupRuleDeleteOptions struct { + RuleId string `help:"Security group rule ID"` + } + shellutils.R(&SecgroupRuleDeleteOptions{}, "secgroup-rule-delete", "Delete security group rule", func(cli *ecloud.SRegion, args *SecgroupRuleDeleteOptions) error { + return cli.DeleteSecurityGroupRule(args.RuleId) + }) +} diff --git a/pkg/multicloud/ecloud/shell/snapshot.go b/pkg/multicloud/ecloud/shell/snapshot.go new file mode 100644 index 000000000..d326da042 --- /dev/null +++ b/pkg/multicloud/ecloud/shell/snapshot.go @@ -0,0 +1,82 @@ +package shell + +import ( + "yunion.io/x/pkg/util/shellutils" + + "yunion.io/x/cloudmux/pkg/cloudprovider" + "yunion.io/x/cloudmux/pkg/multicloud/ecloud" +) + +func init() { + type DiskSnapshotListOptions struct { + DiskId string `help:"Disk ID"` + } + shellutils.R(&DiskSnapshotListOptions{}, "disk-snapshot-list", "List snapshots of a data disk", func(cli *ecloud.SRegion, args *DiskSnapshotListOptions) error { + snapshots, err := cli.GetSnapshots("", args.DiskId, false) + if err != nil { + return err + } + printList(snapshots) + return nil + }) + + type ServerSnapshotListOptions struct { + ServerId string `help:"Server ID"` + } + shellutils.R(&ServerSnapshotListOptions{}, "server-snapshot-list", "List snapshots of a system disk by server", func(cli *ecloud.SRegion, args *ServerSnapshotListOptions) error { + snapshots, err := cli.GetSnapshots("", args.ServerId, true) + if err != nil { + return err + } + printList(snapshots) + return nil + }) + + type SnapshotShowOptions struct { + SnapshotId string `help:"Snapshot ID"` + DiskId string `help:"Disk ID (for data disk snapshot)" optional:"true"` + ServerId string `help:"Server ID (for system disk snapshot)" optional:"true"` + } + shellutils.R(&SnapshotShowOptions{}, "snapshot-show", "Show snapshot detail", func(cli *ecloud.SRegion, args *SnapshotShowOptions) error { + isSystem := args.ServerId != "" + parentId := args.DiskId + if isSystem { + parentId = args.ServerId + } + snapshots, err := cli.GetSnapshots(args.SnapshotId, parentId, isSystem) + if err != nil { + return err + } + if len(snapshots) == 0 { + return cloudprovider.ErrNotFound + } + printObject(&snapshots[0]) + return nil + }) + + type SnapshotCreateOptions struct { + DiskId string `help:"Disk ID"` + Name string `help:"Snapshot name"` + Description string `help:"Description" optional:"true"` + } + shellutils.R(&SnapshotCreateOptions{}, "snapshot-create", "Create snapshot for disk", func(cli *ecloud.SRegion, args *SnapshotCreateOptions) error { + snapshotId, err := cli.CreateEbsSnapshot(args.DiskId, args.Name, args.Description) + if err != nil { + return err + } + snapshots, err := cli.GetSnapshots(snapshotId, args.DiskId, false) + if err != nil || len(snapshots) == 0 { + return err + } + printObject(&snapshots[0]) + return nil + }) + + type SnapshotDeleteOptions struct { + ID string `help:"Snapshot ID"` + } + shellutils.R(&SnapshotDeleteOptions{}, "snapshot-delete", "Delete snapshot", func(cli *ecloud.SRegion, args *SnapshotDeleteOptions) error { + return cli.DeleteEbsSnapshot(args.ID) + }) +} + diff --git a/pkg/multicloud/ecloud/shell/storage.go b/pkg/multicloud/ecloud/shell/storage.go new file mode 100644 index 000000000..de8e8abfd --- /dev/null +++ b/pkg/multicloud/ecloud/shell/storage.go @@ -0,0 +1,59 @@ +// Copyright 2019 Yunion +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package shell + +import ( + "yunion.io/x/pkg/util/shellutils" + + "yunion.io/x/cloudmux/pkg/multicloud/ecloud" +) + +func init() { + type StorageListOptions struct { + ZoneCode string `help:"Zone code to filter storages" optional:"true"` + } + shellutils.R(&StorageListOptions{}, "storage-list", "List storages in region (all zones or specific zone)", func(cli *ecloud.SRegion, args *StorageListOptions) error { + storages, err := cli.GetStorages(args.ZoneCode) + if err != nil { + return err + } + printList(storages) + return nil + }) + + type PoolInfoOptions struct { + ProductType string `help:"Product type, e.g. capebs/ssdebs/ssd" required:"true"` + } + shellutils.R(&PoolInfoOptions{}, "pool-info-list", "List EBS pool infos by productType", func(cli *ecloud.SRegion, args *PoolInfoOptions) error { + pools, err := cli.GetPoolInfo(args.ProductType) + if err != nil { + return err + } + printList(pools) + return nil + }) + + type VolumeConfigOptions struct { + } + shellutils.R(&VolumeConfigOptions{}, "volume-config-list", "List volume type config", func(cli *ecloud.SRegion, args *VolumeConfigOptions) error { + cfg, err := cli.GetVolumeConfig() + if err != nil { + return err + } + printList(cfg) + return nil + }) +} + diff --git a/pkg/multicloud/ecloud/shell/vpc.go b/pkg/multicloud/ecloud/shell/vpc.go index 388faf45b..40c57a718 100644 --- a/pkg/multicloud/ecloud/shell/vpc.go +++ b/pkg/multicloud/ecloud/shell/vpc.go @@ -17,6 +17,7 @@ package shell import ( "yunion.io/x/pkg/util/shellutils" + "yunion.io/x/cloudmux/pkg/cloudprovider" "yunion.io/x/cloudmux/pkg/multicloud/ecloud" ) @@ -24,11 +25,46 @@ func init() { type VpcListOptions struct { } shellutils.R(&VpcListOptions{}, "vpc-list", "List vpcs", func(cli *ecloud.SRegion, args *VpcListOptions) error { - vpcs, err := cli.GetIVpcs() + vpcs, err := cli.GetVpcs() if err != nil { return err } - printList(vpcs, 0, 0, 0, nil) + printList(vpcs) return nil }) + + type VpcCreateOptions struct { + Name string `help:"VPC name"` + Desc string `help:"Description"` + CIDR string `help:"CIDR block, e.g. 192.168.0.0/16" optional:"true"` + } + shellutils.R(&VpcCreateOptions{}, "vpc-create", "Create vpc", func(cli *ecloud.SRegion, args *VpcCreateOptions) error { + opts := &cloudprovider.VpcCreateOptions{ + NAME: args.Name, + Desc: args.Desc, + CIDR: args.CIDR, + } + vpc, err := cli.CreateIVpc(opts) + if err != nil { + return err + } + printObject(vpc) + return nil + }) + + type VpcIdOptions struct { + ID string `help:"VPC id or router id"` + } + shellutils.R(&VpcIdOptions{}, "vpc-show", "Show vpc detail", func(cli *ecloud.SRegion, args *VpcIdOptions) error { + ivpc, err := cli.GetVpc(args.ID) + if err != nil { + return err + } + printObject(ivpc) + return nil + }) + + shellutils.R(&VpcIdOptions{}, "vpc-delete", "Delete vpc", func(cli *ecloud.SRegion, args *VpcIdOptions) error { + return cli.DeleteVpc(args.ID) + }) } diff --git a/pkg/multicloud/ecloud/signer.go b/pkg/multicloud/ecloud/signer.go deleted file mode 100644 index d008224f5..000000000 --- a/pkg/multicloud/ecloud/signer.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2019 Yunion -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ecloud - -import ( - "crypto/hmac" - "crypto/sha1" - "encoding/hex" - - "yunion.io/x/pkg/util/stringutils" -) - -type ISigner interface { - GetName() string - GetVersion() string - GetAccessKeyId() string - GetNonce() string - Sign(stringToSign, secretPrefix string) string -} - -type SRamRoleSigner struct { - accessKeyId string - accessKeySecret string -} - -func NewRamRoleSigner(accessKeyId, accessKeySecret string) *SRamRoleSigner { - return &SRamRoleSigner{ - accessKeyId: accessKeyId, - accessKeySecret: accessKeySecret, - } -} - -func (s *SRamRoleSigner) GetName() string { - return "HmacSHA1" -} - -func (s *SRamRoleSigner) GetVersion() string { - return "V2.0" -} - -func (s *SRamRoleSigner) GetAccessKeyId() string { - return s.accessKeyId -} - -func (s *SRamRoleSigner) GetNonce() string { - return stringutils.UUID4() -} - -func (s *SRamRoleSigner) Sign(stringToSign, secretPrefix string) string { - secret := secretPrefix + s.accessKeySecret - return shaHmac1(stringToSign, secret) -} - -func shaHmac1(source, secret string) string { - key := []byte(secret) - hmac := hmac.New(sha1.New, key) - hmac.Write([]byte(source)) - signedBytes := hmac.Sum(nil) - return hex.EncodeToString(signedBytes) -} diff --git a/pkg/multicloud/ecloud/snapshot.go b/pkg/multicloud/ecloud/snapshot.go index 53bda7593..640c23b73 100644 --- a/pkg/multicloud/ecloud/snapshot.go +++ b/pkg/multicloud/ecloud/snapshot.go @@ -15,7 +15,6 @@ package ecloud import ( - "context" "strings" "time" @@ -30,18 +29,20 @@ type SSnapshot struct { region *SRegion SCreateTime - BackupType string - CreateBy string - Description string - EcType string - Id string - Name string - Size int - VolumeId string - VolumeType string - Status string - IsSystem bool - SystemDisk string + BackupType string `json:"backupType,omitempty"` + CreateBy string `json:"createBy,omitempty"` + Description string `json:"description,omitempty"` + EcType string `json:"ecType,omitempty"` + Id string `json:"id"` + Name string `json:"name,omitempty"` + Size int `json:"size"` + VolumeId string `json:"volumeId,omitempty"` + VolumeType string `json:"volumeType,omitempty"` + Status string `json:"status,omitempty"` + IsSystem bool `json:"isSystem,omitempty"` + SystemDisk string `json:"systemDisk,omitempty"` + // EBS 快照列表返回 createTime + CreateTime string `json:"createTime,omitempty"` } func (s *SSnapshot) GetId() string { @@ -81,6 +82,14 @@ func (s *SSnapshot) GetDiskId() string { } func (s *SSnapshot) GetCreatedAt() time.Time { + if s.CreateTime != "" { + if t, err := time.Parse("2006-01-02 15:04:05", s.CreateTime); err == nil { + return t + } + if t, err := time.Parse(time.RFC3339, s.CreateTime); err == nil { + return t + } + } return s.SCreateTime.GetCreatedAt() } @@ -96,7 +105,10 @@ func (s *SSnapshot) GetGlobalId() string { } func (s *SSnapshot) Delete() error { - return cloudprovider.ErrNotImplemented + if s.region == nil { + return cloudprovider.ErrNotImplemented + } + return s.region.DeleteEbsSnapshot(s.Id) } func (s *SSnapshot) GetProjectId() string { @@ -104,27 +116,5 @@ func (s *SSnapshot) GetProjectId() string { } func (s *SRegion) GetSnapshots(snapshotId string, parentId string, isSystem bool) ([]SSnapshot, error) { - var apiRequest *SApiRequest - query := map[string]string{} - if len(snapshotId) > 0 { - query["backupId"] = snapshotId - } - if isSystem { - if len(parentId) > 0 { - query["serverId"] = parentId - } - apiRequest = NewApiRequest(s.ID, "/api/v2/vmBackup", query, nil) - } else { - if len(parentId) > 0 { - query["volumeId"] = parentId - } - apiRequest = NewApiRequest(s.ID, "/api/v2/volume/volumebackup", query, nil) - } - request := NewNovaRequest(apiRequest) - snapshots := make([]SSnapshot, 0) - err := s.client.doList(context.Background(), request, &snapshots) - if err != nil { - return nil, err - } - return snapshots, nil + return nil, cloudprovider.ErrNotImplemented } diff --git a/pkg/multicloud/ecloud/storage.go b/pkg/multicloud/ecloud/storage.go index ba07068c6..e124e2498 100644 --- a/pkg/multicloud/ecloud/storage.go +++ b/pkg/multicloud/ecloud/storage.go @@ -24,28 +24,43 @@ import ( "yunion.io/x/cloudmux/pkg/multicloud" ) -var storageTypes = []string{ - api.STORAGE_ECLOUD_CAPEBS, - api.STORAGE_ECLOUD_EBS, - api.STORAGE_ECLOUD_SSD, - api.STORAGE_ECLOUD_SSDEBS, - // special storage - api.STORAGE_ECLOUD_SYSTEM, +// storageTypeConstMap 将 API 返回的存储类型 code 映射为 cloudmux 统一常量值。 +// 例如 API 返回 "local" 时应使用 api.STORAGE_ECLOUD_LOCAL。 +var storageTypeConstMap = map[string]string{ + "local": api.STORAGE_ECLOUD_LOCAL, + "capacity": api.STORAGE_ECLOUD_CAPEBS, + "highPerformance": api.STORAGE_ECLOUD_SSD, + "performanceOptimization": api.STORAGE_ECLOUD_SSDEBS, + "highPerformanceyc": api.STORAGE_ECLOUD_SSDYC, + "performanceOptimizationyc": api.STORAGE_ECLOUD_SSDEBS_YC, } type SStorage struct { multicloud.SStorageBase EcloudTags - zone *SZone - storageType string + zone *SZone + + // 对齐 /api/ebs/customer/v3/volume/volumeType/list(GetVolumeConfig) + CinderType string `json:"cinderType,omitempty"` + BackupType string `json:"backupType,omitempty"` + SnapshotType string `json:"snapshotType,omitempty"` + Priority int `json:"priority,omitempty"` + AttachServerTypes []string `json:"attachServerTypes,omitempty"` + CustomBack bool `json:"customBack,omitempty"` + Iscsi bool `json:"iscsi,omitempty"` + Region string `json:"region,omitempty"` + Encryption bool `json:"encryption,omitempty"` + IsEdge bool `json:"isEdge,omitempty"` + IsStorage bool `json:"isStorage,omitempty"` + StorageType string `json:"opType,omitempty"` } func (s *SStorage) GetId() string { - return fmt.Sprintf("%s-%s-%s", s.zone.region.client.cpcfg.Id, s.zone.GetGlobalId(), s.storageType) + return fmt.Sprintf("%s-%s-%s", s.zone.region.client.cpcfg.Id, s.zone.GetGlobalId(), s.StorageType) } func (s *SStorage) GetName() string { - return fmt.Sprintf("%s-%s-%s", s.zone.region.client.cpcfg.Name, s.zone.GetId(), s.storageType) + return fmt.Sprintf("%s-%s-%s", s.zone.region.client.cpcfg.Name, s.zone.GetId(), s.StorageType) } func (s *SStorage) GetGlobalId() string { @@ -53,6 +68,9 @@ func (s *SStorage) GetGlobalId() string { } func (s *SStorage) GetStatus() string { + if !s.IsStorage { + return api.STORAGE_OFFLINE + } return api.STORAGE_ONLINE } @@ -73,9 +91,6 @@ func (s *SStorage) GetIZone() cloudprovider.ICloudZone { } func (s *SStorage) GetIDisks() ([]cloudprovider.ICloudDisk, error) { - if s.storageType == api.STORAGE_ECLOUD_SYSTEM { - return nil, nil - } disks, err := s.zone.region.GetDisks() if err != nil { return nil, err @@ -85,7 +100,7 @@ func (s *SStorage) GetIDisks() ([]cloudprovider.ICloudDisk, error) { filtedDisks := make([]SDisk, 0) for i := range disks { disk := disks[i] - if disk.Type == s.storageType && disk.Region == s.zone.Region { + if disk.Type == s.StorageType && disk.Region == s.zone.ZoneCode { filtedDisks = append(filtedDisks, disk) } } @@ -99,15 +114,11 @@ func (s *SStorage) GetIDisks() ([]cloudprovider.ICloudDisk, error) { } func (s *SStorage) GetStorageType() string { - return s.storageType + return s.StorageType } func (s *SStorage) GetMediumType() string { - if s.storageType == api.STORAGE_ECLOUD_SSD { - return api.DISK_TYPE_SSD - } else { - return api.DISK_TYPE_ROTATE - } + return api.DISK_TYPE_SSD } func (s *SStorage) GetCapacityMB() int64 { @@ -123,7 +134,7 @@ func (s *SStorage) GetStorageConf() jsonutils.JSONObject { } func (s *SStorage) GetEnabled() bool { - return true + return s.StorageType != api.STORAGE_ECLOUD_LOCAL } func (s *SStorage) CreateIDisk(conf *cloudprovider.DiskCreateConfig) (cloudprovider.ICloudDisk, error) { @@ -131,16 +142,12 @@ func (s *SStorage) CreateIDisk(conf *cloudprovider.DiskCreateConfig) (cloudprovi } func (s *SStorage) GetIDiskById(idStr string) (cloudprovider.ICloudDisk, error) { - if len(idStr) == 0 { - return nil, cloudprovider.ErrNotFound - } - - if disk, err := s.zone.region.GetDisk(idStr); err != nil { + disk, err := s.zone.region.GetDisk(idStr) + if err != nil { return nil, err - } else { - disk.storage = s - return disk, nil } + disk.storage = s + return disk, nil } func (s *SStorage) GetMountPoint() string { @@ -152,15 +159,9 @@ func (s *SStorage) IsSysDiskStore() bool { } func (s *SStorage) DisableSync() bool { - if s.storageType == api.STORAGE_ECLOUD_SYSTEM { - return true - } - return s.SStorageBase.DisableSync() + return s.StorageType == api.STORAGE_ECLOUD_LOCAL } func (s *SRegion) getStoragecache() *SStoragecache { - if s.storageCache == nil { - s.storageCache = &SStoragecache{region: s} - } - return s.storageCache + return &SStoragecache{region: s} } diff --git a/pkg/multicloud/ecloud/vpc.go b/pkg/multicloud/ecloud/vpc.go index 1250eb1a7..015692926 100644 --- a/pkg/multicloud/ecloud/vpc.go +++ b/pkg/multicloud/ecloud/vpc.go @@ -16,7 +16,6 @@ package ecloud import ( "yunion.io/x/jsonutils" - "yunion.io/x/pkg/errors" api "yunion.io/x/cloudmux/pkg/apis/compute" "yunion.io/x/cloudmux/pkg/cloudprovider" @@ -28,19 +27,15 @@ type SVpc struct { EcloudTags region *SRegion - iwires []cloudprovider.ICloudWire - - // wires - // secgroups - - Id string - Name string - Region string - EcStatus string - RouterId string - Scale string - UserId string - UserName string + + Id string `json:"id"` + Name string `json:"name"` + Region string `json:"region"` + EcStatus string `json:"ecStatus"` + RouterId string `json:"routerId"` + Scale string `json:"scale"` + UserId string `json:"userId"` + UserName string `json:"userName"` } func (v *SVpc) GetId() string { @@ -71,16 +66,11 @@ func (v *SVpc) GetStatus() string { } func (v *SVpc) Refresh() error { - n, err := v.region.getVpcById(v.Id) + n, err := v.region.GetVpc(v.Id) if err != nil { return err } return jsonutils.Update(v, n) - // TODO? v.fetchWires() -} - -func (v *SVpc) IsEmulated() bool { - return false } func (v *SVpc) GetRegion() cloudprovider.ICloudRegion { @@ -96,60 +86,23 @@ func (v *SVpc) GetCidrBlock() string { } func (v *SVpc) GetIWires() ([]cloudprovider.ICloudWire, error) { - if v.iwires == nil { - err := v.fetchWires() - if err != nil { - return nil, err - } - } - return v.iwires, nil -} - -func (v *SVpc) fetchWires() error { - networks, err := v.region.GetNetworks(v.RouterId, "") + zones, err := v.region.GetZones() if err != nil { - return err - } - izones, err := v.region.GetIZones() - if err != nil { - return errors.Wrap(err, "unable to GetZones") - } - findZone := func(zoneRegion string) *SZone { - for i := range izones { - zone := izones[i].(*SZone) - if zone.Region == zoneRegion { - return zone - } - } - return nil - } - zoneRegion2Wire := map[string]*SWire{} - for i := range networks { - zoneRegion := networks[i].Region - zone := findZone(zoneRegion) - var ( - wire *SWire - ok bool - ) - if wire, ok = zoneRegion2Wire[zoneRegion]; !ok { - wire = &SWire{ - vpc: v, - zone: zone, - } - zoneRegion2Wire[zoneRegion] = wire - } - wire.inetworks = append(wire.inetworks, &networks[i]) + return nil, err } - iwires := make([]cloudprovider.ICloudWire, 0, len(zoneRegion2Wire)) - for _, wire := range zoneRegion2Wire { - iwires = append(iwires, wire) + ret := []cloudprovider.ICloudWire{} + for i := range zones { + ret = append(ret, &SWire{ + vpc: v, + zone: &zones[i], + }) } - v.iwires = iwires - return nil + return ret, nil } func (v *SVpc) GetISecurityGroups() ([]cloudprovider.ICloudSecurityGroup, error) { - return nil, nil + // 移动云安全组为 region 维度,返回本 region 下全部安全组 + return v.region.GetISecurityGroups() } func (v *SVpc) GetIRouteTables() ([]cloudprovider.ICloudRouteTable, error) { @@ -161,7 +114,7 @@ func (v *SVpc) GetIRouteTableById(routeTableId string) (cloudprovider.ICloudRout } func (v *SVpc) Delete() error { - return cloudprovider.ErrNotImplemented + return v.region.DeleteVpc(v.RouterId) } func (v *SVpc) GetIWireById(wireId string) (cloudprovider.ICloudWire, error) { diff --git a/pkg/multicloud/ecloud/wire.go b/pkg/multicloud/ecloud/wire.go index 5ba9a2864..236643a50 100644 --- a/pkg/multicloud/ecloud/wire.go +++ b/pkg/multicloud/ecloud/wire.go @@ -25,13 +25,12 @@ import ( type SWire struct { multicloud.SResourceBase EcloudTags - vpc *SVpc - zone *SZone - inetworks []cloudprovider.ICloudNetwork + vpc *SVpc + zone *SZone } func (w *SWire) GetId() string { - return fmt.Sprintf("%s-%s", w.vpc.GetId(), w.vpc.region.GetId()) + return fmt.Sprintf("%s-%s", w.vpc.GetId(), w.zone.GetId()) } func (w *SWire) GetName() string { @@ -39,7 +38,7 @@ func (w *SWire) GetName() string { } func (w *SWire) GetGlobalId() string { - return fmt.Sprintf("%s-%s", w.vpc.GetGlobalId(), w.vpc.region.GetGlobalId()) + return w.GetId() } func (w *SWire) GetStatus() string { @@ -48,7 +47,6 @@ func (w *SWire) GetStatus() string { func (w *SWire) Refresh() error { return nil - // TODO? w.fetchNetworks() } func (w *SWire) IsEmulated() bool { @@ -64,13 +62,16 @@ func (w *SWire) GetIZone() cloudprovider.ICloudZone { } func (w *SWire) GetINetworks() ([]cloudprovider.ICloudNetwork, error) { - if w.inetworks == nil { - err := w.fetchNetworks() - if err != nil { - return nil, err - } + networks, err := w.vpc.region.GetNetworks(w.vpc.Id, w.zone.ZoneCode) + if err != nil { + return nil, err + } + inetworks := make([]cloudprovider.ICloudNetwork, len(networks)) + for i := range networks { + networks[i].wire = w + inetworks[i] = &networks[i] } - return w.inetworks, nil + return inetworks, nil } func (w *SWire) GetBandwidth() int { @@ -78,7 +79,7 @@ func (w *SWire) GetBandwidth() int { } func (w *SWire) GetINetworkById(netid string) (cloudprovider.ICloudNetwork, error) { - n, err := w.vpc.region.GetNetworkById(w.vpc.RouterId, w.zone.Region, netid) + n, err := w.vpc.region.GetNetwork(netid) if err != nil { return nil, err } @@ -87,54 +88,38 @@ func (w *SWire) GetINetworkById(netid string) (cloudprovider.ICloudNetwork, erro } func (w *SWire) CreateINetwork(opts *cloudprovider.SNetworkCreateOptions) (cloudprovider.ICloudNetwork, error) { - return nil, nil -} - -func (w *SWire) CreateNetworks() (*SNetwork, error) { - // data := jsonutils.NewDict() - // req := jsonutils.NewDict() - // req.Set("availabilityZoneHints", jsonutils.NewString("RegionOne")) - // req.Set("networkName", jsonutils.NewString("zyone")) - // req.Set("networkTypeEnum", jsonutils.NewString("VM")) - // req.Set("region", jsonutils.NewString("RegionOne")) - // req.Set("routerId", jsonutils.NewString(w.vpc.RouterId)) - // subnet := jsonutils.NewDict() - // subnet.Set("cidr", jsonutils.NewString("192.168.46.0/24")) - // subnet.Set("ipVersion", jsonutils.NewString("4")) - // subnet.Set("subnetName", jsonutils.NewString("zyone")) - // req.Set("subnets", jsonutils.NewArray(subnet)) - // data.Set("networkCreateReq", req) - // fmt.Printf("data:\n%s", data.PrettyString()) - // request := NewConsoleRequest(w.vpc.region.ID, "/api/v2/netcenter/network", nil, req) - // request.SetMethod("POST") - // resp, err := w.vpc.region.client.request(context.Background(), request) - // if err != nil { - // return nil, err - // } - // fmt.Printf("resp:%s", resp) - // return nil, nil - return nil, cloudprovider.ErrNotImplemented -} - -func (w *SWire) GetNetworkById(netId string) (*SNetwork, error) { - n, err := w.vpc.region.GetNetworkById(w.vpc.RouterId, w.zone.Region, netId) - if err != nil { - return nil, err + networkName := opts.Name + if networkName == "" { + networkName = "subnet-1" } - n.wire = w - return n, nil -} - -func (w *SWire) fetchNetworks() error { - networks, err := w.vpc.region.GetNetworks(w.vpc.RouterId, w.zone.Region) - if err != nil { - return err + // 规范:5-22 位、字母开头 + if len(networkName) < 5 { + networkName = networkName + "xxxx"[:5-len(networkName)] } - inetworks := make([]cloudprovider.ICloudNetwork, len(networks)) - for i := range networks { - networks[i].wire = w - inetworks[i] = &networks[i] + if len(networkName) > 22 { + networkName = networkName[:22] } - w.inetworks = inetworks - return nil + if c := networkName[0]; (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') { + networkName = "n" + networkName + if len(networkName) > 22 { + networkName = networkName[:22] + } + } + cidr := opts.Cidr + if cidr == "" { + cidr = "192.168.0.0/24" + } + regionPoolId := "" + if w.zone != nil && w.zone.ZoneCode != "" { + regionPoolId = regionIdToPoolId[w.zone.ZoneCode] + if regionPoolId == "" { + regionPoolId = w.zone.ZoneCode + } + } + net, err := w.vpc.region.CreateNetwork(w.vpc.RouterId, regionPoolId, networkName, cidr) + if err != nil { + return nil, err + } + net.wire = w + return net, nil } diff --git a/pkg/multicloud/ecloud/zone.go b/pkg/multicloud/ecloud/zone.go index 8a09d213b..1908c5652 100644 --- a/pkg/multicloud/ecloud/zone.go +++ b/pkg/multicloud/ecloud/zone.go @@ -17,6 +17,7 @@ package ecloud import ( "fmt" + api "yunion.io/x/cloudmux/pkg/apis/compute" "yunion.io/x/cloudmux/pkg/cloudprovider" "yunion.io/x/cloudmux/pkg/multicloud" ) @@ -25,35 +26,26 @@ type SZone struct { multicloud.SResourceBase EcloudTags region *SRegion - host *SHost - istorages []cloudprovider.ICloudStorage - - ID string `json:"id"` - Name string `json:"name"` - Region string `json:"region"` - Deleted bool - Visible bool + ZoneId string `json:"zoneId"` + ZoneName string `json:"zoneName"` + ZoneCode string `json:"zoneCode"` } func (z *SZone) GetId() string { - return z.ID + return z.ZoneCode } func (z *SZone) GetName() string { - //return fmt.Sprintf("%s %s", CLOUD_PROVIDER_ECLOUD_CN, z.Name) - return z.Name + return fmt.Sprintf("%s %s", z.region.GetName(), z.ZoneName) } func (z *SZone) GetGlobalId() string { - return fmt.Sprintf("%s/%s", z.region.GetGlobalId(), z.ID) + return fmt.Sprintf("%s/%s", z.region.GetGlobalId(), z.ZoneCode) } func (z *SZone) GetStatus() string { - if z.Deleted || !z.Visible { - return "disable" - } - return "enable" + return api.ZONE_ENABLE } func (z *SZone) Refresh() error { @@ -66,7 +58,6 @@ func (z *SZone) IsEmulated() bool { func (z *SZone) GetI18n() cloudprovider.SModelI18nTable { table := cloudprovider.SModelI18nTable{} - table["name"] = cloudprovider.NewSModelI18nEntry(z.GetName()).CN(z.GetName()).EN(fmt.Sprintf("%s %s", CLOUD_PROVIDER_ECLOUD_EN, z.Name)) return table } @@ -74,21 +65,16 @@ func (z *SZone) GetIRegion() cloudprovider.ICloudRegion { return z.region } -func (z *SZone) getHost() cloudprovider.ICloudHost { - if z.host == nil { - z.host = &SHost{ - zone: z, - } - } - return z.host +func (z *SZone) GetHost() *SHost { + return &SHost{zone: z} } func (z *SZone) GetIHosts() ([]cloudprovider.ICloudHost, error) { - return []cloudprovider.ICloudHost{z.getHost()}, nil + return []cloudprovider.ICloudHost{z.GetHost()}, nil } func (z *SZone) GetIHostById(id string) (cloudprovider.ICloudHost, error) { - host := z.getHost() + host := z.GetHost() if host.GetGlobalId() == id { return host, nil } @@ -96,28 +82,21 @@ func (z *SZone) GetIHostById(id string) (cloudprovider.ICloudHost, error) { } func (z *SZone) GetIStorages() ([]cloudprovider.ICloudStorage, error) { - if z.istorages == nil { - err := z.fetchStorages() - if err != nil { - return nil, err - } + // 直接复用区域级 GetStorages 能力,并按当前 Zone 过滤。 + storages, err := z.region.GetStorages(z.ZoneCode) + if err != nil { + return nil, err } - return z.istorages, nil -} - -func (z *SZone) fetchStorages() error { - istorages := make([]cloudprovider.ICloudStorage, len(storageTypes)) - for i := range istorages { - istorages[i] = &SStorage{ - zone: z, - storageType: storageTypes[i], - } + istorages := make([]cloudprovider.ICloudStorage, len(storages)) + for i := range storages { + // 确保 zone 指针为当前 z + storages[i].zone = z + istorages[i] = &storages[i] } - z.istorages = istorages - return nil + return istorages, nil } -func (z *SZone) getStorageByType(t string) (*SStorage, error) { +func (z *SZone) GetStorageByType(t string) (*SStorage, error) { istorages, err := z.GetIStorages() if err != nil { return nil, err @@ -146,3 +125,18 @@ func (z *SZone) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) type SZoneRegionBase struct { Region string } + +func (z *SZone) GetIWires() ([]cloudprovider.ICloudWire, error) { + vpcs, err := z.region.GetVpcs() + if err != nil { + return nil, err + } + for i := range vpcs { + vpcs[i].region = z.region + } + wires := []cloudprovider.ICloudWire{} + for i := range vpcs { + wires = append(wires, &SWire{zone: z, vpc: &vpcs[i]}) + } + return wires, nil +}