fix(region): support gcp shared vpc and image (#23295)

This commit is contained in:
屈轩
2025-09-12 23:03:22 +08:00
committed by GitHub
parent 633c8e2969
commit e6bad2e309
17 changed files with 451 additions and 44 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ require (
k8s.io/cri-api v0.22.17
k8s.io/klog/v2 v2.20.0
moul.io/http2curl/v2 v2.3.0
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250910065010-3f3e23356086
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250912144144-d0d8cf049d7f
yunion.io/x/executor v0.0.0-20250518005516-5402e9e0bed0
yunion.io/x/jsonutils v1.0.1-0.20250507052344-1abcf4f443b1
yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91
+2 -2
View File
@@ -1409,8 +1409,8 @@ sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250910065010-3f3e23356086 h1:13R94JDtFvP+pXmcTJR9L1oueeQiuPWzYs7ucHTTKH0=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250910065010-3f3e23356086/go.mod h1:7P/TJZk8o4JjhFnF1nGZcsPg+sIpMoV0dWPPuG6yGLg=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250912144144-d0d8cf049d7f h1:E17WmoLx6siAZLLzi1bho2mV006FFXo4q4l2CgwV0mQ=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250912144144-d0d8cf049d7f/go.mod h1:7P/TJZk8o4JjhFnF1nGZcsPg+sIpMoV0dWPPuG6yGLg=
yunion.io/x/executor v0.0.0-20250518005516-5402e9e0bed0 h1:msG4SiDSVU7CrXH06WuHlNEZXIooTcmNbfrIGHuIHBU=
yunion.io/x/executor v0.0.0-20250518005516-5402e9e0bed0/go.mod h1:Uxuou9WQIeJXNpy7t2fPLL0BYLvLiMvGQwY7Qc6aSws=
yunion.io/x/jsonutils v0.0.0-20190625054549-a964e1e8a051/go.mod h1:4N0/RVzsYL3kH3WE/H1BjUQdFiWu50JGCFQuuy+Z634=
+22
View File
@@ -329,6 +329,28 @@ func (self *SGoogleRegionDriver) GetSecurityGroupFilter(vpc *models.SVpc) (func(
}, nil
}
func (self *SGoogleRegionDriver) RequestPrepareSecurityGroups(
ctx context.Context,
userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider,
secgroups []models.SSecurityGroup,
vpc *models.SVpc,
callback func(ids []string) error,
task taskman.ITask,
) error {
// 共享vpc不支持创建安全组
if strings.HasPrefix(vpc.ExternalId, "projects/") {
if callback != nil {
err := callback([]string{})
if err != nil {
return errors.Wrapf(err, "callback")
}
}
return task.ScheduleRun(nil)
}
return self.SManagedVirtualizationRegionDriver.RequestPrepareSecurityGroups(ctx, userCred, ownerId, secgroups, vpc, callback, task)
}
func (self *SGoogleRegionDriver) CreateDefaultSecurityGroup(
ctx context.Context,
userCred mcclient.TokenCredential,
+1 -1
View File
@@ -1852,7 +1852,7 @@ sigs.k8s.io/structured-merge-diff/v4/value
# sigs.k8s.io/yaml v1.2.0
## explicit; go 1.12
sigs.k8s.io/yaml
# yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250910065010-3f3e23356086
# yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250912144144-d0d8cf049d7f
## explicit; go 1.24
yunion.io/x/cloudmux/pkg/apis
yunion.io/x/cloudmux/pkg/apis/billing
+3
View File
@@ -227,6 +227,9 @@ func (awscli *SAwsClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error)
subAccount.Name = awscli.cpcfg.Name
subAccount.Account = awscli.accessKey
subAccount.Id = awscli.accountId
if len(subAccount.Id) == 0 {
subAccount.Id = awscli.GetAccountId()
}
subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
return []cloudprovider.SSubAccount{subAccount}, nil
} else {
+8
View File
@@ -156,5 +156,13 @@ func (self *SGoogleClient) GetICloudGlobalVpcs() ([]cloudprovider.ICloudGlobalVp
gvpcs[i].client = self
ret = append(ret, &gvpcs[i])
}
sharedVpcs, err := self.GetSharedGlobalNetworks()
if err != nil {
return nil, errors.Wrapf(err, "GetSharedVpcs")
}
for i := range sharedVpcs {
sharedVpcs[i].client = self
ret = append(ret, &sharedVpcs[i])
}
return ret, nil
}
+9 -3
View File
@@ -327,7 +327,9 @@ func (self *SGoogleClient) ecsList(resource string, params map[string]string) (j
}
func (self *SGoogleClient) _ecsList(method, resource string, params map[string]string) (jsonutils.JSONObject, error) {
resource = fmt.Sprintf("projects/%s/%s", self.projectId, resource)
if !strings.HasPrefix(resource, "projects/") {
resource = fmt.Sprintf("projects/%s/%s", self.projectId, resource)
}
return jsonRequest(self.client, httputils.THttpMethod(method), GOOGLE_COMPUTE_DOMAIN, GOOGLE_API_VERSION, resource, params, nil, self.debug)
}
@@ -453,9 +455,13 @@ func (self *SGoogleClient) ecsPatch(resource string, action string, params map[s
return selfLink, nil
}
func (self *SGoogleClient) ecsDo(resource string, action string, params map[string]string, body jsonutils.JSONObject) (string, error) {
func (self *SGoogleClient) ecsPost(resource string, action string, params map[string]string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) {
resource = fmt.Sprintf("%s/%s", resource, action)
resp, err := jsonRequest(self.client, "POST", GOOGLE_COMPUTE_DOMAIN, GOOGLE_API_VERSION, resource, params, body, self.debug)
return jsonRequest(self.client, "POST", GOOGLE_COMPUTE_DOMAIN, GOOGLE_API_VERSION, resource, params, body, self.debug)
}
func (self *SGoogleClient) ecsDo(resource string, action string, params map[string]string, body jsonutils.JSONObject) (string, error) {
resp, err := self.ecsPost(resource, action, params, body)
if err != nil {
return "", err
}
+7 -1
View File
@@ -169,9 +169,15 @@ func (image *SImage) Refresh() error {
}
func (image *SImage) GetImageType() cloudprovider.TImageType {
if strings.Index(image.SelfLink, image.storagecache.region.GetProjectId()) >= 0 {
projects, err := image.storagecache.region.client.GetProjects()
if err != nil {
return cloudprovider.ImageTypeCustomized
}
for _, project := range projects {
if strings.Contains(image.SelfLink, project.ProjectId) {
return cloudprovider.ImageTypeCustomized
}
}
return cloudprovider.ImageTypeSystem
}
+10 -7
View File
@@ -568,10 +568,6 @@ func (region *SRegion) CreateInstance(zone, name, desc, instanceType string, cpu
}
func (region *SRegion) _createVM(zone string, desc *cloudprovider.SManagedVMCreateConfig) (*SInstance, error) {
vpc, err := region.GetVpc(desc.ExternalNetworkId)
if err != nil {
return nil, errors.Wrap(err, "region.GetNetwork")
}
if len(desc.InstanceType) == 0 {
desc.InstanceType = fmt.Sprintf("custom-%d-%d", desc.Cpu, desc.MemoryMB)
}
@@ -611,9 +607,16 @@ func (region *SRegion) _createVM(zone string, desc *cloudprovider.SManagedVMCrea
"autoDelete": true,
})
}
networkInterface := map[string]string{
"network": vpc.Network,
"subnetwork": vpc.SelfLink,
"subnetwork": desc.ExternalNetworkId,
}
if !strings.HasPrefix(desc.ExternalNetworkId, "projects/") {
vpc, err := region.GetVpc(desc.ExternalNetworkId)
if err != nil {
return nil, errors.Wrap(err, "region.GetNetwork")
}
networkInterface["network"] = vpc.SelfLink
}
if len(desc.IpAddr) > 0 {
networkInterface["networkIp"] = desc.IpAddr
@@ -658,7 +661,7 @@ func (region *SRegion) _createVM(zone string, desc *cloudprovider.SManagedVMCrea
log.Debugf("create google instance params: %s", jsonutils.Marshal(params).String())
instance := &SInstance{}
resource := fmt.Sprintf("zones/%s/instances", zone)
err = region.Insert(resource, jsonutils.Marshal(params), instance)
err := region.Insert(resource, jsonutils.Marshal(params), instance)
if err != nil {
return nil, err
}
+5
View File
@@ -15,6 +15,8 @@
package google
import (
"strings"
"yunion.io/x/pkg/util/netutils"
"yunion.io/x/cloudmux/pkg/cloudprovider"
@@ -56,6 +58,9 @@ func (nic *SNetworkInterface) InClassicNetwork() bool {
}
func (nic *SNetworkInterface) GetINetworkId() string {
if !strings.Contains(nic.Subnetwork, nic.instance.host.zone.region.client.projectId) {
return getGlobalId(nic.Subnetwork)
}
vpc := &SVpc{region: nic.instance.host.zone.region}
err := nic.instance.host.zone.region.GetBySelfId(nic.Subnetwork, vpc)
if err != nil {
+58 -19
View File
@@ -32,31 +32,49 @@ type SNetwork struct {
}
func (network *SNetwork) GetProjectId() string {
return network.wire.vpc.region.GetProjectId()
if network.wire.vpc != nil {
return network.wire.vpc.region.GetProjectId()
}
return network.wire.shareVpc.region.GetProjectId()
}
func (self *SNetwork) GetName() string {
return self.wire.vpc.GetName()
func (network *SNetwork) GetName() string {
if network.wire.vpc != nil {
return network.wire.vpc.GetName()
}
return network.wire.shareVpc.GetName()
}
func (self *SNetwork) GetId() string {
return self.wire.vpc.GetId()
func (network *SNetwork) GetId() string {
if network.wire.vpc != nil {
return network.wire.vpc.GetId()
}
return network.wire.shareVpc.GetId()
}
func (self *SNetwork) GetDescription() string {
func (network *SNetwork) GetDescription() string {
return ""
}
func (self *SNetwork) GetGlobalId() string {
return self.wire.vpc.GetGlobalId()
func (network *SNetwork) GetGlobalId() string {
if network.wire.vpc != nil {
return network.wire.vpc.GetGlobalId()
}
return network.wire.shareVpc.GetGlobalId()
}
func (self *SNetwork) Refresh() error {
return self.wire.vpc.Refresh()
func (network *SNetwork) Refresh() error {
if network.wire.vpc != nil {
return network.wire.vpc.Refresh()
}
return network.wire.shareVpc.Refresh()
}
func (network *SNetwork) IsEmulated() bool {
return false
if network.wire.vpc != nil {
return network.wire.vpc.IsEmulated()
}
return network.wire.shareVpc.IsEmulated()
}
func (network *SNetwork) GetStatus() string {
@@ -79,27 +97,48 @@ func (network *SNetwork) GetIWire() cloudprovider.ICloudWire {
return network.wire
}
func (self *SNetwork) GetIpStart() string {
pref, _ := netutils.NewIPV4Prefix(self.wire.vpc.IpCidrRange)
func (network *SNetwork) GetIpStart() string {
cidr := ""
if network.wire.vpc != nil {
cidr = network.wire.vpc.IpCidrRange
} else {
cidr = network.wire.shareVpc.IpCidrRange
}
pref, _ := netutils.NewIPV4Prefix(cidr)
startIp := pref.Address.NetAddr(pref.MaskLen) // 0
startIp = startIp.StepUp() // 1
return startIp.String()
}
func (self *SNetwork) GetIpEnd() string {
pref, _ := netutils.NewIPV4Prefix(self.wire.vpc.IpCidrRange)
func (network *SNetwork) GetIpEnd() string {
cidr := ""
if network.wire.vpc != nil {
cidr = network.wire.vpc.IpCidrRange
} else {
cidr = network.wire.shareVpc.IpCidrRange
}
pref, _ := netutils.NewIPV4Prefix(cidr)
endIp := pref.Address.BroadcastAddr(pref.MaskLen) // 255
endIp = endIp.StepDown() // 254
return endIp.String()
}
func (self *SNetwork) GetIpMask() int8 {
pref, _ := netutils.NewIPV4Prefix(self.wire.vpc.IpCidrRange)
func (network *SNetwork) GetIpMask() int8 {
cidr := ""
if network.wire.vpc != nil {
cidr = network.wire.vpc.IpCidrRange
} else {
cidr = network.wire.shareVpc.IpCidrRange
}
pref, _ := netutils.NewIPV4Prefix(cidr)
return pref.MaskLen
}
func (self *SNetwork) GetGateway() string {
return self.wire.vpc.GatewayAddress
func (network *SNetwork) GetGateway() string {
if network.wire.vpc != nil {
return network.wire.vpc.GatewayAddress
}
return network.GetIpStart()
}
func (network *SNetwork) GetServerType() string {
+27
View File
@@ -189,10 +189,37 @@ func (self *SRegion) GetIVpcs() ([]cloudprovider.ICloudVpc, error) {
vpcs[i].region = self
ret = append(ret, &vpcs[i])
}
sharedNetworks, err := self.client.GetSharedGlobalNetworks()
if err != nil {
return nil, errors.Wrapf(err, "GetSharedGlobalNetworks")
}
for i := range sharedNetworks {
for j := range sharedNetworks[i].Subnetworks {
if strings.Contains(sharedNetworks[i].Subnetworks[j].Subnetwork, fmt.Sprintf("/%s/", self.Name)) {
sharedNetworks[i].Subnetworks[j].region = self
ret = append(ret, &sharedNetworks[i].Subnetworks[j])
}
}
}
return ret, nil
}
func (self *SRegion) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) {
if utils.IsInStringArray(self.Name, MultiRegions) || utils.IsInStringArray(self.Name, DualRegions) {
return nil, cloudprovider.ErrNotFound
}
if strings.Contains(id, "projects/") {
vpcs, err := self.GetIVpcs()
if err != nil {
return nil, err
}
for i := range vpcs {
if vpcs[i].GetGlobalId() == id {
return vpcs[i], nil
}
}
return nil, cloudprovider.ErrNotFound
}
vpc, err := self.GetVpc(id)
if err != nil {
return nil, errors.Wrapf(err, "GetVpc")
+108
View File
@@ -0,0 +1,108 @@
package google
import (
"fmt"
"strings"
"time"
api "yunion.io/x/cloudmux/pkg/apis/compute"
"yunion.io/x/cloudmux/pkg/cloudprovider"
)
type SSharedGlobalNetwork struct {
GoogleTags
client *SGoogleClient
Network string
Subnetworks []SXpnNetwork
}
func (network *SSharedGlobalNetwork) GetId() string {
return getGlobalId(network.Network)
}
func (network *SSharedGlobalNetwork) GetGlobalId() string {
return network.GetId()
}
func (network *SSharedGlobalNetwork) GetName() string {
info := strings.Split(network.GetId(), "/")
if len(info) == 6 {
return fmt.Sprintf("%s(%s)", info[5], info[1])
}
return network.GetId()
}
func (network *SSharedGlobalNetwork) GetStatus() string {
return api.GLOBAL_VPC_STATUS_AVAILABLE
}
func (network *SSharedGlobalNetwork) IsEmulated() bool {
return true
}
func (network *SSharedGlobalNetwork) Refresh() error {
return nil
}
func (network *SSharedGlobalNetwork) GetCreatedAt() time.Time {
return time.Time{}
}
func (network *SSharedGlobalNetwork) GetDescription() string {
return ""
}
func (network *SSharedGlobalNetwork) GetISecurityGroups() ([]cloudprovider.ICloudSecurityGroup, error) {
return []cloudprovider.ICloudSecurityGroup{}, nil
}
func (network *SSharedGlobalNetwork) CreateISecurityGroup(opts *cloudprovider.SecurityGroupCreateInput) (cloudprovider.ICloudSecurityGroup, error) {
return nil, cloudprovider.ErrNotSupported
}
func (network *SSharedGlobalNetwork) Delete() error {
return cloudprovider.ErrNotSupported
}
func (client *SGoogleClient) GetSharedGlobalNetworks() ([]SSharedGlobalNetwork, error) {
xhosts, err := client.GetXpnHosts()
if err != nil {
if e, ok := err.(*gError); ok && e.ErrorInfo.Code == 400 {
return []SSharedGlobalNetwork{}, nil
}
return nil, err
}
ret := []SSharedGlobalNetwork{}
networkMap := map[string][]SXpnNetwork{}
for _, xhost := range xhosts {
resources, err := client.GetXpnResources(xhost.Name)
if err != nil {
return nil, err
}
for _, resource := range resources {
if strings.EqualFold(resource.Type, "project") && resource.Id == client.projectId {
networks, err := client.GetXpnNetworks(xhost.Name)
if err != nil {
return nil, err
}
for i := range networks {
_, ok := networkMap[networks[i].Network]
if !ok {
networkMap[networks[i].Network] = []SXpnNetwork{}
}
networkMap[networks[i].Network] = append(networkMap[networks[i].Network], networks[i])
}
}
}
}
for network, subnetworks := range networkMap {
ret = append(ret, SSharedGlobalNetwork{
client: client,
Network: network,
Subnetworks: subnetworks,
})
}
return ret, nil
}
+11 -5
View File
@@ -66,14 +66,20 @@ func (cache *SStoragecache) GetICloudImages() ([]cloudprovider.ICloudImage, erro
}
func (cache *SStoragecache) GetICustomizedCloudImages() ([]cloudprovider.ICloudImage, error) {
images, err := cache.region.GetImages(cache.region.client.projectId, 1000, "")
projects, err := cache.region.client.GetProjects()
if err != nil {
return nil, errors.Wrapf(err, "GetImages")
return nil, errors.Wrapf(err, "GetProjects")
}
ret := []cloudprovider.ICloudImage{}
for i := range images {
images[i].storagecache = cache
ret = append(ret, &images[i])
for _, project := range projects {
images, err := cache.region.GetImages(project.ProjectId, 1000, "")
if err != nil {
return nil, errors.Wrapf(err, "GetImages")
}
for i := range images {
images[i].storagecache = cache
ret = append(ret, &images[i])
}
}
return ret, nil
}
+21 -5
View File
@@ -28,19 +28,32 @@ import (
type SWire struct {
multicloud.SResourceBase
GoogleTags
vpc *SVpc
vpc *SVpc
shareVpc *SXpnNetwork
}
func (wire *SWire) GetId() string {
return wire.vpc.GetGlobalId()
if wire.vpc != nil {
return wire.vpc.GetGlobalId()
}
return wire.shareVpc.GetGlobalId()
}
func (wire *SWire) GetGlobalId() string {
return fmt.Sprintf("%s-%s", wire.GetId(), wire.vpc.region.Name)
name := ""
if wire.vpc != nil {
name = wire.vpc.region.Name
} else {
name = wire.shareVpc.region.Name
}
return fmt.Sprintf("%s-%s", wire.GetId(), name)
}
func (wire *SWire) GetName() string {
return wire.vpc.GetName()
if wire.vpc != nil {
return wire.vpc.GetName()
}
return wire.shareVpc.GetName()
}
func (wire *SWire) GetCreatedAt() time.Time {
@@ -52,7 +65,10 @@ func (wire *SWire) CreateINetwork(opts *cloudprovider.SNetworkCreateOptions) (cl
}
func (wire *SWire) GetIVpc() cloudprovider.ICloudVpc {
return wire.vpc
if wire.vpc != nil {
return wire.vpc
}
return wire.shareVpc
}
func (wire *SWire) GetIZone() cloudprovider.ICloudZone {
+41
View File
@@ -0,0 +1,41 @@
package google
import "fmt"
type SXpnHost struct {
Kind string
Name string
}
func (cli *SGoogleClient) GetXpnHosts() ([]SXpnHost, error) {
res := fmt.Sprintf("projects/%s", cli.projectId)
resp, err := cli.ecsPost(res, "listXpnHosts", nil, nil)
if err != nil {
return nil, err
}
ret := []SXpnHost{}
err = resp.Unmarshal(&ret, "items")
if err != nil {
return nil, err
}
return ret, nil
}
type SXpnResource struct {
Type string
Id string
}
func (cli *SGoogleClient) GetXpnResources(projectId string) ([]SXpnResource, error) {
res := fmt.Sprintf("projects/%s/getXpnResources", projectId)
resp, err := cli.ecsList(res, nil)
if err != nil {
return nil, err
}
ret := []SXpnResource{}
err = resp.Unmarshal(&ret, "resources")
if err != nil {
return nil, err
}
return ret, nil
}
+117
View File
@@ -0,0 +1,117 @@
package google
import (
"fmt"
"strings"
"time"
api "yunion.io/x/cloudmux/pkg/apis/compute"
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/cloudmux/pkg/multicloud"
)
type SXpnNetwork struct {
multicloud.SVpc
multicloud.STagBase
region *SRegion
Subnetwork string
Network string
IpCidrRange string
StackType string
Purpose string
}
func (network *SXpnNetwork) GetGlobalVpcId() string {
return getGlobalId(network.Network)
}
func (network *SXpnNetwork) GetId() string {
return getGlobalId(network.Subnetwork)
}
func (network *SXpnNetwork) GetName() string {
info := strings.Split(network.GetId(), "/")
if len(info) == 6 {
return fmt.Sprintf("%s(%s)", info[5], info[1])
}
return network.GetId()
}
func (network *SXpnNetwork) GetGlobalId() string {
return network.GetId()
}
func (network *SXpnNetwork) GetStatus() string {
return api.VPC_STATUS_AVAILABLE
}
func (network *SXpnNetwork) Refresh() error {
return nil
}
func (network *SXpnNetwork) Delete() error {
return cloudprovider.ErrNotSupported
}
func (network *SXpnNetwork) GetCreatedAt() time.Time {
return time.Time{}
}
func (network *SXpnNetwork) GetCidrBlock() string {
return network.IpCidrRange
}
func (network *SXpnNetwork) IsEmulated() bool {
return true
}
func (network *SXpnNetwork) GetIsDefault() bool {
return false
}
func (network *SXpnNetwork) GetRegion() cloudprovider.ICloudRegion {
return network.region
}
func (network *SXpnNetwork) GetIRouteTables() ([]cloudprovider.ICloudRouteTable, error) {
return nil, cloudprovider.ErrNotSupported
}
func (network *SXpnNetwork) GetIRouteTableById(routeTableId string) (cloudprovider.ICloudRouteTable, error) {
return nil, cloudprovider.ErrNotSupported
}
func (network *SXpnNetwork) GetISecurityGroups() ([]cloudprovider.ICloudSecurityGroup, error) {
return nil, cloudprovider.ErrNotSupported
}
func (network *SXpnNetwork) getWire() *SWire {
return &SWire{shareVpc: network}
}
func (network *SXpnNetwork) GetIWires() ([]cloudprovider.ICloudWire, error) {
return []cloudprovider.ICloudWire{network.getWire()}, nil
}
func (network *SXpnNetwork) GetIWireById(wireId string) (cloudprovider.ICloudWire, error) {
if wireId != network.getWire().GetGlobalId() {
return nil, cloudprovider.ErrNotFound
}
return network.getWire(), nil
}
func (client *SGoogleClient) GetXpnNetworks(projectId string) ([]SXpnNetwork, error) {
res := fmt.Sprintf("projects/%s/aggregated/subnetworks/listUsable", projectId)
resp, err := client.ecsList(res, nil)
if err != nil {
return nil, err
}
ret := []SXpnNetwork{}
err = resp.Unmarshal(&ret, "items")
if err != nil {
return nil, err
}
return ret, nil
}