This is an automated email from the ASF dual-hosted git repository.
sureshanaparti pushed a commit to branch main
in repository
https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git
The following commit(s) were added to refs/heads/main by this push:
new d89fc4f SystemVM offering declaration (#327)
d89fc4f is described below
commit d89fc4f61d0a6ea44ea100034eba1d0239e7ac19
Author: bddvlpr <[email protected]>
AuthorDate: Mon Sep 7 08:38:40 2026 +0200
SystemVM offering declaration (#327)
* Add support for SystemVM offerings
* Add network rate <-> domainrouter and ha <-> local storage acc tests
---
cloudstack/provider.go | 1 +
cloudstack/resource_cloudstack_nic_test.go | 99 ++++++
.../resource_cloudstack_system_service_offering.go | 352 +++++++++++++++++++++
...urce_cloudstack_system_service_offering_test.go | 215 +++++++++++++
website/docs/README.md | 1 +
.../docs/r/system_service_offering.html.markdown | 83 +++++
6 files changed, 751 insertions(+)
diff --git a/cloudstack/provider.go b/cloudstack/provider.go
index d268867..f4d3a4a 100644
--- a/cloudstack/provider.go
+++ b/cloudstack/provider.go
@@ -164,6 +164,7 @@ func Provider() *schema.Provider {
"cloudstack_volume":
resourceCloudStackVolume(),
"cloudstack_zone":
resourceCloudStackZone(),
"cloudstack_service_offering":
resourceCloudStackServiceOffering(),
+ "cloudstack_system_service_offering":
resourceCloudStackSystemServiceOffering(),
"cloudstack_account":
resourceCloudStackAccount(),
"cloudstack_project":
resourceCloudStackProject(),
"cloudstack_user":
resourceCloudStackUser(),
diff --git a/cloudstack/resource_cloudstack_nic_test.go
b/cloudstack/resource_cloudstack_nic_test.go
index d82d542..76cf36c 100644
--- a/cloudstack/resource_cloudstack_nic_test.go
+++ b/cloudstack/resource_cloudstack_nic_test.go
@@ -79,6 +79,69 @@ func TestAccCloudStackNIC_update(t *testing.T) {
})
}
+func TestAccCloudStackNIC_projectRefresh(t *testing.T) {
+ const resourceName = "cloudstack_nic.foo"
+ var nicID string
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ Providers: testAccProviders,
+ CheckDestroy: testAccCheckCloudStackNICDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccCloudStackNIC_project,
+ Check: resource.ComposeTestCheckFunc(
+
testAccCaptureCloudStackNICID(resourceName, &nicID),
+
resource.TestCheckResourceAttr("cloudstack_instance.foobar", "project",
"terraform"),
+
resource.TestCheckResourceAttrSet(resourceName, "ip_address"),
+
resource.TestCheckResourceAttr(resourceName, "ip_address", "10.1.2.123"),
+ ),
+ },
+ {
+ RefreshState: true,
+ Check: resource.ComposeTestCheckFunc(
+
testAccCheckCloudStackNICID(resourceName, &nicID),
+
resource.TestCheckResourceAttrSet(resourceName, "ip_address"),
+
resource.TestCheckResourceAttr(resourceName, "ip_address", "10.1.2.123"),
+ ),
+ },
+ {
+ Config: testAccCloudStackNIC_project,
+ PlanOnly: true,
+ },
+ },
+ })
+}
+
+func testAccCaptureCloudStackNICID(n string, nicID *string)
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 NIC ID is set")
+ }
+
+ *nicID = rs.Primary.ID
+ return nil
+ }
+}
+
+func testAccCheckCloudStackNICID(n string, nicID *string)
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 != *nicID {
+ return fmt.Errorf("NIC ID changed from %s to %s during
refresh", *nicID, rs.Primary.ID)
+ }
+
+ return nil
+ }
+}
+
func TestAccCloudStackNIC_macaddress(t *testing.T) {
var nic cloudstack.Nic
@@ -271,6 +334,42 @@ resource "cloudstack_nic" "foo" {
ip_address = "10.1.2.123"
}`
+const testAccCloudStackNIC_project = `
+resource "cloudstack_network" "foo" {
+ name = "terraform-project-network-primary"
+ display_text = "terraform-project-network-primary"
+ cidr = "10.1.1.0/24"
+ network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService"
+ project = "terraform"
+ zone = "Sandbox-simulator"
+}
+
+resource "cloudstack_network" "bar" {
+ name = "terraform-project-network-secondary"
+ display_text = "terraform-project-network-secondary"
+ cidr = "10.1.2.0/24"
+ network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService"
+ project = "terraform"
+ zone = "Sandbox-simulator"
+}
+
+resource "cloudstack_instance" "foobar" {
+ name = "terraform-project-nic-test"
+ display_name = "terraform-project-nic-test"
+ service_offering = "Medium Instance"
+ network_id = cloudstack_network.foo.id
+ template = "CentOS 5.6 (64-bit) no GUI (Simulator)"
+ project = "terraform"
+ zone = "Sandbox-simulator"
+ expunge = true
+}
+
+resource "cloudstack_nic" "foo" {
+ network_id = cloudstack_network.bar.id
+ virtual_machine_id = cloudstack_instance.foobar.id
+ ip_address = "10.1.2.123"
+}`
+
const testAccCloudStackNIC_macaddress = `
resource "cloudstack_network" "foo" {
name = "terraform-network-primary"
diff --git a/cloudstack/resource_cloudstack_system_service_offering.go
b/cloudstack/resource_cloudstack_system_service_offering.go
new file mode 100644
index 0000000..d2d3e94
--- /dev/null
+++ b/cloudstack/resource_cloudstack_system_service_offering.go
@@ -0,0 +1,352 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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 cloudstack
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "strings"
+
+ "github.com/apache/cloudstack-go/v2/cloudstack"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
+)
+
+func resourceCloudStackSystemServiceOffering() *schema.Resource {
+ return &schema.Resource{
+ Create: resourceCloudStackSystemServiceOfferingCreate,
+ Read: resourceCloudStackSystemServiceOfferingRead,
+ Update: resourceCloudStackSystemServiceOfferingUpdate,
+ Delete: resourceCloudStackSystemServiceOfferingDelete,
+ Importer: &schema.ResourceImporter{
+ State: schema.ImportStatePassthrough,
+ },
+ CustomizeDiff:
resourceCloudStackSystemServiceOfferingCustomizeDiff,
+ Schema: map[string]*schema.Schema{
+ "name": {
+ Type: schema.TypeString,
+ Required: true,
+ ValidateFunc: validation.StringIsNotEmpty,
+ },
+ "display_text": {
+ Type: schema.TypeString,
+ Required: true,
+ ValidateFunc: validation.StringIsNotEmpty,
+ },
+ "system_vm_type": {
+ Description: "The system VM type that uses this
offering",
+ Type: schema.TypeString,
+ Required: true,
+ ForceNew: true,
+ StateFunc: func(v interface{}) string {
+ return strings.ToLower(v.(string))
+ },
+ ValidateFunc: validation.StringInSlice([]string{
+ "domainrouter",
+ "consoleproxy",
+ "secondarystoragevm",
+ }, true),
+ },
+ "cpu_number": {
+ Description: "Number of CPU cores",
+ Type: schema.TypeInt,
+ Required: true,
+ ForceNew: true,
+ ValidateFunc: validation.IntAtLeast(1),
+ },
+ "cpu_speed": {
+ Description: "Speed of each CPU core in MHz",
+ Type: schema.TypeInt,
+ Required: true,
+ ForceNew: true,
+ ValidateFunc: validation.IntAtLeast(0),
+ },
+ "memory": {
+ Description: "Memory reserved by the system VM
in MB",
+ Type: schema.TypeInt,
+ Required: true,
+ ForceNew: true,
+ ValidateFunc: validation.IntAtLeast(32),
+ },
+ "storage_type": {
+ Description: "The storage type of the offering",
+ Type: schema.TypeString,
+ Optional: true,
+ ForceNew: true,
+ Default: "shared",
+ StateFunc: func(v interface{}) string {
+ return strings.ToLower(v.(string))
+ },
+ ValidateFunc:
validation.StringInSlice([]string{"local", "shared"}, true),
+ },
+ "network_rate": {
+ Description: "Network rate in Mbps; valid only
for domain router offerings",
+ Type: schema.TypeInt,
+ Optional: true,
+ ForceNew: true,
+ ValidateFunc: validation.IntAtLeast(1),
+ },
+ "offer_ha": {
+ Description: "Whether the system offering
supports HA",
+ Type: schema.TypeBool,
+ Optional: true,
+ ForceNew: true,
+ Default: false,
+ },
+ "limit_cpu_use": {
+ Description: "Whether CPU usage is limited to
the offering's committed resources",
+ Type: schema.TypeBool,
+ Optional: true,
+ ForceNew: true,
+ Default: false,
+ },
+ "host_tags": {
+ Description: "Host tags associated with the
offering",
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "storage_tags": {
+ Description: "Storage tags associated with the
offering",
+ Type: schema.TypeString,
+ Optional: true,
+ },
+ "domain_ids": {
+ Description: "IDs of the domains that can use
the offering; omit for a public offering",
+ Type: schema.TypeSet,
+ Optional: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ ValidateFunc:
validation.StringIsNotEmpty,
+ },
+ Set: schema.HashString,
+ },
+ },
+ }
+}
+
+func resourceCloudStackSystemServiceOfferingCustomizeDiff(_ context.Context, d
*schema.ResourceDiff, _ interface{}) error {
+ _, hasNetworkRate := d.GetOk("network_rate")
+ return validateSystemServiceOfferingConfiguration(
+ d.Get("system_vm_type").(string),
+ hasNetworkRate,
+ d.Get("storage_type").(string),
+ d.Get("offer_ha").(bool),
+ )
+}
+
+func validateSystemServiceOfferingConfiguration(systemVMType string,
hasNetworkRate bool, storageType string, offerHA bool) error {
+ if hasNetworkRate && !strings.EqualFold(systemVMType, "domainrouter") {
+ return fmt.Errorf("network_rate can only be set when
system_vm_type is domainrouter")
+ }
+
+ if offerHA && strings.EqualFold(storageType, "local") {
+ return fmt.Errorf("offer_ha cannot be enabled when storage_type
is local")
+ }
+
+ return nil
+}
+
+func resourceCloudStackSystemServiceOfferingCreate(d *schema.ResourceData,
meta interface{}) error {
+ cs := meta.(*cloudstack.CloudStackClient)
+ name := d.Get("name").(string)
+ p :=
cs.ServiceOffering.NewCreateServiceOfferingParams(d.Get("display_text").(string),
name)
+
+ p.SetIssystem(true)
+ p.SetSystemvmtype(d.Get("system_vm_type").(string))
+ p.SetCpunumber(d.Get("cpu_number").(int))
+ p.SetCpuspeed(d.Get("cpu_speed").(int))
+ p.SetMemory(d.Get("memory").(int))
+ p.SetCustomized(false)
+ p.SetStoragetype(d.Get("storage_type").(string))
+ p.SetOfferha(d.Get("offer_ha").(bool))
+ p.SetLimitcpuuse(d.Get("limit_cpu_use").(bool))
+
+ if v, ok := d.GetOk("network_rate"); ok {
+ p.SetNetworkrate(v.(int))
+ }
+ if v, ok := d.GetOk("host_tags"); ok {
+ p.SetHosttags(v.(string))
+ }
+ if v, ok := d.GetOk("storage_tags"); ok {
+ p.SetTags(v.(string))
+ }
+ if domainIDs :=
expandSystemServiceOfferingDomainIDs(d.Get("domain_ids")); len(domainIDs) > 0 {
+ p.SetDomainid(domainIDs)
+ }
+
+ log.Printf("[DEBUG] Creating System Service Offering %s", name)
+ offering, err := cs.ServiceOffering.CreateServiceOffering(p)
+ if err != nil {
+ return fmt.Errorf("error creating System Service Offering %s:
%s", name, err)
+ }
+
+ d.SetId(offering.Id)
+ log.Printf("[DEBUG] System Service Offering %s successfully created",
name)
+
+ return resourceCloudStackSystemServiceOfferingRead(d, meta)
+}
+
+func resourceCloudStackSystemServiceOfferingRead(d *schema.ResourceData, meta
interface{}) error {
+ cs := meta.(*cloudstack.CloudStackClient)
+ log.Printf("[DEBUG] Retrieving System Service Offering %s", d.Id())
+
+ offering, count, err := getSystemServiceOfferingByID(cs, d.Id())
+ if err != nil {
+ return err
+ }
+ if count == 0 {
+ log.Printf("[DEBUG] System Service Offering %s no longer
exists", d.Id())
+ d.SetId("")
+ return nil
+ }
+
+ fields := map[string]interface{}{
+ "name": offering.Name,
+ "display_text": offering.Displaytext,
+ "system_vm_type": strings.ToLower(offering.Systemvmtype),
+ "cpu_number": offering.Cpunumber,
+ "cpu_speed": offering.Cpuspeed,
+ "memory": offering.Memory,
+ "storage_type": strings.ToLower(offering.Storagetype),
+ "network_rate": offering.Networkrate,
+ "offer_ha": offering.Offerha,
+ "limit_cpu_use": offering.Limitcpuuse,
+ "host_tags": offering.Hosttags,
+ "storage_tags": offering.Storagetags,
+ "domain_ids":
flattenSystemServiceOfferingDomainIDs(offering.Domainid),
+ }
+
+ for key, value := range fields {
+ if err := d.Set(key, value); err != nil {
+ return fmt.Errorf("error setting %s for System Service
Offering %s: %s", key, d.Id(), err)
+ }
+ }
+
+ return nil
+}
+
+func resourceCloudStackSystemServiceOfferingUpdate(d *schema.ResourceData,
meta interface{}) error {
+ cs := meta.(*cloudstack.CloudStackClient)
+ p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id())
+ hasChanges := false
+
+ if d.HasChange("name") {
+ p.SetName(d.Get("name").(string))
+ hasChanges = true
+ }
+ if d.HasChange("display_text") {
+ p.SetDisplaytext(d.Get("display_text").(string))
+ hasChanges = true
+ }
+ if d.HasChange("host_tags") {
+ p.SetHosttags(d.Get("host_tags").(string))
+ hasChanges = true
+ }
+ if d.HasChange("storage_tags") {
+ p.SetStoragetags(d.Get("storage_tags").(string))
+ hasChanges = true
+ }
+ if d.HasChange("domain_ids") {
+ domainIDs :=
expandSystemServiceOfferingDomainIDs(d.Get("domain_ids"))
+ if len(domainIDs) == 0 {
+ p.SetDomainid("public")
+ } else {
+ p.SetDomainid(strings.Join(domainIDs, ","))
+ }
+ hasChanges = true
+ }
+
+ if hasChanges {
+ log.Printf("[DEBUG] Updating System Service Offering %s",
d.Id())
+ if _, err := cs.ServiceOffering.UpdateServiceOffering(p); err
!= nil {
+ return fmt.Errorf("error updating System Service
Offering %s: %s", d.Id(), err)
+ }
+ }
+
+ return resourceCloudStackSystemServiceOfferingRead(d, meta)
+}
+
+func resourceCloudStackSystemServiceOfferingDelete(d *schema.ResourceData,
meta interface{}) error {
+ cs := meta.(*cloudstack.CloudStackClient)
+ p := cs.ServiceOffering.NewDeleteServiceOfferingParams(d.Id())
+
+ log.Printf("[DEBUG] Deleting System Service Offering %s", d.Id())
+ if _, err := cs.ServiceOffering.DeleteServiceOffering(p); err != nil {
+ return fmt.Errorf("error deleting System Service Offering %s:
%s", d.Id(), err)
+ }
+
+ d.SetId("")
+ return nil
+}
+
+func getSystemServiceOfferingByID(cs *cloudstack.CloudStackClient, id string)
(*cloudstack.ServiceOffering, int, error) {
+ p := cs.ServiceOffering.NewListServiceOfferingsParams()
+ p.SetId(id)
+ p.SetIssystem(true)
+
+ response, err := cs.ServiceOffering.ListServiceOfferings(p)
+ if err != nil {
+ return nil, -1, fmt.Errorf("error retrieving System Service
Offering %s: %s", id, err)
+ }
+ if response.Count == 0 {
+ return nil, 0, nil
+ }
+ if response.Count != 1 {
+ return nil, response.Count, fmt.Errorf("expected one System
Service Offering for ID %s, got %d", id, response.Count)
+ }
+
+ offering := response.ServiceOfferings[0]
+ if !offering.Issystem {
+ return nil, 1, fmt.Errorf("service offering %s is not a system
offering", id)
+ }
+
+ return offering, 1, nil
+}
+
+func expandSystemServiceOfferingDomainIDs(value interface{}) []string {
+ set, ok := value.(*schema.Set)
+ if !ok || set.Len() == 0 {
+ return nil
+ }
+
+ domainIDs := make([]string, 0, set.Len())
+ for _, value := range set.List() {
+ domainIDs = append(domainIDs, value.(string))
+ }
+
+ return domainIDs
+}
+
+func flattenSystemServiceOfferingDomainIDs(domainID string) []string {
+ if domainID == "" || strings.EqualFold(domainID, "public") {
+ return nil
+ }
+
+ values := strings.Split(domainID, ",")
+ domainIDs := make([]string, 0, len(values))
+ for _, value := range values {
+ if value = strings.TrimSpace(value); value != "" {
+ domainIDs = append(domainIDs, value)
+ }
+ }
+
+ return domainIDs
+}
diff --git a/cloudstack/resource_cloudstack_system_service_offering_test.go
b/cloudstack/resource_cloudstack_system_service_offering_test.go
new file mode 100644
index 0000000..37ec43a
--- /dev/null
+++ b/cloudstack/resource_cloudstack_system_service_offering_test.go
@@ -0,0 +1,215 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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 cloudstack
+
+import (
+ "fmt"
+ "regexp"
+ "testing"
+
+ "github.com/apache/cloudstack-go/v2/cloudstack"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+func TestAccSystemServiceOffering(t *testing.T) {
+ var offering cloudstack.ServiceOffering
+ var originalID string
+ const resourceName = "cloudstack_system_service_offering.test"
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ Providers: testAccProviders,
+ CheckDestroy:
testAccCheckCloudStackSystemServiceOfferingDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config:
testAccCloudStackSystemServiceOfferingConfig("terraform-system-offering",
"Terraform System Offering", 1, "compute", "primary"),
+ Check: resource.ComposeTestCheckFunc(
+
testAccCheckCloudStackSystemServiceOfferingExists(resourceName, &offering),
+
testAccCaptureCloudStackSystemServiceOfferingID(resourceName, &originalID),
+
resource.TestCheckResourceAttr(resourceName, "system_vm_type", "domainrouter"),
+
resource.TestCheckResourceAttr(resourceName, "cpu_number", "1"),
+
resource.TestCheckResourceAttr(resourceName, "cpu_speed", "500"),
+
resource.TestCheckResourceAttr(resourceName, "memory", "256"),
+
resource.TestCheckResourceAttr(resourceName, "storage_type", "shared"),
+
resource.TestCheckResourceAttr(resourceName, "network_rate", "100"),
+
resource.TestCheckResourceAttr(resourceName, "offer_ha", "true"),
+
resource.TestCheckResourceAttr(resourceName, "limit_cpu_use", "true"),
+ ),
+ },
+ {
+ Config:
testAccCloudStackSystemServiceOfferingConfig("terraform-system-offering-updated",
"Terraform System Offering Updated", 1, "compute-updated", "secondary"),
+ Check: resource.ComposeTestCheckFunc(
+
testAccCheckCloudStackSystemServiceOfferingExists(resourceName, &offering),
+
testAccCheckCloudStackSystemServiceOfferingID(resourceName, &originalID, false),
+
resource.TestCheckResourceAttr(resourceName, "name",
"terraform-system-offering-updated"),
+
resource.TestCheckResourceAttr(resourceName, "display_text", "Terraform System
Offering Updated"),
+
resource.TestCheckResourceAttr(resourceName, "host_tags", "compute-updated"),
+
resource.TestCheckResourceAttr(resourceName, "storage_tags", "secondary"),
+ ),
+ },
+ {
+ ResourceName: resourceName,
+ ImportState: true,
+ ImportStateVerify: true,
+ },
+ {
+ Config:
testAccCloudStackSystemServiceOfferingConfig("terraform-system-offering-updated",
"Terraform System Offering Updated", 2, "compute-updated", "secondary"),
+ Check: resource.ComposeTestCheckFunc(
+
testAccCheckCloudStackSystemServiceOfferingExists(resourceName, &offering),
+
testAccCheckCloudStackSystemServiceOfferingID(resourceName, &originalID, true),
+
resource.TestCheckResourceAttr(resourceName, "cpu_number", "2"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccSystemServiceOfferingValidation(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ Providers: testAccProviders,
+ Steps: []resource.TestStep{
+ {
+ Config:
testAccCloudStackSystemServiceOfferingInvalidNetworkRate,
+ ExpectError: regexp.MustCompile("network_rate
can only be set when system_vm_type is domainrouter"),
+ },
+ {
+ Config:
testAccCloudStackSystemServiceOfferingInvalidHA,
+ ExpectError: regexp.MustCompile("offer_ha
cannot be enabled when storage_type is local"),
+ },
+ },
+ })
+}
+
+const testAccCloudStackSystemServiceOfferingInvalidNetworkRate = `
+resource "cloudstack_system_service_offering" "invalid" {
+ name = "terraform-invalid-system-offering"
+ display_text = "Terraform Invalid System Offering"
+ system_vm_type = "consoleproxy"
+ cpu_number = 1
+ cpu_speed = 500
+ memory = 256
+ network_rate = 100
+}
+`
+
+const testAccCloudStackSystemServiceOfferingInvalidHA = `
+resource "cloudstack_system_service_offering" "invalid" {
+ name = "terraform-invalid-system-offering"
+ display_text = "Terraform Invalid System Offering"
+ system_vm_type = "domainrouter"
+ cpu_number = 1
+ cpu_speed = 500
+ memory = 256
+ storage_type = "local"
+ offer_ha = true
+}
+`
+
+func testAccCloudStackSystemServiceOfferingConfig(name, displayText string,
cpuNumber int, hostTags, storageTags string) string {
+ return fmt.Sprintf(`
+resource "cloudstack_system_service_offering" "test" {
+ name = %q
+ display_text = %q
+ system_vm_type = "domainrouter"
+ cpu_number = %d
+ cpu_speed = 500
+ memory = 256
+ network_rate = 100
+ offer_ha = true
+ limit_cpu_use = true
+ host_tags = %q
+ storage_tags = %q
+}
+`, name, displayText, cpuNumber, hostTags, storageTags)
+}
+
+func testAccCheckCloudStackSystemServiceOfferingExists(name string, offering
*cloudstack.ServiceOffering) resource.TestCheckFunc {
+ return func(state *terraform.State) error {
+ resourceState, ok := state.RootModule().Resources[name]
+ if !ok {
+ return fmt.Errorf("not found: %s", name)
+ }
+ if resourceState.Primary.ID == "" {
+ return fmt.Errorf("no System Service Offering ID is
set")
+ }
+
+ cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)
+ result, count, err := getSystemServiceOfferingByID(cs,
resourceState.Primary.ID)
+ if err != nil {
+ return err
+ }
+ if count != 1 {
+ return fmt.Errorf("System Service Offering %s not
found", resourceState.Primary.ID)
+ }
+
+ *offering = *result
+ return nil
+ }
+}
+
+func testAccCaptureCloudStackSystemServiceOfferingID(name string, id *string)
resource.TestCheckFunc {
+ return func(state *terraform.State) error {
+ resourceState, ok := state.RootModule().Resources[name]
+ if !ok || resourceState.Primary.ID == "" {
+ return fmt.Errorf("no System Service Offering ID is
set")
+ }
+
+ *id = resourceState.Primary.ID
+ return nil
+ }
+}
+
+func testAccCheckCloudStackSystemServiceOfferingID(name string, originalID
*string, expectChanged bool) resource.TestCheckFunc {
+ return func(state *terraform.State) error {
+ resourceState, ok := state.RootModule().Resources[name]
+ if !ok || resourceState.Primary.ID == "" {
+ return fmt.Errorf("no System Service Offering ID is
set")
+ }
+
+ changed := resourceState.Primary.ID != *originalID
+ if changed != expectChanged {
+ return fmt.Errorf("unexpected System Service Offering
ID change: original=%s current=%s", *originalID, resourceState.Primary.ID)
+ }
+
+ return nil
+ }
+}
+
+func testAccCheckCloudStackSystemServiceOfferingDestroy(state
*terraform.State) error {
+ cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)
+
+ for _, resourceState := range state.RootModule().Resources {
+ if resourceState.Type != "cloudstack_system_service_offering" {
+ continue
+ }
+
+ _, count, err := getSystemServiceOfferingByID(cs,
resourceState.Primary.ID)
+ if err != nil {
+ return err
+ }
+ if count != 0 {
+ return fmt.Errorf("System Service Offering %s still
exists", resourceState.Primary.ID)
+ }
+ }
+
+ return nil
+}
diff --git a/website/docs/README.md b/website/docs/README.md
index c99234a..c664185 100644
--- a/website/docs/README.md
+++ b/website/docs/README.md
@@ -98,6 +98,7 @@ The following arguments are supported:
- [security_group](./r/security_group.html.markdown)
- [security_group_rule](./r/security_group_rule.html.markdown)
- [service_offering](./r/service_offering.html.markdown)
+- [system_service_offering](./r/system_service_offering.html.markdown)
- [ssh_keypair](./r/ssh_keypair.html.markdown)
- [static_nat](./r/static_nat.html.markdown)
- [static_route](./r/static_route.html.markdown)
diff --git a/website/docs/r/system_service_offering.html.markdown
b/website/docs/r/system_service_offering.html.markdown
new file mode 100644
index 0000000..be71b5f
--- /dev/null
+++ b/website/docs/r/system_service_offering.html.markdown
@@ -0,0 +1,83 @@
+---
+layout: default
+title: "CloudStack: cloudstack_system_service_offering"
+sidebar_current: "docs-cloudstack-resource-system-service-offering"
+description: |-
+ Creates a System Service Offering
+---
+
+# CloudStack: cloudstack_system_service_offering
+
+A `cloudstack_system_service_offering` resource manages a service offering for
+CloudStack system VMs.
+
+## Example Usage
+
+```hcl
+resource "cloudstack_system_service_offering" "router" {
+ name = "redundant-router-offering"
+ display_text = "Redundant router offering"
+ system_vm_type = "domainrouter"
+ cpu_number = 2
+ cpu_speed = 1000
+ memory = 2048
+ network_rate = 200
+ offer_ha = true
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `name` - (Required) Name of the system service offering.
+
+* `display_text` - (Required) Display text of the system service offering.
+
+* `system_vm_type` - (Required) Type of system VM that uses the offering. Valid
+ values are `domainrouter`, `consoleproxy`, and `secondarystoragevm`. Changing
+ this forces a new resource to be created.
+
+* `cpu_number` - (Required) Number of CPU cores. Changing this forces a new
+ resource to be created.
+
+* `cpu_speed` - (Required) Speed of each CPU core in MHz. Changing this forces
+ a new resource to be created.
+
+* `memory` - (Required) Memory reserved by the system VM in MB. Changing this
+ forces a new resource to be created.
+
+* `storage_type` - (Optional) Storage type of the offering. Valid values are
+ `local` and `shared`. Defaults to `shared`. Changing this forces a new
resource
+ to be created. `local` storage cannot be combined with `offer_ha = true`.
+
+* `network_rate` - (Optional) Network rate in Mbps. This can only be set for a
+ `domainrouter` offering. Changing this forces a new resource to be created.
+
+* `offer_ha` - (Optional) Whether the offering supports HA. Defaults to
`false`.
+ Changing this forces a new resource to be created.
+
+* `limit_cpu_use` - (Optional) Whether CPU usage is limited to the offering's
+ committed resources. Defaults to `false`. Changing this forces a new resource
+ to be created.
+
+* `host_tags` - (Optional) Host tags associated with the offering.
+
+* `storage_tags` - (Optional) Storage tags associated with the offering.
+
+* `domain_ids` - (Optional) Set of domain IDs that can use the offering. Omit
+ this argument to make the offering public.
+
+## Attributes Reference
+
+The following attributes are exported:
+
+* `id` - The ID of the system service offering.
+
+## Import
+
+System service offerings can be imported using their ID. For example:
+
+```shell
+$ terraform import cloudstack_system_service_offering.router
<SYSTEMSERVICEOFFERINGID>
+```