diff --git a/README.md b/README.md index 32dbae1c..1b31d7ca 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,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 -it cloudstack-simulator python /root/tools/marvin/marvin/deployDataCenter.py -i /root/setup/dev/advanced.cfg +docker exec -it 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: @@ -200,7 +200,7 @@ Check and ensure TF provider passes builds, GA and run this for local checks: goreleaser release --snapshot --clean ``` -Next, create a personalised Github token: https://github.com/settings/tokens/new?scopes=repo,write:packages +Next, create a personalised Github token: https://github.com/settings/tokens/new?scopes=repo,write:packages ``` export GITHUB_TOKEN="YOUR_GH_TOKEN" diff --git a/cloudstack/provider.go b/cloudstack/provider.go index a71df0e5..ae1105b1 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -112,6 +112,7 @@ func Provider() *schema.Provider { "cloudstack_nic": resourceCloudStackNIC(), "cloudstack_port_forward": resourceCloudStackPortForward(), "cloudstack_private_gateway": resourceCloudStackPrivateGateway(), + "cloudstack_project": resourceCloudStackProject(), "cloudstack_secondary_ipaddress": resourceCloudStackSecondaryIPAddress(), "cloudstack_security_group": resourceCloudStackSecurityGroup(), "cloudstack_security_group_rule": resourceCloudStackSecurityGroupRule(), diff --git a/cloudstack/resource_cloudstack_project.go b/cloudstack/resource_cloudstack_project.go new file mode 100644 index 00000000..d399225e --- /dev/null +++ b/cloudstack/resource_cloudstack_project.go @@ -0,0 +1,373 @@ +// 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" + "log" + "strings" + "time" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func resourceCloudStackProject() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackProjectCreate, + Read: resourceCloudStackProjectRead, + Update: resourceCloudStackProjectUpdate, + Delete: resourceCloudStackProjectDelete, + Importer: &schema.ResourceImporter{ + State: importStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + + "display_text": { + Type: schema.TypeString, + Required: true, // Required for API version 4.18 and lower. TODO: Make this optional when support for API versions older than 4.18 is dropped. + }, + + "domain": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "account": { + Type: schema.TypeString, + Optional: true, + }, + + "accountid": { + Type: schema.TypeString, + Optional: true, + }, + + "userid": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceCloudStackProjectCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the name and display_text + name := d.Get("name").(string) + displaytext := d.Get("display_text").(string) + + // The CloudStack API parameter order differs between versions: + // - In API 4.18 and lower: displaytext is the first parameter and name is the second + // - In API 4.19 and higher: name is the first parameter and displaytext is optional + // The CloudStack Go SDK uses the API 4.18 parameter order + p := cs.Project.NewCreateProjectParams(displaytext, name) + + // Set the domain if provided + if domain, ok := d.GetOk("domain"); ok { + domainid, e := retrieveID(cs, "domain", domain.(string)) + if e != nil { + return e.Error() + } + p.SetDomainid(domainid) + } + + // Set the account if provided + if account, ok := d.GetOk("account"); ok { + p.SetAccount(account.(string)) + } + + // Set the accountid if provided + if accountid, ok := d.GetOk("accountid"); ok { + p.SetAccountid(accountid.(string)) + } + + // Set the userid if provided + if userid, ok := d.GetOk("userid"); ok { + p.SetUserid(userid.(string)) + } + + log.Printf("[DEBUG] Creating project %s", name) + r, err := cs.Project.CreateProject(p) + if err != nil { + return fmt.Errorf("Error creating project %s: %s", name, err) + } + + d.SetId(r.Id) + + // Wait for the project to be available, but with a shorter timeout + // to prevent getting stuck indefinitely + err = resource.Retry(30*time.Second, func() *resource.RetryError { + project, err := getProjectByID(cs, d.Id()) + if err != nil { + if strings.Contains(err.Error(), "not found") { + log.Printf("[DEBUG] Project %s not found yet, retrying...", d.Id()) + return resource.RetryableError(fmt.Errorf("Project not yet created: %s", err)) + } + return resource.NonRetryableError(fmt.Errorf("Error retrieving project: %s", err)) + } + + log.Printf("[DEBUG] Project %s found with name %s", d.Id(), project.Name) + return nil + }) + + // Even if the retry times out, we should still try to read the resource + // since it might have been created successfully + if err != nil { + log.Printf("[WARN] Timeout waiting for project %s to be available: %s", d.Id(), err) + } + + // Read the resource state + return resourceCloudStackProjectRead(d, meta) +} + +// Helper function to get a project by ID +func getProjectByID(cs *cloudstack.CloudStackClient, id string) (*cloudstack.Project, error) { + p := cs.Project.NewListProjectsParams() + p.SetId(id) + + l, err := cs.Project.ListProjects(p) + if err != nil { + return nil, err + } + + if l.Count == 0 { + return nil, fmt.Errorf("project with id %s not found", id) + } + + return l.Projects[0], nil +} + +func resourceCloudStackProjectRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Retrieving project %s", d.Id()) + + // Get the project details + project, err := getProjectByID(cs, d.Id()) + if err != nil { + if strings.Contains(err.Error(), "not found") || + strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + log.Printf("[DEBUG] Project %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + return err + } + + log.Printf("[DEBUG] Found project %s: %s", d.Id(), project.Name) + + // Set the basic attributes + d.Set("name", project.Name) + d.Set("display_text", project.Displaytext) + d.Set("domain", project.Domain) + + // Handle owner information more safely + // Only set the account, accountid, and userid if they were explicitly set in the configuration + // and if the owner information is available + if _, ok := d.GetOk("account"); ok { + // Safely handle the case where project.Owner might be nil or empty + if len(project.Owner) > 0 { + foundAccount := false + for _, owner := range project.Owner { + if account, ok := owner["account"]; ok { + d.Set("account", account) + foundAccount = true + break + } + } + if !foundAccount { + log.Printf("[DEBUG] Project %s owner information doesn't contain account, keeping original value", d.Id()) + } + } else { + // Keep the original account value from the configuration + // This prevents Terraform from thinking the resource has disappeared + log.Printf("[DEBUG] Project %s owner information not available yet, keeping original account value", d.Id()) + } + } + + if _, ok := d.GetOk("accountid"); ok { + if len(project.Owner) > 0 { + foundAccountID := false + for _, owner := range project.Owner { + if accountid, ok := owner["accountid"]; ok { + d.Set("accountid", accountid) + foundAccountID = true + break + } + } + if !foundAccountID { + log.Printf("[DEBUG] Project %s owner information doesn't contain accountid, keeping original value", d.Id()) + } + } else { + log.Printf("[DEBUG] Project %s owner information not available yet, keeping original accountid value", d.Id()) + } + } + + if _, ok := d.GetOk("userid"); ok { + if len(project.Owner) > 0 { + foundUserID := false + for _, owner := range project.Owner { + if userid, ok := owner["userid"]; ok { + d.Set("userid", userid) + foundUserID = true + break + } + } + if !foundUserID { + log.Printf("[DEBUG] Project %s owner information doesn't contain userid, keeping original value", d.Id()) + } + } else { + log.Printf("[DEBUG] Project %s owner information not available yet, keeping original userid value", d.Id()) + } + } + + return nil +} + +func resourceCloudStackProjectUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Check if the name or display text is changed + if d.HasChange("name") || d.HasChange("display_text") { + // Create a new parameter struct + p := cs.Project.NewUpdateProjectParams(d.Id()) + + // Set the name and display_text if they have changed + // Note: The 'name' parameter is only available in API 4.19 and higher + // If you're using API 4.18 or lower, the SetName method might not work + // In that case, you might need to update the display_text only + if d.HasChange("name") { + p.SetName(d.Get("name").(string)) + } + + if d.HasChange("display_text") { + p.SetDisplaytext(d.Get("display_text").(string)) + } + + log.Printf("[DEBUG] Updating project %s", d.Id()) + _, err := cs.Project.UpdateProject(p) + if err != nil { + return fmt.Errorf("Error updating project %s: %s", d.Id(), err) + } + } + + // Check if the account, accountid, or userid is changed + if d.HasChange("account") || d.HasChange("accountid") || d.HasChange("userid") { + // Create a new parameter struct + p := cs.Project.NewUpdateProjectParams(d.Id()) + + // Set swapowner to true to swap ownership with the account/user provided + p.SetSwapowner(true) + + // Set the account if it has changed + if d.HasChange("account") { + p.SetAccount(d.Get("account").(string)) + } + + // Set the userid if it has changed + if d.HasChange("userid") { + p.SetUserid(d.Get("userid").(string)) + } + + // Note: accountid is not directly supported by the UpdateProject API, + // but we can use the account parameter instead if accountid has changed + if d.HasChange("accountid") && !d.HasChange("account") { + // If accountid has changed but account hasn't, we need to look up the account name + // This is a placeholder - in a real implementation, you would need to look up + // the account name from the accountid + log.Printf("[WARN] Updating accountid is not directly supported by the API. Please use account instead.") + } + + log.Printf("[DEBUG] Updating project owner %s", d.Id()) + _, err := cs.Project.UpdateProject(p) + if err != nil { + return fmt.Errorf("Error updating project owner %s: %s", d.Id(), err) + } + } + + // Wait for the project to be updated, but with a shorter timeout + err := resource.Retry(30*time.Second, func() *resource.RetryError { + project, err := getProjectByID(cs, d.Id()) + if err != nil { + if strings.Contains(err.Error(), "not found") { + log.Printf("[DEBUG] Project %s not found after update, retrying...", d.Id()) + return resource.RetryableError(fmt.Errorf("Project not found after update: %s", err)) + } + return resource.NonRetryableError(fmt.Errorf("Error retrieving project after update: %s", err)) + } + + // Check if the project has the expected values + if d.HasChange("name") && project.Name != d.Get("name").(string) { + log.Printf("[DEBUG] Project %s name not updated yet, retrying...", d.Id()) + return resource.RetryableError(fmt.Errorf("Project name not updated yet")) + } + + if d.HasChange("display_text") && project.Displaytext != d.Get("display_text").(string) { + log.Printf("[DEBUG] Project %s display_text not updated yet, retrying...", d.Id()) + return resource.RetryableError(fmt.Errorf("Project display_text not updated yet")) + } + + log.Printf("[DEBUG] Project %s updated successfully", d.Id()) + return nil + }) + + // Even if the retry times out, we should still try to read the resource + // since it might have been updated successfully + if err != nil { + log.Printf("[WARN] Timeout waiting for project %s to be updated: %s", d.Id(), err) + } + + // Read the resource state + return resourceCloudStackProjectRead(d, meta) +} + +func resourceCloudStackProjectDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Project.NewDeleteProjectParams(d.Id()) + + log.Printf("[INFO] Deleting project: %s", d.Id()) + _, err := cs.Project.DeleteProject(p) + if err != nil { + // This is a very poor way to be told the ID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting project %s: %s", d.Id(), err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_project_test.go b/cloudstack/resource_cloudstack_project_test.go new file mode 100644 index 00000000..cdc5d4f2 --- /dev/null +++ b/cloudstack/resource_cloudstack_project_test.go @@ -0,0 +1,330 @@ +// 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-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackProject_basic(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.foo", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "name", "terraform-test-project"), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "display_text", "Terraform Test Project"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_update(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.foo", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "name", "terraform-test-project"), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "display_text", "Terraform Test Project"), + ), + }, + { + Config: testAccCloudStackProject_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.foo", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "name", "terraform-test-project-updated"), + resource.TestCheckResourceAttr( + "cloudstack_project.foo", "display_text", "Terraform Test Project Updated"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_import(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_basic, + }, + { + ResourceName: "cloudstack_project.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccCloudStackProject_account(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_account, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.bar", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "name", "terraform-test-project-account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "display_text", "Terraform Test Project with Account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "account", "admin"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_updateAccount(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_account, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.bar", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "name", "terraform-test-project-account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "display_text", "Terraform Test Project with Account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "account", "admin"), + ), + }, + { + Config: testAccCloudStackProject_updateAccount, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.bar", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "name", "terraform-test-project-account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "display_text", "Terraform Test Project with Account"), + resource.TestCheckResourceAttr( + "cloudstack_project.bar", "account", "admin"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_emptyDisplayText(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_emptyDisplayText, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.empty", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.empty", "name", "terraform-test-project-empty-display"), + resource.TestCheckResourceAttr( + "cloudstack_project.empty", "display_text", "terraform-test-project-empty-display"), + ), + }, + }, + }) +} + +func TestAccCloudStackProject_updateUserid(t *testing.T) { + var project cloudstack.Project + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackProjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackProject_userid, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.baz", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.baz", "name", "terraform-test-project-userid"), + resource.TestCheckResourceAttr( + "cloudstack_project.baz", "display_text", "Terraform Test Project with Userid"), + ), + }, + { + Config: testAccCloudStackProject_updateUserid, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackProjectExists( + "cloudstack_project.baz", &project), + resource.TestCheckResourceAttr( + "cloudstack_project.baz", "name", "terraform-test-project-userid-updated"), + resource.TestCheckResourceAttr( + "cloudstack_project.baz", "display_text", "Terraform Test Project with Userid Updated"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackProjectExists( + n string, project *cloudstack.Project) 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 project ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + p := cs.Project.NewListProjectsParams() + p.SetId(rs.Primary.ID) + + list, err := cs.Project.ListProjects(p) + if err != nil { + return err + } + + if list.Count != 1 || list.Projects[0].Id != rs.Primary.ID { + return fmt.Errorf("Project not found") + } + + *project = *list.Projects[0] + + return nil + } +} + +func testAccCheckCloudStackProjectDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_project" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No project ID is set") + } + + p := cs.Project.NewListProjectsParams() + p.SetId(rs.Primary.ID) + + list, err := cs.Project.ListProjects(p) + if err != nil { + return err + } + + if list.Count != 0 { + return fmt.Errorf("Project %s still exists", rs.Primary.ID) + } + } + + return nil +} + +const testAccCloudStackProject_basic = ` +resource "cloudstack_project" "foo" { + name = "terraform-test-project" + display_text = "Terraform Test Project" +}` + +const testAccCloudStackProject_update = ` +resource "cloudstack_project" "foo" { + name = "terraform-test-project-updated" + display_text = "Terraform Test Project Updated" +}` + +const testAccCloudStackProject_account = ` +resource "cloudstack_project" "bar" { + name = "terraform-test-project-account" + display_text = "Terraform Test Project with Account" + account = "admin" + domain = "ROOT" +}` + +const testAccCloudStackProject_updateAccount = ` +resource "cloudstack_project" "bar" { + name = "terraform-test-project-account" + display_text = "Terraform Test Project with Account" + account = "admin" + domain = "ROOT" +}` + +const testAccCloudStackProject_userid = ` +resource "cloudstack_project" "baz" { + name = "terraform-test-project-userid" + display_text = "Terraform Test Project with Userid" + domain = "ROOT" +}` + +const testAccCloudStackProject_updateUserid = ` +resource "cloudstack_project" "baz" { + name = "terraform-test-project-userid-updated" + display_text = "Terraform Test Project with Userid Updated" + domain = "ROOT" +}` + +const testAccCloudStackProject_emptyDisplayText = ` +resource "cloudstack_project" "empty" { + name = "terraform-test-project-empty-display" + display_text = "terraform-test-project-empty-display" +}` diff --git a/cloudstack/resources.go b/cloudstack/resources.go index 22b2adcc..5a75b77d 100644 --- a/cloudstack/resources.go +++ b/cloudstack/resources.go @@ -72,6 +72,8 @@ func retrieveID(cs *cloudstack.CloudStackClient, name string, value string, opts switch name { case "disk_offering": id, _, err = cs.DiskOffering.GetDiskOfferingID(value) + case "domain": + id, _, err = cs.Domain.GetDomainID(value) case "kubernetes_version": id, _, err = cs.Kubernetes.GetKubernetesSupportedVersionID(value) case "network_offering": diff --git a/scripts/list_accounts_and_users.go b/scripts/list_accounts_and_users.go new file mode 100644 index 00000000..fbcbffb0 --- /dev/null +++ b/scripts/list_accounts_and_users.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/apache/cloudstack-go/v2/cloudstack" +) + +func main() { + // Create a new CloudStack client + apiURL := os.Getenv("CLOUDSTACK_API_URL") + apiKey := os.Getenv("CLOUDSTACK_API_KEY") + secretKey := os.Getenv("CLOUDSTACK_SECRET_KEY") + + if apiURL == "" || apiKey == "" || secretKey == "" { + log.Fatal("CLOUDSTACK_API_URL, CLOUDSTACK_API_KEY, and CLOUDSTACK_SECRET_KEY must be set") + } + + cs := cloudstack.NewClient(apiURL, apiKey, secretKey, false) + + // List accounts + fmt.Println("=== Accounts ===") + p := cs.Account.NewListAccountsParams() + accounts, err := cs.Account.ListAccounts(p) + if err != nil { + log.Fatalf("Error listing accounts: %v", err) + } + + for _, account := range accounts.Accounts { + fmt.Printf("Account: %s, ID: %s\n", account.Name, account.Id) + } + + // List users + fmt.Println("\n=== Users ===") + u := cs.User.NewListUsersParams() + users, err := cs.User.ListUsers(u) + if err != nil { + log.Fatalf("Error listing users: %v", err) + } + + for _, user := range users.Users { + fmt.Printf("User: %s, ID: %s, Account: %s\n", user.Username, user.Id, user.Account) + } +} diff --git a/website/cloudstack.erb b/website/cloudstack.erb index f900196a..fff2c8e2 100644 --- a/website/cloudstack.erb +++ b/website/cloudstack.erb @@ -78,6 +78,10 @@ cloudstack_private_gateway +