From 7dd5bc912b583e39e074f08ebe4ff175ffcb382e Mon Sep 17 00:00:00 2001 From: Dimitrios Karagiannis Date: Mon, 7 Oct 2019 14:02:42 +0100 Subject: [PATCH 1/5] Initialise Vxc resources This is the first draft implementation of managing VXCs. The underlying API is still incomplete at this stage. Signed-off-by: Dimitrios Karagiannis --- megaport/api/client.go | 2 + megaport/api/vxc.go | 90 ++++++++-------- megaport/provider.go | 3 +- megaport/resource_megaport_private_vxc.go | 123 ++++++++++++++++++++++ 4 files changed, 176 insertions(+), 42 deletions(-) create mode 100644 megaport/resource_megaport_private_vxc.go diff --git a/megaport/api/client.go b/megaport/api/client.go index f44e58d..8e29b99 100644 --- a/megaport/api/client.go +++ b/megaport/api/client.go @@ -30,11 +30,13 @@ type Client struct { UserAgent string Port *PortService + Vxc *VxcService } func NewClient(baseURL string) *Client { c := &Client{c: &http.Client{}, BaseURL: baseURL} c.Port = NewPortService(c) + c.Vxc = NewVxcService(c) return c } diff --git a/megaport/api/vxc.go b/megaport/api/vxc.go index 6656bea..7537640 100644 --- a/megaport/api/vxc.go +++ b/megaport/api/vxc.go @@ -8,65 +8,73 @@ import ( "net/http" ) -type OrderVxc struct { +type vxcOrder struct { ProductUid string `json:"productUid"` - AssociatedVxcs []OrderVxcAssociatedVxcs `json:"associatedVxcs"` + AssociatedVxcs []vxcOrderAssociatedVxcs `json:"associatedVxcs"` } -type OrderVxcAssociatedVxcs struct { +type vxcOrderAssociatedVxcs struct { ProductName string `json:"productName"` RateLimit uint64 `json:"rateLimit"` - AEnd *OrderVxcEnd `json:"aEnd,omitempty"` - BEnd *OrderVxcEnd `json:"bEnd"` + AEnd *vxcOrderEnd `json:"aEnd,omitempty"` + BEnd *vxcOrderEnd `json:"bEnd"` } -type OrderVxcEnd struct { +type vxcOrderEnd struct { ProductUid string `json:"productUid"` VLan uint64 `json:"vlan,omitempty"` } -func (c *Client) PostPortOrder(o OrderPort) (string, error) { - p, err := json.Marshal([]OrderPort{o}) +type VxcService struct { + c *Client +} + +func NewVxcService(c *Client) *VxcService { + return &VxcService{c} +} + +func (p *VxcService) Create(productAUid, productBUid, name string, vlanA, vlanB, rateLimit uint64) (string, error) { + order := []vxcOrder{vxcOrder{ + ProductUid: productAUid, + AssociatedVxcs: []vxcOrderAssociatedVxcs{ + vxcOrderAssociatedVxcs{ + ProductName: name, + RateLimit: rateLimit, + BEnd: &vxcOrderEnd{ + ProductUid: productBUid, + }, + }, + }, + }} + if vlanA != 0 { + order[0].AssociatedVxcs[0].AEnd = &vxcOrderEnd{VLan: vlanB} + } + if vlanB != 0 { + order[0].AssociatedVxcs[0].BEnd.VLan = vlanB + } + payload, err := json.Marshal(order) if err != nil { return "", err } - b := bytes.NewBuffer(p) - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/v2/networkdesign/buy", c.BaseURL), b) + b := bytes.NewReader(payload) + validate := true // TODO: think + if validate { // TODO: do we really want to make this conditional? + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/v2/networkdesign/validate", p.c.BaseURL), b) + if err != nil { + return "", err + } + if err := p.c.do(req, nil); err != nil { + return "", err + } + b.Seek(0, 0) // TODO: ? + } + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/v2/networkdesign/buy", p.c.BaseURL), b) if err != nil { return "", err } d := []map[string]interface{}{} - if err := c.do(req, &d); err != nil { + if err := p.c.do(req, &d); err != nil { return "", err } - return d[0]["technicalServiceUid"].(string), nil -} - -func (c *Client) PostVxcOrder(o []OrderVxc) error { - if o == nil { - o = []OrderVxc{ - OrderVxc{ - ProductUid: "94ec5655-5bef-4734-b172-97f3aed05382", - AssociatedVxcs: []OrderVxcAssociatedVxcs{ - OrderVxcAssociatedVxcs{ - ProductName: "bar", - RateLimit: 100, - BEnd: &OrderVxcEnd{ - ProductUid: "f2c5b25b-e202-4708-9c25-1130c94689b3", - VLan: 99, - }, - }, - }, - }, - } - } - s, err := json.MarshalIndent(o, "", " ") - fmt.Printf("%+v\n", err) - fmt.Printf("%s\n", s) - b := bytes.NewBuffer(s) - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/v2/networkdesign/buy", c.BaseURL), b) - if err != nil { - return nil - } - return c.do(req, nil) + return d[0]["vxcJTechnicalServiceUid"].(string), nil } diff --git a/megaport/provider.go b/megaport/provider.go index e494612..b6551de 100644 --- a/megaport/provider.go +++ b/megaport/provider.go @@ -35,7 +35,8 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "megaport_port": resourceMegaportPort(), + "megaport_port": resourceMegaportPort(), + "megaport_private_vxc": resourceMegaportPrivateVxc(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/megaport/resource_megaport_private_vxc.go b/megaport/resource_megaport_private_vxc.go new file mode 100644 index 0000000..a40b5df --- /dev/null +++ b/megaport/resource_megaport_private_vxc.go @@ -0,0 +1,123 @@ +package megaport + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceMegaportPrivateVxc() *schema.Resource { + return &schema.Resource{ + Create: resourceMegaportPrivateVxcCreate, + Read: resourceMegaportPrivateVxcRead, + Update: resourceMegaportPrivateVxcUpdate, + Delete: resourceMegaportPrivateVxcDelete, + + Importer: &schema.ResourceImporter{ + State: resourceMegaportPrivateVxcImportState, + }, + + Schema: map[string]*schema.Schema{ + "product_uid": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "rate_limit": { + Type: schema.TypeInt, + Required: true, + }, + "a_end": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MaxItems: 1, + ConfigMode: schema.SchemaConfigModeAttr, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "vlan": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "untag": { // TODO: what's the key for the API request? + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, + "b_end": { + Type: schema.TypeSet, + Required: true, + MaxItems: 1, + ConfigMode: schema.SchemaConfigModeAttr, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "product_uid": { + Type: schema.TypeString, + Required: true, + }, + "vlan": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + }, + }, + }, + "invoice_reference": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceMegaportPrivateVxcRead(d *schema.ResourceData, m interface{}) error { + return nil +} + +func resourceMegaportPrivateVxcCreate(d *schema.ResourceData, m interface{}) error { + cfg := m.(*Config) + var a, b map[string]interface{} + //if t := d.Get("a_end"); t != nil { + //a = t.(*schema.Set).List()[0].(*schema.ResourceData) + //} + if t := d.Get("b_end").(*schema.Set).List(); t != nil { + //b = (t.(*schema.Set)).List()[0].(map[string]interface) + b = t[0].(map[string]interface{}) + } + var vlanA uint64 + if a != nil { + vlanA = uint64(a["vlan"].(int)) + } + uid, err := cfg.Client.Vxc.Create( + d.Get("product_uid").(string), + b["product_uid"].(string), + d.Get("name").(string), + vlanA, + uint64(b["vlan"].(int)), + uint64(d.Get("rate_limit").(int)), + ) + if err != nil { + return err + } + d.SetId(uid) + return nil +} + +func resourceMegaportPrivateVxcUpdate(d *schema.ResourceData, m interface{}) error { + return nil +} + +func resourceMegaportPrivateVxcDelete(d *schema.ResourceData, m interface{}) error { + return nil +} + +func resourceMegaportPrivateVxcImportState(*schema.ResourceData, interface{}) ([]*schema.ResourceData, error) { + return nil, nil +} From c928b059389a93d341858fb7fce0c2afe5eecf62 Mon Sep 17 00:00:00 2001 From: Dimitrios Karagiannis Date: Mon, 7 Oct 2019 16:46:12 +0100 Subject: [PATCH 2/5] Update megaport api error handling The data returned in case of an error can be either a map or a string (at least that's what's been observed up until now). This change should handle it without crashing. Signed-off-by: Dimitrios Karagiannis --- megaport/api/client.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/megaport/api/client.go b/megaport/api/client.go index 8e29b99..4fab85a 100644 --- a/megaport/api/client.go +++ b/megaport/api/client.go @@ -181,17 +181,23 @@ func (c *Client) do(req *http.Request, data interface{}) error { return ErrNotFound } if resp.StatusCode != http.StatusOK { - r.Data = map[string]interface{}{} if err = parseResponseBody(resp, &r); err != nil { return err } - errData := &strings.Builder{} - for k, v := range r.Data.(map[string]interface{}) { - if _, err := fmt.Fprintf(errData, "%s=%#v ", k, v); err != nil { - return err + switch e := r.Data.(type) { + case string: + return fmt.Errorf("megaport-api: %s (%s)", r.Message, e) + case map[string]interface{}: + errData := &strings.Builder{} + for k, v := range e { + if _, err := fmt.Fprintf(errData, "%s=%#v ", k, v); err != nil { + return err + } } + return fmt.Errorf("megaport-api: %s (%s)", r.Message, strings.TrimSpace(errData.String())) + default: + return fmt.Errorf("megaport-api: %s (cannot process data of type %T: %#v", r.Message, e, e) } - return fmt.Errorf("megaport-api: %s (%s)", r.Message, strings.TrimSpace(errData.String())) } r.Data = data return parseResponseBody(resp, &r) From fc2c9deb7563d3a3f04d332f8dbe0513e658329b Mon Sep 17 00:00:00 2001 From: Dimitrios Karagiannis Date: Mon, 7 Oct 2019 16:50:34 +0100 Subject: [PATCH 3/5] Fix handling of embedded resources Signed-off-by: Dimitrios Karagiannis --- megaport/resource_megaport_port.go | 131 +++++++++++++++-------------- 1 file changed, 70 insertions(+), 61 deletions(-) diff --git a/megaport/resource_megaport_port.go b/megaport/resource_megaport_port.go index 35b9a96..1430e5b 100644 --- a/megaport/resource_megaport_port.go +++ b/megaport/resource_megaport_port.go @@ -47,58 +47,8 @@ func resourceMegaportPort() *schema.Resource { Optional: true, Computed: true, ConfigMode: schema.SchemaConfigModeAttr, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - "rate_limit": { - Type: schema.TypeInt, - Required: true, - }, - "a_end": { - Type: schema.TypeSet, - Optional: true, - Computed: true, - MaxItems: 1, - ConfigMode: schema.SchemaConfigModeAttr, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "vlan": { - Type: schema.TypeInt, - Optional: true, - }, - // TODO: untag? - // TODO: product_uid might be needed for independant? - }, - }, - }, - "b_end": { - Type: schema.TypeSet, - Optional: true, - Computed: true, - MaxItems: 1, - ConfigMode: schema.SchemaConfigModeAttr, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "product_uid": { - Type: schema.TypeString, - Required: true, - }, - "vlan": { - Type: schema.TypeInt, - Optional: true, - }, - }, - }, - }, - "invoice_reference": { - Type: schema.TypeString, - Optional: true, - }, - }, - }, + Elem: vxcResource, + Set: schema.HashResource(vxcResource), }, "marketplace_visibility": { Type: schema.TypeString, @@ -120,6 +70,65 @@ func resourceMegaportPort() *schema.Resource { } } +var ( + vxcResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "rate_limit": { + Type: schema.TypeInt, + Required: true, + }, + "a_end": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MaxItems: 1, + ConfigMode: schema.SchemaConfigModeAttr, + Elem: vxcAEndResource, + }, + "b_end": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MaxItems: 1, + ConfigMode: schema.SchemaConfigModeAttr, + Elem: vxcBEndResource, + }, + "invoice_reference": { + Type: schema.TypeString, + Optional: true, + }, + }, + } + + vxcAEndResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "vlan": { + Type: schema.TypeInt, + Optional: true, + }, + // TODO: untag? + // TODO: product_uid might be needed for independant? + }, + } + + vxcBEndResource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "product_uid": { + Type: schema.TypeString, + Required: true, + }, + "vlan": { + Type: schema.TypeInt, + Optional: true, + }, + }, + } +) + func resourceMegaportPortRead(d *schema.ResourceData, m interface{}) error { cfg := m.(*Config) p, err := cfg.Client.Port.Get(d.Id()) @@ -132,21 +141,21 @@ func resourceMegaportPortRead(d *schema.ResourceData, m interface{}) error { d.Set("name", p.ProductName) d.Set("speed", p.PortSpeed) d.Set("term", p.ContractTermMonths) - vxcs := make([]map[string]interface{}, len(p.AssociatedVxcs)) + vxcs := make([]interface{}, len(p.AssociatedVxcs)) for i, v := range p.AssociatedVxcs { vxcs[i] = map[string]interface{}{ "name": v.ProductName, - "rate_limit": v.RateLimit, - "a_end": map[string]interface{}{ - "vlan": v.AEnd.Vlan, - }, - "b_end": map[string]interface{}{ + "rate_limit": int(v.RateLimit), + "a_end": schema.NewSet(schema.HashResource(vxcAEndResource), []interface{}{map[string]interface{}{ + "vlan": int(v.AEnd.Vlan), + }}), + "b_end": schema.NewSet(schema.HashResource(vxcBEndResource), []interface{}{map[string]interface{}{ "product_uid": v.BEnd.ProductUid, - "vlan": v.BEnd.Vlan, - }, + "vlan": int(v.BEnd.Vlan), + }}), } } - d.Set("associated_vxcs", vxcs) + d.Set("associated_vxcs", schema.NewSet(schema.HashResource(vxcResource), vxcs)) d.Set("marketplace_visibility", p.MarketplaceVisibility) //d.Set("invoice_reference", p.) // TODO: is this even exported? return nil From 3a2554f27bd1731fc86e1e646e5721766e7be05f Mon Sep 17 00:00:00 2001 From: Dimitrios Karagiannis Date: Mon, 7 Oct 2019 16:51:37 +0100 Subject: [PATCH 4/5] Add missing len check and remove comments Signed-off-by: Dimitrios Karagiannis --- megaport/resource_megaport_private_vxc.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/megaport/resource_megaport_private_vxc.go b/megaport/resource_megaport_private_vxc.go index a40b5df..32215a4 100644 --- a/megaport/resource_megaport_private_vxc.go +++ b/megaport/resource_megaport_private_vxc.go @@ -84,11 +84,10 @@ func resourceMegaportPrivateVxcRead(d *schema.ResourceData, m interface{}) error func resourceMegaportPrivateVxcCreate(d *schema.ResourceData, m interface{}) error { cfg := m.(*Config) var a, b map[string]interface{} - //if t := d.Get("a_end"); t != nil { - //a = t.(*schema.Set).List()[0].(*schema.ResourceData) - //} - if t := d.Get("b_end").(*schema.Set).List(); t != nil { - //b = (t.(*schema.Set)).List()[0].(map[string]interface) + if t := d.Get("a_end").(*schema.Set).List(); t != nil && len(t) == 1 { + a = t[0].(map[string]interface{}) + } + if t := d.Get("b_end").(*schema.Set).List(); t != nil && len(t) == 1 { b = t[0].(map[string]interface{}) } var vlanA uint64 From 344abc0303507f8fb5a14341886ff8d0d17edd9b Mon Sep 17 00:00:00 2001 From: Dimitrios Karagiannis Date: Mon, 7 Oct 2019 16:54:12 +0100 Subject: [PATCH 5/5] Add partner_port datasource Which replaces the (sort of useless) megaports datasource Signed-off-by: Dimitrios Karagiannis --- megaport/api/client.go | 4 +- megaport/api/types.go | 1 + megaport/data_source_megaport_location.go | 2 +- megaport/data_source_megaport_megaports.go | 39 ---------- megaport/data_source_megaport_partner_port.go | 75 +++++++++++++++++++ megaport/provider.go | 2 +- 6 files changed, 80 insertions(+), 43 deletions(-) delete mode 100644 megaport/data_source_megaport_megaports.go create mode 100644 megaport/data_source_megaport_partner_port.go diff --git a/megaport/api/client.go b/megaport/api/client.go index 4fab85a..bc926fa 100644 --- a/megaport/api/client.go +++ b/megaport/api/client.go @@ -79,12 +79,12 @@ func (c *Client) GetLocations() ([]*Location, error) { return data, nil } -func (c *Client) GetMegaports() ([]Megaport, error) { +func (c *Client) GetMegaports() ([]*Megaport, error) { req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v2/dropdowns/partner/megaports", c.BaseURL), nil) if err != nil { return nil, err } - data := []Megaport{} + data := []*Megaport{} if err := c.do(req, &data); err != nil { return nil, err } diff --git a/megaport/api/types.go b/megaport/api/types.go index 55cd3d9..b1d0cbf 100644 --- a/megaport/api/types.go +++ b/megaport/api/types.go @@ -66,6 +66,7 @@ type Megaport struct { ProductUid string Rank uint64 Speed uint64 + Title string VxcPermitted bool } diff --git a/megaport/data_source_megaport_location.go b/megaport/data_source_megaport_location.go index 4953710..be08b69 100644 --- a/megaport/data_source_megaport_location.go +++ b/megaport/data_source_megaport_location.go @@ -41,7 +41,7 @@ func dataSourceUpdateLocations(c *api.Client) error { if megaportLocations != nil { return nil } - log.Printf("Updating Megaport location list") + log.Printf("Updating location list") loc, err := c.GetLocations() if err != nil { return err diff --git a/megaport/data_source_megaport_megaports.go b/megaport/data_source_megaport_megaports.go deleted file mode 100644 index 24c409b..0000000 --- a/megaport/data_source_megaport_megaports.go +++ /dev/null @@ -1,39 +0,0 @@ -package megaport - -import ( - "fmt" - "time" - - "github.com/hashicorp/terraform/helper/schema" -) - -func dataSourceMegaportMegaports() *schema.Resource { - return &schema.Resource{ - Read: dataSourceMegaportMegaportsRead, - - Schema: map[string]*schema.Schema{ - "ids": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - }, - } -} - -func dataSourceMegaportMegaportsRead(d *schema.ResourceData, m interface{}) error { - cfg := m.(*Config) - d.SetId(time.Now().UTC().String()) - loc, err := cfg.Client.GetMegaports() - if err != nil { - return err - } - ids := make([]string, len(loc), len(loc)) - for i, v := range loc { - ids[i] = v.ProductUid - } - if err := d.Set("ids", ids); err != nil { - return fmt.Errorf("Error setting Megaport Product UIDs: %s", err) - } - return nil -} diff --git a/megaport/data_source_megaport_partner_port.go b/megaport/data_source_megaport_partner_port.go new file mode 100644 index 0000000..5cea864 --- /dev/null +++ b/megaport/data_source_megaport_partner_port.go @@ -0,0 +1,75 @@ +package megaport + +import ( + "fmt" + "log" + "regexp" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" + "github.com/utilitywarehouse/terraform-provider-megaport/megaport/api" +) + +var ( + megaportPartnerPorts []*api.Megaport +) + +func dataSourceMegaportPartnerPort() *schema.Resource { + return &schema.Resource{ + Read: dataSourceMegaportPartnerPortRead, + + Schema: map[string]*schema.Schema{ + "name_regex": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.ValidateRegexp, + }, + // computed attributes + "uid": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceUpdatePartnerPorts(c *api.Client) error { + megaportMutexKV.Lock("partner_ports") + defer megaportMutexKV.Unlock("partner_ports") + if megaportPartnerPorts != nil { + return nil + } + log.Printf("Updating partner port list") + pp, err := c.GetMegaports() // TODO: rename in api + if err != nil { + return err + } + megaportPartnerPorts = pp + return nil +} + +func dataSourceMegaportPartnerPortRead(d *schema.ResourceData, m interface{}) error { + cfg := m.(*Config) + if err := dataSourceUpdatePartnerPorts(cfg.Client); err != nil { + return err + } + var filtered []*api.Megaport + if nameRegex, ok := d.GetOk("name_regex"); ok { + nr := regexp.MustCompile(nameRegex.(string)) + for _, port := range megaportPartnerPorts { + if nr.MatchString(port.Title) { + filtered = append(filtered, port) + } + } + } + if len(filtered) < 1 { + return fmt.Errorf("No partner ports were found.") + } + if len(filtered) > 1 { + return fmt.Errorf("Multiple partner ports were found. Please use a more specific query.") + } + d.SetId(newUUID(filtered[0].ProductUid)) // TODO: simply use the uuid? + d.Set("uid", filtered[0].ProductUid) + return nil +} diff --git a/megaport/provider.go b/megaport/provider.go index b6551de..51b5cc8 100644 --- a/megaport/provider.go +++ b/megaport/provider.go @@ -41,7 +41,7 @@ func Provider() terraform.ResourceProvider { DataSourcesMap: map[string]*schema.Resource{ "megaport_location": dataSourceMegaportLocation(), - "megaport_megaports": dataSourceMegaportMegaports(), + "megaport_partner_port": dataSourceMegaportPartnerPort(), "megaport_internet_exchanges": dataSourceMegaportInternetExchanges(), },