diff --git a/.github/workflows/testacc.yml b/.github/workflows/testacc.yml new file mode 100644 index 00000000..fa364f81 --- /dev/null +++ b/.github/workflows/testacc.yml @@ -0,0 +1,88 @@ +# 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. + +name: Acceptance Test + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-testacc + cancel-in-progress: true + +jobs: + testacc: + name: Acceptance Test + runs-on: ubuntu-22.04 + env: + CLOUDSTACK_API_URL: http://localhost:8080/client/api + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19.x + - name: Wait Cloudstack to be ready + run: | + echo "Starting Cloudstack health check" + T=0 + until [ $T -gt 20 ] || curl -sfL http://localhost:8080 --output /dev/null + do + echo "Waiting for Cloudstack to be ready..." + ((T+=1)) + sleep 30 + done + - name: Setting up Cloudstack + run: | + docker exec $(docker container ls --format=json -l | jq -r .ID) python /root/tools/marvin/marvin/deployDataCenter.py -i /root/setup/dev/advanced.cfg + curl -sf --location "${CLOUDSTACK_API_URL}" \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'command=login' \ + --data-urlencode 'username=admin' \ + --data-urlencode 'password=password' \ + --data-urlencode 'response=json' \ + --data-urlencode 'domain=/' -j -c cookies.txt --output /dev/null + + CLOUDSTACK_USER_ID=$(curl -fs "${CLOUDSTACK_API_URL}?command=listUsers&response=json" -b cookies.txt | jq -r '.listusersresponse.user[0].id') + CLOUDSTACK_API_KEY=$(curl -s "${CLOUDSTACK_API_URL}?command=getUserKeys&id=${CLOUDSTACK_USER_ID}&response=json" -b cookies.txt | jq -r '.getuserkeysresponse.userkeys.apikey') + CLOUDSTACK_SECRET_KEY=$(curl -fs "${CLOUDSTACK_API_URL}?command=getUserKeys&id=${CLOUDSTACK_USER_ID}&response=json" -b cookies.txt | jq -r '.getuserkeysresponse.userkeys.secretkey') + + echo "::add-mask::$CLOUDSTACK_API_KEY" + echo "::add-mask::$CLOUDSTACK_SECRET_KEY" + + echo "CLOUDSTACK_API_KEY=$CLOUDSTACK_API_KEY" >> $GITHUB_ENV + echo "CLOUDSTACK_SECRET_KEY=$CLOUDSTACK_SECRET_KEY" >> $GITHUB_ENV + - name: Install CMK + run: | + curl -sfL https://github.com/apache/cloudstack-cloudmonkey/releases/download/6.3.0/cmk.linux.x86-64 -o /usr/local/bin/cmk + chmod +x /usr/local/bin/cmk + - name: Create extra resources + run: | + cmk -u $CLOUDSTACK_API_URL -k $CLOUDSTACK_API_KEY -s $CLOUDSTACK_SECRET_KEY -o json create project name=terraform displaytext=terraform + - name: Run acceptance test + run: | + make testacc + services: + cloudstack-simulator: + image: apache/cloudstack-simulator:${{ matrix.cloudstack_version }} + ports: + - 8080:5050 + strategy: + matrix: + cloudstack_version: + - 4.17.2.0 + - 4.18.1.0 + - 4.19.0.0 diff --git a/README.md b/README.md index 57ac409b..0b929bbc 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ When Docker started the container you can go to http://localhost:8080/client and Once the login page is shown and you can login, you need to provision a simulated data-center: ```sh -$ docker exec -ti cloudstack python /root/tools/marvin/marvin/deployDataCenter.py -i /root/setup/dev/advanced.cfg +docker exec -it cloudstack-simulator python /root/tools/marvin/marvin/deployDataCenter.py -i /root/setup/dev/advanced.cfg ``` If you refresh the client or login again, you will now get passed the initial welcome screen and be able to go to your account details and retrieve the API key and secret. Export those together with the URL: diff --git a/cloudstack/resource_cloudstack_service_offering.go b/cloudstack/resource_cloudstack_service_offering.go index ad887b13..25dad334 100644 --- a/cloudstack/resource_cloudstack_service_offering.go +++ b/cloudstack/resource_cloudstack_service_offering.go @@ -42,6 +42,61 @@ func resourceCloudStackServiceOffering() *schema.Resource { Type: schema.TypeString, Required: true, }, + "cpu_number": { + Description: "Number of CPU cores", + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "cpu_speed": { + Description: "Speed of CPU in Mhz", + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "host_tags": { + Description: "The host tag for this service offering", + Type: schema.TypeString, + Optional: true, + }, + "limit_cpu_use": { + Description: "Restrict the CPU usage to committed service offering", + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: false, + }, + "memory": { + Description: "The total memory of the service offering in MB", + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + "offer_ha": { + Description: "The HA for the service offering", + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + Default: false, + }, + "storage_type": { + Description: "The storage type of the service offering. Values are local and shared", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "shared", + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + + if v == "local" || v == "shared" { + return + } + + errs = append(errs, fmt.Errorf("storage type should be either local or shared, got %s", v)) + + return + }, + }, }, } } @@ -53,6 +108,33 @@ func resourceCloudStackServiceOfferingCreate(d *schema.ResourceData, meta interf // Create a new parameter struct p := cs.ServiceOffering.NewCreateServiceOfferingParams(display_text, name) + if v, ok := d.GetOk("cpu_number"); ok { + p.SetCpunumber(v.(int)) + } + + if v, ok := d.GetOk("cpu_speed"); ok { + p.SetCpuspeed(v.(int)) + } + + if v, ok := d.GetOk("host_tags"); ok { + p.SetHosttags(v.(string)) + } + + if v, ok := d.GetOk("limit_cpu_use"); ok { + p.SetLimitcpuuse(v.(bool)) + } + + if v, ok := d.GetOk("memory"); ok { + p.SetMemory(v.(int)) + } + + if v, ok := d.GetOk("offer_ha"); ok { + p.SetOfferha(v.(bool)) + } + + if v, ok := d.GetOk("storage_type"); ok { + p.SetStoragetype(v.(string)) + } log.Printf("[DEBUG] Creating Service Offering %s", name) s, err := cs.ServiceOffering.CreateServiceOffering(p) @@ -84,8 +166,22 @@ func resourceCloudStackServiceOfferingRead(d *schema.ResourceData, meta interfac } d.SetId(s.Id) - d.Set("name", s.Name) - d.Set("display_text", s.Displaytext) + + fields := map[string]interface{}{ + "name": s.Name, + "display_text": s.Displaytext, + "cpu_number": s.Cpunumber, + "cpu_speed": s.Cpuspeed, + "host_tags": s.Hosttags, + "limit_cpu_use": s.Limitcpuuse, + "memory": s.Memory, + "offer_ha": s.Offerha, + "storage_type": s.Storagetype, + } + + for k, v := range fields { + d.Set(k, v) + } return nil } @@ -136,6 +232,25 @@ func resourceCloudStackServiceOfferingUpdate(d *schema.ResourceData, meta interf d.SetPartial("display_text") } + if d.HasChange("host_tags") { + log.Printf("[DEBUG] Host tags changed for %s, starting update", name) + + // Create a new parameter struct + p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id()) + + // Set the new host tags + p.SetHosttags(d.Get("host_tags").(string)) + + // Update the host tags + _, err := cs.ServiceOffering.UpdateServiceOffering(p) + if err != nil { + return fmt.Errorf( + "Error updating the host tags for service offering %s: %s", name, err) + } + + d.SetPartial("host_tags") + } + d.Partial(false) return resourceCloudStackServiceOfferingRead(d, meta) diff --git a/cloudstack/resource_cloudstack_service_offering_test.go b/cloudstack/resource_cloudstack_service_offering_test.go new file mode 100644 index 00000000..1e1d6ccf --- /dev/null +++ b/cloudstack/resource_cloudstack_service_offering_test.go @@ -0,0 +1,85 @@ +// +// 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" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccCloudStackServiceOffering_basic(t *testing.T) { + var so cloudstack.ServiceOffering + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackServiceOffering_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackServiceOfferingExists("cloudstack_service_offering.test1", &so), + resource.TestCheckResourceAttr("cloudstack_service_offering.test1", "cpu_number", "2"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test1", "cpu_speed", "2200"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test1", "memory", "8096"), + ), + }, + }, + }) +} + +const testAccCloudStackServiceOffering_basic = ` +resource "cloudstack_service_offering" "test1" { + name = "service_offering_1" + display_text = "Test" + cpu_number = 2 + cpu_speed = 2200 + memory = 8096 +} +` + +func testAccCheckCloudStackServiceOfferingExists(n string, so *cloudstack.ServiceOffering) 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 service offering ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + resp, _, err := cs.ServiceOffering.GetServiceOfferingByID(rs.Primary.ID) + if err != nil { + return err + } + + if resp.Id != rs.Primary.ID { + return fmt.Errorf("Service offering not found") + } + + *so = *resp + + return nil + } +} diff --git a/website/docs/r/service_offering.html.markdown b/website/docs/r/service_offering.html.markdown index c4b50963..94735eb8 100644 --- a/website/docs/r/service_offering.html.markdown +++ b/website/docs/r/service_offering.html.markdown @@ -24,16 +24,36 @@ resource "cloudstack_service_offering" "example" { The following arguments are supported: -* `name` - (Required) The name of the service offering. -* `display_text` - (Required) The display text of the service offering. +* `name` - (Required) Name of the service offering. + Changing this forces a new resource to be created. + +* `display_text` - (Optional) The display text of the service offering. + +* `cpu_number` - (Optional) The number of CPU cores. + Changing this forces a new resource to be created. + +* `cpu_speed` - (Optional) The speed of the CPU in Mhz. + Changing this forces a new resource to be created. + +* `memory` - (Optional) Memory reserved by the VM in MB. + Changing this forces a new resource to be created. + +* `host_tags` - (Optional) The host tags for the service offering. + +* `limit_cpu_use` - (Optional) Restrict the CPU usage to committed service offering. + Changing this forces a new resource to be created. + +* `offer_ha` - (Optional) The HA for the service offering. + Changing this forces a new resource to be created. + +* `storage_type` - (Optional) The storage type of the service offering. Values are `local` and `shared`. + Changing this forces a new resource to be created. ## Attributes Reference The following attributes are exported: * `id` - The ID of the service offering. -* `name` - The name of the service offering. -* `display_text` - The display text of the service offering. ## Import