From 8e2d084e60066b87b4931a391cac2dbd6f852c35 Mon Sep 17 00:00:00 2001 From: Zhenguo Niu Date: Fri, 3 Apr 2020 15:42:46 +0800 Subject: [PATCH] Add CCI Resource (#294) --- huaweicloud/config.go | 7 + huaweicloud/provider.go | 1 + huaweicloud/provider_test.go | 10 + .../resource_huaweicloud_cci_network_v1.go | 230 ++++++++++++++++++ ...esource_huaweicloud_cci_network_v1_test.go | 98 ++++++++ .../openstack/cci/v1/networks/requests.go | 86 +++++++ .../openstack/cci/v1/networks/results.go | 62 +++++ .../openstack/cci/v1/networks/urls.go | 11 + vendor/modules.txt | 1 + website/docs/r/cci_network_v1.html.md | 68 ++++++ website/huaweicloud.erb | 14 ++ 11 files changed, 588 insertions(+) create mode 100644 huaweicloud/resource_huaweicloud_cci_network_v1.go create mode 100644 huaweicloud/resource_huaweicloud_cci_network_v1_test.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/requests.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/results.go create mode 100644 vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/urls.go create mode 100644 website/docs/r/cci_network_v1.html.md diff --git a/huaweicloud/config.go b/huaweicloud/config.go index c07bf746e5..95000bc6cc 100644 --- a/huaweicloud/config.go +++ b/huaweicloud/config.go @@ -520,6 +520,13 @@ func (c *Config) cceV3Client(region string) (*golangsdk.ServiceClient, error) { }) } +func (c *Config) cciV1Client(region string) (*golangsdk.ServiceClient, error) { + return huaweisdk.CCIV1(c.HwClient, golangsdk.EndpointOpts{ + Region: c.determineRegion(region), + Availability: c.getHwEndpointType(), + }) +} + func (c *Config) objectStorageV1Client(region string) (*golangsdk.ServiceClient, error) { // If Swift Authentication is being used, return a swauth client. if c.Swauth { diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index dc6efe3ae4..61f8fb89de 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -308,6 +308,7 @@ func Provider() terraform.ResourceProvider { "huaweicloud_vpc_subnet_v1": resourceVpcSubnetV1(), "huaweicloud_cce_cluster_v3": resourceCCEClusterV3(), "huaweicloud_cce_node_v3": resourceCCENodeV3(), + "huaweicloud_cci_network_v1": resourceCCINetworkV1(), "huaweicloud_rts_software_config_v1": resourceSoftwareConfigV1(), "huaweicloud_ces_alarmrule": resourceAlarmRule(), "huaweicloud_as_configuration_v1": resourceASConfiguration(), diff --git a/huaweicloud/provider_test.go b/huaweicloud/provider_test.go index 13f592121f..d4f663780e 100644 --- a/huaweicloud/provider_test.go +++ b/huaweicloud/provider_test.go @@ -30,12 +30,14 @@ var ( OS_SRC_SECRET_KEY = os.Getenv("OS_SRC_SECRET_KEY") OS_VPC_ID = os.Getenv("OS_VPC_ID") OS_TENANT_ID = os.Getenv("OS_TENANT_ID") + OS_DOMAIN_ID = os.Getenv("OS_DOMAIN_ID") OS_SSH_KEY = os.Getenv("OS_SSH_KEY") OS_DWS_ENVIRONMENT = os.Getenv("OS_DWS_ENVIRONMENT") OS_MRS_ENVIRONMENT = os.Getenv("OS_MRS_ENVIRONMENT") OS_DMS_ENVIRONMENT = os.Getenv("OS_DMS_ENVIRONMENT") OS_NAT_ENVIRONMENT = os.Getenv("OS_NAT_ENVIRONMENT") OS_KMS_ENVIRONMENT = os.Getenv("OS_KMS_ENVIRONMENT") + OS_CCI_ENVIRONMENT = os.Getenv("OS_CCI_ENVIRONMENT") OS_CDN_DOMAIN_NAME = os.Getenv("OS_CDN_DOMAIN_NAME") ) @@ -207,6 +209,14 @@ func testAccPreCheckCDN(t *testing.T) { } } +func testAccPreCheckCCI(t *testing.T) { + testAccPreCheckRequiredEnvVars(t) + + if OS_CCI_ENVIRONMENT == "" { + t.Skip("This environment does not support CCI tests") + } +} + func TestProvider(t *testing.T) { if err := Provider().(*schema.Provider).InternalValidate(); err != nil { t.Fatalf("err: %s", err) diff --git a/huaweicloud/resource_huaweicloud_cci_network_v1.go b/huaweicloud/resource_huaweicloud_cci_network_v1.go new file mode 100644 index 0000000000..d05fbf9ccd --- /dev/null +++ b/huaweicloud/resource_huaweicloud_cci_network_v1.go @@ -0,0 +1,230 @@ +package huaweicloud + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/openstack/cci/v1/networks" +) + +func resourceCCINetworkV1() *schema.Resource { + return &schema.Resource{ + Create: resourceCCINetworkV1Create, + Read: resourceCCINetworkV1Read, + Delete: resourceCCINetworkV1Delete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + //request and response parameters + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "namespace": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "security_group": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "project_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "domain_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "network_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "subnet_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "available_zone": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cidr": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func resourceNetworkAnnotationsV1(d *schema.ResourceData) map[string]string { + m := map[string]string{ + "network.alpha.kubernetes.io/default-security-group": d.Get("security_group").(string), + "network.alpha.kubernetes.io/domain_id": d.Get("domain_id").(string), + "network.alpha.kubernetes.io/project_id": d.Get("project_id").(string), + } + return m +} + +func resourceCCINetworkV1Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + cciClient, err := config.cciV1Client(GetRegion(d, config)) + + if err != nil { + return fmt.Errorf("Unable to create HuaweiCloud CCI client : %s", err) + } + + createOpts := networks.CreateOpts{ + Kind: "Network", + ApiVersion: "networking.cci.io/v1beta1", + Metadata: networks.CreateMetaData{ + Name: d.Get("name").(string), + Annotations: resourceNetworkAnnotationsV1(d), + }, + Spec: networks.Spec{ + AttachedVPC: d.Get("vpc_id").(string), + NetworkType: "underlay_neutron", + NetworkID: d.Get("network_id").(string), + SubnetID: d.Get("subnet_id").(string), + AvailableZone: d.Get("available_zone").(string), + Cidr: d.Get("cidr").(string), + }, + } + + ns := d.Get("namespace").(string) + create, err := networks.Create(cciClient, ns, createOpts).Extract() + + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud CCI Network: %s", err) + } + + log.Printf("[DEBUG] Waiting for HuaweiCloud CCI network (%s) to become available", create.Metadata.Name) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"Initializing", "Pending"}, + Target: []string{"Active"}, + Refresh: waitForCCINetworkActive(cciClient, ns, create.Metadata.Name), + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud CCI network: %s", err) + } + d.SetId(create.Metadata.Name) + + return resourceCCINetworkV1Read(d, meta) +} + +func resourceCCINetworkV1Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + cciClient, err := config.cciV1Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud CCI client: %s", err) + } + + ns := d.Get("namespace").(string) + n, err := networks.Get(cciClient, ns, d.Id()).Extract() + if err != nil { + if _, ok := err.(golangsdk.ErrDefault404); ok { + d.SetId("") + return nil + } + + return fmt.Errorf("Error retrieving HuaweiCloud CCI: %s", err) + } + + d.Set("name", n.Metadata.Name) + d.Set("vpc_id", n.Spec.AttachedVPC) + d.Set("network_id", n.Spec.NetworkID) + d.Set("subnet_id", n.Spec.SubnetID) + d.Set("available_zone", n.Spec.AvailableZone) + d.Set("cidr", n.Spec.Cidr) + + return nil +} + +func resourceCCINetworkV1Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + cciClient, err := config.cciV1Client(GetRegion(d, config)) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud CCI Client: %s", err) + } + + ns := d.Get("namespace").(string) + err = networks.Delete(cciClient, ns, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting HuaweiCloud CCI Network: %s", err) + } + stateConf := &resource.StateChangeConf{ + Pending: []string{"Terminating", "Active"}, + Target: []string{"Deleted"}, + Refresh: waitForCCINetworkDelete(cciClient, ns, d.Id()), + Timeout: d.Timeout(schema.TimeoutDelete), + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + + if err != nil { + return fmt.Errorf("Error deleting HuaweiCloud CCI network: %s", err) + } + + d.SetId("") + return nil +} + +func waitForCCINetworkActive(cciClient *golangsdk.ServiceClient, ns, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + n, err := networks.Get(cciClient, ns, name).Extract() + if err != nil { + return nil, "", err + } + + return n, n.Status.State, nil + } +} + +func waitForCCINetworkDelete(cciClient *golangsdk.ServiceClient, ns, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[DEBUG] Attempting to delete HuaweiCloud CCI network %s.\n", name) + + r, err := networks.Get(cciClient, ns, name).Extract() + if err != nil { + if _, ok := err.(golangsdk.ErrDefault404); ok { + log.Printf("[DEBUG] Successfully deleted HuaweiCloud CCI network %s", name) + return r, "Deleted", nil + } + } + if r.Status.State == "Terminating" { + return r, "Terminating", nil + } + log.Printf("[DEBUG] HuaweiCloud CCI network %s still available.\n", name) + return r, "Active", nil + } +} diff --git a/huaweicloud/resource_huaweicloud_cci_network_v1_test.go b/huaweicloud/resource_huaweicloud_cci_network_v1_test.go new file mode 100644 index 0000000000..c4f3711176 --- /dev/null +++ b/huaweicloud/resource_huaweicloud_cci_network_v1_test.go @@ -0,0 +1,98 @@ +package huaweicloud + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + + "github.com/huaweicloud/golangsdk/openstack/cci/v1/networks" +) + +func TestAccCCINetworkV1_basic(t *testing.T) { + var network networks.Network + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckCCI(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCCINetworkV1Destroy, + Steps: []resource.TestStep{ + { + Config: testAccCCINetworkV1_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCCINetworkV1Exists("huaweicloud_cci_network_v1.net_1", &network), + resource.TestCheckResourceAttr( + "huaweicloud_cci_network_v1.net_1", "name", "cci-net"), + ), + }, + }, + }) +} + +func testAccCheckCCINetworkV1Destroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + cciClient, err := config.cciV1Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud CCI client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "huaweicloud_cci_network_v1" { + continue + } + + _, err := networks.Get(cciClient, "test_ns", rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Network still exists") + } + } + + return nil +} + +func testAccCheckCCINetworkV1Exists(n string, network *networks.Network) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + cciClient, err := config.cciV1Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating HuaweiCloud CCI client: %s", err) + } + + found, err := networks.Get(cciClient, "test_ns", rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.Metadata.Name != rs.Primary.ID { + return fmt.Errorf("Network not found") + } + + *network = *found + + return nil + } +} + +var testAccCCINetworkV1_basic = fmt.Sprintf(` +resource "huaweicloud_cci_network_v1" "net_1" { + name = "cci-net" + namespace = "test-ns" + security_group = "3b5ceb06-3b8d-43ee-866a-dc0443b85de8" + project_id = "%s" + domain_id = "%s" + vpc_id = "%s" + network_id = "%s" + subnet_id = "%s" + available_zone = "cn-north-1a" + cidr = "192.168.0.0/24" +}`, OS_TENANT_ID, OS_DOMAIN_ID, OS_VPC_ID, OS_NETWORK_ID, OS_SUBNET_ID) diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/requests.go b/vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/requests.go new file mode 100644 index 0000000000..75d650354f --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/requests.go @@ -0,0 +1,86 @@ +package networks + +import ( + "github.com/huaweicloud/golangsdk" +) + +var RequestOpts golangsdk.RequestOpts = golangsdk.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/json"}, +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new network +type CreateOpts struct { + // API type, fixed value Network + Kind string `json:"kind" required:"true"` + // API version, fixed value networking.cci.io + ApiVersion string `json:"apiVersion" required:"true"` + // Metadata required to create a network + Metadata CreateMetaData `json:"metadata" required:"true"` + // Specifications to create a network + Spec Spec `json:"spec" required:"true"` +} + +// Metadata required to create a network +type CreateMetaData struct { + //Network unique name + Name string `json:"name" required:"true"` + //Network annotation, key/value pair format + Annotations map[string]string `json:"annotations" required:"true"` +} + +// Specifications to create a network +type Spec struct { + // Network CIDR + Cidr string `json:"type,omitempty"` + // Network VPC ID + AttachedVPC string `json:"attachedVPC" required:"true"` + // Network Type + NetworkType string `json:"networkType" required:"true"` + // Network ID + NetworkID string `json:"networkID" required:"true"` + // Subnet ID + SubnetID string `json:"subnetID" required:"true"` + // Network AZ + AvailableZone string `json:"availableZone" required:"true"` +} + +// ToNetworkCreateMap builds a create request body from CreateOpts. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Create accepts a CreateOpts struct and uses the values to create a new network. +func Create(c *golangsdk.ServiceClient, ns string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToNetworkCreateMap() + if err != nil { + r.Err = err + return + } + reqOpt := &golangsdk.RequestOpts{OkCodes: []int{201}} + _, r.Err = c.Post(rootURL(c, ns), b, &r.Body, reqOpt) + return +} + +// Get retrieves a particular network based on its unique ID. +func Get(c *golangsdk.ServiceClient, ns, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, ns, id), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return +} + +// Delete will permanently delete a particular network based on its unique ID. +func Delete(c *golangsdk.ServiceClient, ns, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, ns, id), &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return +} diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/results.go b/vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/results.go new file mode 100644 index 0000000000..5c826ecb72 --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/results.go @@ -0,0 +1,62 @@ +package networks + +import ( + "github.com/huaweicloud/golangsdk" +) + +type Network struct { + //API type, fixed value Network + Kind string `json:"kind"` + //API version, fixed value networking.cci.io + ApiVersion string `json:"apiVersion"` + //Metadata of a Network + Metadata MetaData `json:"metadata"` + //Specifications of a Network + Spec Spec `json:"spec"` + //Status of a Network + Status Status `json:"status"` +} + +//Metadata required to create a network +type MetaData struct { + //Network unique name + Name string `json:"name"` + //Network unique Id + Id string `json:"uid"` + //Network annotation, key/value pair format + Annotations map[string]string `json:"annotations"` +} + +type Status struct { + //The state of the network + State string `json:"state"` +} + +type commonResult struct { + golangsdk.Result +} + +// Extract is a function that accepts a result and extracts a network. +func (r commonResult) Extract() (*Network, error) { + var s Network + err := r.ExtractInto(&s) + return &s, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Network. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Network. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} diff --git a/vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/urls.go b/vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/urls.go new file mode 100644 index 0000000000..e0a7ab4116 --- /dev/null +++ b/vendor/github.com/huaweicloud/golangsdk/openstack/cci/v1/networks/urls.go @@ -0,0 +1,11 @@ +package networks + +import "github.com/huaweicloud/golangsdk" + +func rootURL(client *golangsdk.ServiceClient, ns string) string { + return client.ServiceURL("namespaces", ns, "networks") +} + +func resourceURL(client *golangsdk.ServiceClient, ns, name string) string { + return client.ServiceURL("namespaces", ns, "networks", name) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 29f0f69931..35bb813b74 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -191,6 +191,7 @@ github.com/huaweicloud/golangsdk/openstack/blockstorage/extensions/volumeactions github.com/huaweicloud/golangsdk/openstack/blockstorage/v2/volumes github.com/huaweicloud/golangsdk/openstack/cce/v3/clusters github.com/huaweicloud/golangsdk/openstack/cce/v3/nodes +github.com/huaweicloud/golangsdk/openstack/cci/v1/networks github.com/huaweicloud/golangsdk/openstack/cdn/v1/domains github.com/huaweicloud/golangsdk/openstack/cloudeyeservice/alarmrule github.com/huaweicloud/golangsdk/openstack/compute/v2/extensions/attachinterfaces diff --git a/website/docs/r/cci_network_v1.html.md b/website/docs/r/cci_network_v1.html.md new file mode 100644 index 0000000000..eeb91e6ecd --- /dev/null +++ b/website/docs/r/cci_network_v1.html.md @@ -0,0 +1,68 @@ +--- +layout: "huaweicloud" +page_title: "Huaweicloud: huaweicloud_cci_network_v1" +sidebar_current: "docs-huaweicloud-resource-cci-network-v1" +description: |- + Provides Cloud Container Instance(CCI) resource. +--- + +# huaweicloud_cci_network_v1 + +Provides a CCI resource. + +## Example Usage + + ```hcl + variable "sg_id" { } + variable "project_id" { } + variable "domain_id" { } + variable "vpc_id" { } + variable "net_id" { } + variable "subnet_id" { } + + resource "huaweicloud_cci_network_v1" "net_1" { + name = "cci-net" + namespace = "test-ns" + security_group = var.sg_id + project_id = var.project_id + domain_id = var.domain_id + vpc_id = var.vpc_id + network_id = var.net_id + subnet_id = var.subnet_id + available_zone = "cn-north-1a" + cidr = "192.168.0.0/24" + } +``` + +## Argument Reference + +The following arguments are supported: + + +* `name` - (Required) CCI Network name. Changing this parameter will create a new resource. + +* `namespace` - (Required) CCI Network namespace. Changing this parameter will create a new resource. + +* `security_group` - (Required) ID of the security group to which the subnet of the network belongs. Changing this parameter will create a new resource. + +* `project_id` - (Required) Project ID of the tenant. Changing this parameter will create a new resource. + +* `domain_id` - (Required) Domain ID of the tenant. Changing this parameter will create a new resource. + +* `vpc_id` - (Required) ID of the VPC to which the network belongs. Changing this parameter will create a new resource. + +* `network_id` - (Required) Network ID of the VPC subnet in which the network belongs. Changing this parameter will create a new resource. + +* `subnet_id` - (Required) ID of the VPC subnet to which the network belongs. Changing this parameter will create a new resource. + +* `available_zone` - (Required) AZ to which the VPC subnet of the network belongs. Changing this parameter will create a new resource. + +* `cidr` - (Required) Network segment of the VPC subnet to which the network belongs. Changing this parameter will create a new resource. + + +## Attributes Reference + +All above argument parameters can be exported as attribute parameters along with attribute reference. + + * `id` - Id of the instance resource. + diff --git a/website/huaweicloud.erb b/website/huaweicloud.erb index 76624f220c..76bf1e8c4e 100644 --- a/website/huaweicloud.erb +++ b/website/huaweicloud.erb @@ -89,6 +89,20 @@ +
  • + Cloud Container Instance (CCI) + +
  • +
  • Cloud Data Migration (CDM)