From 7838056f8f984c2c9712cbdfa43a9f0768e8eb91 Mon Sep 17 00:00:00 2001 From: Dom Del Nano Date: Mon, 28 Sep 2020 23:31:37 -0700 Subject: [PATCH] Create network data source (#68) * Create network data source and get initial acceptance tests passing * Refactor FindFromGetAllObjects to use shared logic * Use XOA_POOL environment variable to parameterize integration / acceptance tests * Add docs for the data source * Fix verbose logging * Add stronger assertions and add tests for compare logic * Refactor setup code to be shared between the client and acceptance tests --- client/client.go | 24 ++-- client/errors.go | 7 +- client/errors_test.go | 4 +- client/network.go | 133 +++++++++++++++++++ client/network_test.go | 86 ++++++++++++ client/pool.go | 22 +++ client/setup_test.go | 34 +++++ client/template_test.go | 2 +- docs/data-sources/network.md | 35 +++++ xoa/acc_setup_test.go | 21 +++ xoa/data_source_xenorchestra_network.go | 50 +++++++ xoa/data_source_xenorchestra_network_test.go | 98 ++++++++++++++ xoa/provider.go | 1 + xoa/provider_test.go | 3 + 14 files changed, 502 insertions(+), 18 deletions(-) create mode 100644 client/network.go create mode 100644 client/network_test.go create mode 100644 client/setup_test.go create mode 100644 docs/data-sources/network.md create mode 100644 xoa/acc_setup_test.go create mode 100644 xoa/data_source_xenorchestra_network.go create mode 100644 xoa/data_source_xenorchestra_network_test.go diff --git a/client/client.go b/client/client.go index 856fc050..a494ec1d 100644 --- a/client/client.go +++ b/client/client.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log" "net/http" "os" "time" @@ -87,6 +88,7 @@ func NewClient(config Config) (*Client, error) { func (c *Client) Call(ctx context.Context, method string, params, result interface{}, opt ...jsonrpc2.CallOption) error { err := c.rpc.Call(ctx, method, params, &result, opt...) + log.Printf("[TRACE] Made rpc call `%s` with params: %v and received %+v: result with error: %v\n", method, params, result, err) if err != nil { rpcErr, ok := err.(*jsonrpc2.Error) @@ -111,10 +113,11 @@ type XoObject interface { New(obj map[string]interface{}) XoObject } -func (c *Client) FindFromGetAllObjects(obj XoObject) (interface{}, error) { - +func (c *Client) GetAllObjectsOfType(obj XoObject, response interface{}) error { xoApiType := "" switch t := obj.(type) { + case Network: + xoApiType = "network" case PIF: xoApiType = "PIF" case Pool: @@ -135,12 +138,15 @@ func (c *Client) FindFromGetAllObjects(obj XoObject) (interface{}, error) { "type": xoApiType, }, } + ctx, _ := context.WithTimeout(context.Background(), 100*time.Second) + return c.Call(ctx, "xo.getAllObjects", params, &response) +} + +func (c *Client) FindFromGetAllObjects(obj XoObject) (interface{}, error) { var objsRes struct { Objects map[string]interface{} `json:"-"` } - ctx, _ := context.WithTimeout(context.Background(), 100*time.Second) - err := c.Call(ctx, "xo.getAllObjects", params, &objsRes.Objects) - + err := c.GetAllObjectsOfType(obj, &objsRes.Objects) if err != nil { return obj, err } @@ -153,20 +159,16 @@ func (c *Client) FindFromGetAllObjects(obj XoObject) (interface{}, error) { return obj, errors.New("Could not coerce interface{} into map") } - if v["type"].(string) != xoApiType { - continue - } - if obj.Compare(v) { found = true objs = append(objs, obj.New(v)) } } if !found { - return obj, NotFound{Type: xoApiType, Query: obj} + return obj, NotFound{Query: obj} } - fmt.Printf("[DEBUG] Found the following objects from xo.getAllObjects: %+v\n", objs) + log.Printf("[TRACE] Found the following objects from xo.getAllObjects: %+v\n", objs) if len(objs) == 1 { return objs[0], nil diff --git a/client/errors.go b/client/errors.go index 88476a91..bcfb4c42 100644 --- a/client/errors.go +++ b/client/errors.go @@ -1,12 +1,13 @@ package client -import "fmt" +import ( + "fmt" +) type NotFound struct { Query XoObject - Type string } func (e NotFound) Error() string { - return fmt.Sprintf("Could not find %s with query: %+v", e.Type, e.Query) + return fmt.Sprintf("Could not find %[1]T with query: %+[1]v", e.Query) } diff --git a/client/errors_test.go b/client/errors_test.go index 865f4a17..2fabdb3e 100644 --- a/client/errors_test.go +++ b/client/errors_test.go @@ -6,16 +6,14 @@ import ( ) func TestNotFoundErrorMessage(t *testing.T) { - vifType := "VIF" vif := VIF{ MacAddress: "E8:61:7E:8E:F1:81", } err := NotFound{ Query: vif, - Type: vifType, } - expectedMsg := fmt.Sprintf("Could not find %s with query: %+v", vifType, vif) + expectedMsg := fmt.Sprintf("Could not find client.VIF with query: %+v", vif) msg := err.Error() if expectedMsg != msg { diff --git a/client/network.go b/client/network.go new file mode 100644 index 00000000..a2279bd8 --- /dev/null +++ b/client/network.go @@ -0,0 +1,133 @@ +package client + +import ( + "context" + "errors" + "fmt" + "log" + "strings" + "time" +) + +type Network struct { + Id string `json:"id"` + NameLabel string `json:"name_label"` + Bridge string + PoolId string +} + +func (net Network) Compare(obj map[string]interface{}) bool { + id := obj["id"].(string) + nameLabel := obj["name_label"].(string) + poolId := obj["$poolId"].(string) + if net.Id == id { + return true + } + + labelsMatch := false + if net.NameLabel == nameLabel { + labelsMatch = true + } + + if net.PoolId == "" && labelsMatch { + return true + } else if net.PoolId == poolId && labelsMatch { + return true + } + + return false +} + +func (net Network) New(obj map[string]interface{}) XoObject { + id := obj["id"].(string) + poolId := obj["$poolId"].(string) + nameLabel := obj["name_label"].(string) + bridge := obj["bridge"].(string) + return Network{ + Id: id, + Bridge: bridge, + PoolId: poolId, + NameLabel: nameLabel, + } +} + +func (c *Client) CreateNetwork(netReq Network) (*Network, error) { + var id string + params := map[string]interface{}{ + "pool": netReq.PoolId, + "name": netReq.NameLabel, + } + + ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) + err := c.Call(ctx, "network.create", params, &id) + + if err != nil { + return nil, err + } + return c.GetNetwork(Network{Id: id}) +} + +func (c *Client) GetNetwork(netReq Network) (*Network, error) { + obj, err := c.FindFromGetAllObjects(netReq) + + if err != nil { + return nil, err + } + + if _, ok := obj.([]interface{}); ok { + return nil, errors.New("Your query returned more than one result. Use `pool_id` or other attributes to filter the result down to a single network") + } + + net := obj.(Network) + return &net, nil +} + +func (c *Client) GetNetworks() ([]Network, error) { + var response map[string]Network + err := c.GetAllObjectsOfType(Network{}, &response) + + nets := make([]Network, 0, len(response)) + for _, net := range response { + nets = append(nets, net) + } + return nets, err +} + +func (c *Client) DeleteNetwork(id string) error { + var success bool + params := map[string]interface{}{ + "id": id, + } + + ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) + err := c.Call(ctx, "network.delete", params, &success) + + return err +} + +func RemoveNetworksWithNamePrefix(prefix string) func(string) error { + return func(_ string) error { + c, err := NewClient(GetConfigFromEnv()) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + nets, err := c.GetNetworks() + + if err != nil { + return err + } + + for _, net := range nets { + if strings.HasPrefix(net.NameLabel, prefix) { + log.Printf("[DEBUG] Deleting network: %v\n", net) + err = c.DeleteNetwork(net.Id) + + if err != nil { + log.Printf("error destroying network `%s` during sweep: %v", net.NameLabel, err) + } + } + } + return nil + } +} diff --git a/client/network_test.go b/client/network_test.go new file mode 100644 index 00000000..8227b0d1 --- /dev/null +++ b/client/network_test.go @@ -0,0 +1,86 @@ +package client + +import "testing" + +var testNetworkName string = integrationTestPrefix + "network" + +func TestNetworkCompare(t *testing.T) { + + nameLabel := "network label" + poolId := "pool id" + cases := []struct { + net Network + result bool + obj map[string]interface{} + }{ + { + net: Network{ + NameLabel: nameLabel, + }, + result: true, + obj: map[string]interface{}{ + "id": "355ee47d-ff4c-4924-3db2-fd86ae629676", + "name_label": nameLabel, + "$poolId": "355ee47d-ff4c-4924-3db2-fd86ae629676", + }, + }, + { + net: Network{ + NameLabel: nameLabel, + PoolId: poolId, + }, + result: false, + obj: map[string]interface{}{ + "id": "355ee47d-ff4c-4924-3db2-fd86ae629676", + "name_label": nameLabel, + "$poolId": "355ee47d-ff4c-4924-3db2-fd86ae629676", + }, + }, + { + net: Network{ + NameLabel: nameLabel, + PoolId: poolId, + }, + result: true, + obj: map[string]interface{}{ + "id": "355ee47d-ff4c-4924-3db2-fd86ae629676", + "name_label": nameLabel, + "$poolId": poolId, + }, + }, + } + + for _, test := range cases { + net := test.net + obj := test.obj + result := test.result + + if net.Compare(obj) != result { + t.Errorf("expected network `%+v` to Compare '%t' with object `%v`", net, result, obj) + } + } +} + +func TestGetNetwork(t *testing.T) { + c, err := NewClient(GetConfigFromEnv()) + + if err != nil { + t.Fatalf("failed to create client with error: %v", err) + } + + net, err := c.GetNetwork(Network{ + NameLabel: testNetworkName, + }) + + if err != nil { + t.Fatalf("failed to retrieve network `%s` with error: %v", testNetworkName, err) + } + + if net == nil { + t.Fatalf("should have received network, instead received nil") + } + + if net.NameLabel != testNetworkName { + t.Errorf("expected network name_label `%s` to match `%s`", net.NameLabel, testNetworkName) + } +} diff --git a/client/pool.go b/client/pool.go index 375148bf..0f659362 100644 --- a/client/pool.go +++ b/client/pool.go @@ -1,5 +1,10 @@ package client +import ( + "fmt" + "os" +) + type Pool struct { Id string NameLabel string @@ -50,3 +55,20 @@ func (c *Client) GetPoolByName(name string) (Pool, error) { return pool, nil } + +func FindPoolForTests(pool *Pool) { + poolName, found := os.LookupEnv("XOA_POOL") + + if !found { + fmt.Println("The XOA_POOL environment variable must be set") + os.Exit(-1) + } + c, _ := NewClient(GetConfigFromEnv()) + var err error + *pool, err = c.GetPoolByName(poolName) + + if err != nil { + fmt.Printf("failed to find a pool with name: %v with error: %v\n", poolName, err) + os.Exit(-1) + } +} diff --git a/client/setup_test.go b/client/setup_test.go new file mode 100644 index 00000000..ca0236cf --- /dev/null +++ b/client/setup_test.go @@ -0,0 +1,34 @@ +package client + +import ( + "os" + "testing" +) + +func CreateNetwork() error { + + c, err := NewClient(GetConfigFromEnv()) + + if err != nil { + return err + } + + _, err = c.CreateNetwork(Network{ + NameLabel: testNetworkName, + PoolId: accTestPool.Id, + }) + return err +} + +var integrationTestPrefix string = "xenorchestra-client-" +var accTestPool Pool + +func TestMain(m *testing.M) { + FindPoolForTests(&accTestPool) + CreateNetwork() + code := m.Run() + + RemoveNetworksWithNamePrefix(integrationTestPrefix)("") + + os.Exit(code) +} diff --git a/client/template_test.go b/client/template_test.go index 06db73da..d5858dfc 100644 --- a/client/template_test.go +++ b/client/template_test.go @@ -22,7 +22,7 @@ func TestGetTemplate(t *testing.T) { { templateName: "Not found", template: Template{}, - err: NotFound{Type: "VM-template", Query: Template{NameLabel: "Not found"}}, + err: NotFound{Query: Template{NameLabel: "Not found"}}, }, } diff --git a/docs/data-sources/network.md b/docs/data-sources/network.md new file mode 100644 index 00000000..76969f52 --- /dev/null +++ b/docs/data-sources/network.md @@ -0,0 +1,35 @@ +# xenorchestra_network + +Provides information about a network of a Xenserver pool. + +## Example Usage + +```hcl +data "xenorchestra_network" "net" { + name_label = "Pool-wide network associated with eth0" +} + +resource "xenorchestra_vm" "demo-vm" { + // ... + network { + network_id = "${data.xenorchestra_network.net.id}" + } + // ... +} +``` + +## Argument Reference +* name_label - (Required) The name of the network. +* pool_id - (Optional) The id of the pool associated with the network. + +**Note:** If there are multiple networks with the same name terraform will fail. +Ensure that your network, pool_id and other arguments identify a unique network. + +## Attributes Reference +* id is set to the ID generated by the XO api. +* attached - If the PIF is attached to the network +* uuid - uuid of the PIF. +* host - The host the PIF is associated with. +* pool_id - The pool the PIF is associated with. +* network - The network the PIF is associated with. +* vlan - The vlan the PIF is associated with. diff --git a/xoa/acc_setup_test.go b/xoa/acc_setup_test.go new file mode 100644 index 00000000..c304fec1 --- /dev/null +++ b/xoa/acc_setup_test.go @@ -0,0 +1,21 @@ +package xoa + +import ( + "os" + "testing" + + "github.com/ddelnano/terraform-provider-xenorchestra/client" +) + +var testObjectIndex int = 1 +var accTestPrefix string = "terraform-acc-test-" +var accTestPool client.Pool + +func TestMain(m *testing.M) { + client.FindPoolForTests(&accTestPool) + code := m.Run() + + client.RemoveNetworksWithNamePrefix("terraform-acc")("") + + os.Exit(code) +} diff --git a/xoa/data_source_xenorchestra_network.go b/xoa/data_source_xenorchestra_network.go new file mode 100644 index 00000000..6469a380 --- /dev/null +++ b/xoa/data_source_xenorchestra_network.go @@ -0,0 +1,50 @@ +package xoa + +import ( + "github.com/ddelnano/terraform-provider-xenorchestra/client" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func dataSourceXoaNetwork() *schema.Resource { + return &schema.Resource{ + Read: dataSourceNetworkRead, + Schema: map[string]*schema.Schema{ + "bridge": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "name_label": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "pool_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func dataSourceNetworkRead(d *schema.ResourceData, m interface{}) error { + config := m.(client.Config) + c, err := client.NewClient(config) + + nameLabel := d.Get("name_label").(string) + poolId := d.Get("pool_id").(string) + + net, err := c.GetNetwork(client.Network{ + NameLabel: nameLabel, + PoolId: poolId, + }) + + if err != nil { + return err + } + + d.SetId(net.Id) + d.Set("bridge", net.Bridge) + d.Set("name_label", net.NameLabel) + d.Set("pool_id", net.PoolId) + return nil +} diff --git a/xoa/data_source_xenorchestra_network_test.go b/xoa/data_source_xenorchestra_network_test.go new file mode 100644 index 00000000..39790411 --- /dev/null +++ b/xoa/data_source_xenorchestra_network_test.go @@ -0,0 +1,98 @@ +package xoa + +import ( + "fmt" + "regexp" + "testing" + + "github.com/ddelnano/terraform-provider-xenorchestra/client" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +var createNetwork = func(net client.Network, t *testing.T, times int) func() { + return func() { + for i := 0; i < times; i++ { + c, err := client.NewClient(client.GetConfigFromEnv()) + + if err != nil { + t.Fatalf("failed to created client with error: %v", err) + } + + c.CreateNetwork(net) + } + } +} + +var getTestNetwork = func(poolId string) client.Network { + nameLabel := fmt.Sprintf("%s-network-%d", accTestPrefix, testObjectIndex) + testObjectIndex++ + return client.Network{ + NameLabel: nameLabel, + PoolId: poolId, + } +} + +func TestAccXONetworkDataSource_read(t *testing.T) { + resourceName := "data.xenorchestra_network.network" + net := getTestNetwork(accTestPool.Id) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: createNetwork(net, t, 1), + Config: testAccXenorchestraDataSourceNetworkConfig(net.NameLabel), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckXenorchestraDataSourceNetwork(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttrSet(resourceName, "name_label"), + resource.TestCheckResourceAttrSet(resourceName, "pool_id"), + resource.TestCheckResourceAttrSet(resourceName, "bridge")), + }, + }, + }, + ) +} + +func TestAccXONetworkDataSource_multipleCauseError(t *testing.T) { + resourceName := "data.xenorchestra_network.network" + net := getTestNetwork(accTestPool.Id) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + PreConfig: createNetwork(net, t, 2), + Config: testAccXenorchestraDataSourceNetworkConfig(net.NameLabel), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckXenorchestraDataSourceNetwork(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id")), + ExpectError: regexp.MustCompile(`Your query returned more than one result`), + }, + }, + }, + ) +} + +func testAccCheckXenorchestraDataSourceNetwork(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Can't find Network data source: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Network data source ID not set") + } + return nil + } +} + +var testAccXenorchestraDataSourceNetworkConfig = func(name string) string { + return fmt.Sprintf(` +data "xenorchestra_network" "network" { + name_label = "%s" +} +`, name) +} diff --git a/xoa/provider.go b/xoa/provider.go index f3ea4f73..1ca602a1 100644 --- a/xoa/provider.go +++ b/xoa/provider.go @@ -33,6 +33,7 @@ func Provider() terraform.ResourceProvider { "xenorchestra_cloud_config": resourceCloudConfigRecord(), }, DataSourcesMap: map[string]*schema.Resource{ + "xenorchestra_network": dataSourceXoaNetwork(), "xenorchestra_pif": dataSourceXoaPIF(), "xenorchestra_pool": dataSourceXoaPool(), "xenorchestra_template": dataSourceXoaTemplate(), diff --git a/xoa/provider_test.go b/xoa/provider_test.go index b2acbaab..36ffcbf8 100644 --- a/xoa/provider_test.go +++ b/xoa/provider_test.go @@ -28,4 +28,7 @@ func testAccPreCheck(t *testing.T) { if v := os.Getenv("XOA_PASSWORD"); v == "" { t.Fatal("The XOA_PASSWORD environment variable must be set") } + if v := os.Getenv("XOA_POOL"); v == "" { + t.Fatal("The XOA_POOL environment variable must be set") + } }