diff --git a/netbox/client.go b/netbox/client.go index 4269b8d65c39958439d473ccc8240f3c8e60b1c7..11dcf8a347ec434d4b2da5ad42b88165d862dec9 100644 --- a/netbox/client.go +++ b/netbox/client.go @@ -75,7 +75,7 @@ func NewClient(addr string, token string, client *http.Client) (*Client, error) } c.DCIM = NewDCIMService(c) - c.IPAM = &IPAMService{c: c} + c.IPAM = NewIPAMService(c) c.Tenancy = NewTenancyService(c) return c, nil diff --git a/netbox/client_test.go b/netbox/client_test.go index 01c150276213f35ccc7025aa9a3d278daccc78f7..c97d88a6188ff05e8db97e964dd7ca46e1c8808f 100644 --- a/netbox/client_test.go +++ b/netbox/client_test.go @@ -306,3 +306,43 @@ func testTenantWithGroup(n int, t *TenantGroup) *Tenant { Group: t, } } + +func testTenantIdentifier(n int) *Tenant { + return &Tenant{ + ID: n, + Name: fmt.Sprintf("Tenant %d", n), + Slug: fmt.Sprintf("tenant-%d", n), + } +} + +func testVRF(n int) *VRF { + return &VRF{ + ID: n, + Name: fmt.Sprintf("VRF %d", n), + RD: fmt.Sprintf("vrf-%d", n), + EnforceUnique: true, + Description: fmt.Sprintf("VRF %d Description", n), + Tenant: testTenantWithGroup(n, nil), + } +} + +func testVRFCreate(n int) *VRF { + return &VRF{ + Name: fmt.Sprintf("VRF %d", n), + RD: fmt.Sprintf("vrf-%d", n), + EnforceUnique: true, + Description: fmt.Sprintf("VRF %d Description", n), + Tenant: testTenantWithGroup(n, nil), + } +} + +func testVRFWithTenant(n int, tenant *Tenant) *VRF { + return &VRF{ + ID: n, + Name: fmt.Sprintf("VRF %d", n), + RD: fmt.Sprintf("vrf-%d", n), + EnforceUnique: true, + Description: fmt.Sprintf("VRF %d Description", n), + Tenant: tenant, + } +} diff --git a/netbox/ipam.go b/netbox/ipam.go index c61d3fc77a8a4a4b3d9c34bba1bde735680543da..a5abb11e23acac08735f1e81aa7c3466d9e478e8 100644 --- a/netbox/ipam.go +++ b/netbox/ipam.go @@ -16,5 +16,16 @@ package netbox // An IPAMService is used in a Client to access NetBox's IPAM API methods. type IPAMService struct { - c *Client + c *Client + VRFs *VRFsService +} + +// NewIPAMService returns a IPAMService initialized with all sub-services. +func NewIPAMService(client *Client) *IPAMService { + return &IPAMService{ + c: client, + VRFs: &VRFsService{ + c: client, + }, + } } diff --git a/netbox/ipam_vrfs.go b/netbox/ipam_vrfs.go new file mode 100644 index 0000000000000000000000000000000000000000..7524da87f9300d86ea6cacc7e91da5276d1de0d1 --- /dev/null +++ b/netbox/ipam_vrfs.go @@ -0,0 +1,118 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed 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. + +// Code generated by generate_functions.go. DO NOT EDIT. + +package netbox + +import ( + "encoding/json" + "fmt" + "net/http" +) + +// VRFsService is used in a Client to access NetBox's ipam/vrfs API methods. +type VRFsService struct { + c *Client +} + +// Get retrieves an VRF object from NetBox by its ID. +func (s *VRFsService) Get(id int) (*VRF, error) { + req, err := s.c.NewRequest( + http.MethodGet, + fmt.Sprintf("api/ipam/vrfs/%d/", id), + nil, + ) + if err != nil { + return nil, err + } + + t := new(VRF) + err = s.c.Do(req, t) + if err != nil { + return nil, err + } + return t, nil +} + +// List returns a Page associated with an NetBox API Endpoint. +func (s *VRFsService) List(options *ListVRFOptions) *Page { + return NewPage(s.c, "api/ipam/vrfs/", options) +} + +// Extract retrives a list of VRF objects from page. +func (s *VRFsService) Extract(page *Page) ([]*VRF, error) { + if err := page.Err(); err != nil { + return nil, err + } + + var groups []*VRF + if err := json.Unmarshal(page.data.Results, &groups); err != nil { + return nil, err + } + return groups, nil +} + +// Create creates a new VRF object in NetBox and returns the ID of the new object. +func (s *VRFsService) Create(data *VRF) (int, error) { + req, err := s.c.NewJSONRequest(http.MethodPost, "api/ipam/vrfs/", nil, data) + if err != nil { + return 0, err + } + + g := new(writableVRF) + err = s.c.Do(req, g) + if err != nil { + return 0, err + } + return g.ID, nil +} + +// Update changes an existing VRF object in NetBox, and returns the ID of the new object. +func (s *VRFsService) Update(data *VRF) (int, error) { + req, err := s.c.NewJSONRequest( + http.MethodPatch, + fmt.Sprintf("api/ipam/vrfs/%d/", data.ID), + nil, + data, + ) + if err != nil { + return 0, err + } + + // g is just used to verify correct api result. + // data is not changed, because the g is not the full representation that one would + // get with Get. But if the response was unmarshaled into writableVRF correctly, + // everything went fine, and we do not need to update data. + g := new(writableVRF) + err = s.c.Do(req, g) + if err != nil { + return 0, err + } + return g.ID, nil +} + +// Delete deletes an existing VRF object from NetBox. +func (s *VRFsService) Delete(data *VRF) error { + req, err := s.c.NewRequest( + http.MethodDelete, + fmt.Sprintf("api/ipam/vrfs/%d/", data.ID), + nil, + ) + if err != nil { + return err + } + + return s.c.Do(req, nil) +} diff --git a/netbox/ipam_vrfs_basic_test.go b/netbox/ipam_vrfs_basic_test.go new file mode 100644 index 0000000000000000000000000000000000000000..101530e60a90cc9b54b7a6b2083ea1fe175d9fa1 --- /dev/null +++ b/netbox/ipam_vrfs_basic_test.go @@ -0,0 +1,299 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed 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. + +// Code generated by generate_basic_tests.go. DO NOT EDIT. + +package netbox + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "reflect" + "testing" +) + +// Using this to override MarshalJSON +// In all cases when posting data to netbox-API, the VRF.MarshalJSON is what you want, +// but not here as a return in testHandler +type serverDataVRF VRF + +func convertToServerDataVRF(data []*VRF) []*serverDataVRF { + dataWant := make([]*serverDataVRF, len(data)) + for i := range data { + tmp := serverDataVRF(*data[i]) + dataWant[i] = &tmp + } + return dataWant +} + +func TestBasicVRFGet(t *testing.T) { + var tests = []struct { + desc string + want *VRF + }{ + { + desc: "Simple VRF", + want: testVRF(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverDataVRF(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vrfs/1/", &serverData)) + defer done() + + res, err := c.IPAM.VRFs.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.IPAM.VRFs.Get: %v", err) + } + + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected VRF:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicVRFGet404(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/ipam/vrfs/1/", &struct { + Detail string `json:"detail"` + }{ + Detail: "Not found.", + }, + http.StatusNotFound)) + defer done() + + res, err := c.IPAM.VRFs.Get(1) + errstr := "404 - Not found." + if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error from Client.IPAM.VRFs.Get:\n- want: %v\n- got: %v", want, got) + } + + if res != nil { + t.Fatalf("unexpected result:\n- want: %v\n- got: %v", nil, res) + } +} + +func TestBasicListExtractVRF(t *testing.T) { + want := []*VRF{ + testVRF(1), + testVRF(2), + } + serverWant := convertToServerDataVRF(want) + serverData, _ := json.Marshal(serverWant) + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vrfs/", &pageData{ + Count: 2, + NextURL: "", + PreviousURL: "", + Results: serverData, + })) + defer done() + + page := c.IPAM.VRFs.List(nil) + + if page == nil { + t.Fatalf("unexpexted result from c.IPAM.VRFs.List.") + } + + got := []*VRF{} + counter := 0 + for page.Next() { + var err error + got, err = c.IPAM.VRFs.Extract(page) + if err != nil { + t.Fatalf("unexpected error from c.IPAM.VRFs.Extract: %v", err) + } + counter = counter + 1 + if counter > 2 { // Safe guard + break + } + } + if counter != 1 { + t.Fatalf("unexpected page count:\n- want: 1\n- got: %d", counter) + } + + if !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected result:\n- want: %v\n- got: %v", want, got) + } + + if page.Err() != nil { + t.Fatalf("unexpected error from page:\n- want: %v\n- got: %v", want, got) + } +} + +func TestBasicCreateVRF(t *testing.T) { + var tests = []struct { + desc string + data *VRF + want int + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 0", + data: testVRFCreate(1), + want: 1, + status: 0, + errstr: "", + serverData: testVRF(1), + }, + { + desc: "Create duplicate", + data: testVRFCreate(1), + want: 0, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"IpamVRFService with this name already exists.\"]}\n", + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"IpamVRFService with this name already exists."}, + }, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodPost, "/api/ipam/vrfs/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + res, err := c.IPAM.VRFs.Create(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected VRF:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicUpdateVRF(t *testing.T) { + var tests = []struct { + desc string + data *VRF + want int + serverData interface{} + status int + errstr string + }{ + { + desc: "Update with ID 1", + data: testVRF(1), + want: 1, + serverData: testVRF(1), + status: 0, + errstr: "", + }, + { + desc: "Update not found", + data: testVRF(1), + want: 0, + serverData: &struct { + Detail string + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + { + desc: "Update to duplicate", + data: testVRF(1), + want: 0, + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"IpamVRFService with this name already exists."}, + }, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"IpamVRFService with this name already exists.\"]}\n", + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodPatch, "/api/ipam/vrfs/1/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + res, err := c.IPAM.VRFs.Update(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected VRF:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicDeleteVRF(t *testing.T) { + var tests = []struct { + desc string + data *VRF + serverData interface{} + status int + errstr string + }{ + { + desc: "Delete ID 1", + data: testVRF(1), + serverData: testVRF(1), + status: 0, + errstr: "", + }, + { + desc: "Delete not Found", + data: testVRF(1), + serverData: &struct { + Detail string `json:"detail"` + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodDelete, "/api/ipam/vrfs/1/", tt.serverData, tt.status)) + defer done() + + var terr error + if tt.errstr != "" { + terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint + } + + err := c.IPAM.VRFs.Delete(tt.data) + if want, got := terr, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + }) + } +} diff --git a/netbox/ipam_vrfs_test.go b/netbox/ipam_vrfs_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3d00c622c2ffa1128e72f77256e7cc05e0c7e40e --- /dev/null +++ b/netbox/ipam_vrfs_test.go @@ -0,0 +1,217 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed 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 netbox + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "reflect" + "testing" +) + +func TestVRFGet(t *testing.T) { + var tests = []struct { + desc string + want *VRF + }{ + { + desc: "Without Tenant", + want: testVRFWithTenant(1, nil), + }, + { + desc: "With Tenant", + want: testVRFWithTenant(1, testTenantWithGroup(1, nil)), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverDataVRF(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vrfs/1/", &serverData)) + defer done() + + res, err := c.IPAM.VRFs.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.IPAM.VRFs.Get: %v", err) + } + + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected Tenant:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestVRFUnmarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data []byte + want *VRF + }{ + { + desc: "Nil Group", + data: []byte(`{ "id": 1, "name": "VRF 1", "rd": "vrf-1", "tenant": null, "enforce_unique": true, "description": "VRF 1 Description", "custom_fields": {} }`), + want: testVRFWithTenant(1, nil), + }, + { + desc: "With Group", + data: []byte(`{ "id": 1, "name": "VRF 1", "rd": "vrf-1", "tenant": { "id": 1, "name": "Tenant 1", "slug": "tenant-1" }, "enforce_unique": true, "description": "VRF 1 Description", "custom_fields": {} }`), + want: testVRFWithTenant(1, testTenantIdentifier(1)), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + result := new(VRF) + err := json.Unmarshal(tt.data, result) + if err != nil { + t.Fatalf("unexpected error from Tenant.UnmarshalJSON: %v", err) + } + + if want, got := tt.want, result; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected Tenant:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestVRFMarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data *VRF + want []byte + }{ + { + desc: "Nil Group", + data: testVRFWithTenant(1, nil), + want: []byte(`{"id":1,"name":"VRF 1","rd":"vrf-1","enforce_unique":true,"description":"VRF 1 Description"}`), + }, + { + desc: "With Group", + data: testVRFWithTenant(1, testTenantIdentifier(1)), + want: []byte(`{"id":1,"name":"VRF 1","rd":"vrf-1","enforce_unique":true,"description":"VRF 1 Description","tenant":1}`), + }, + { + desc: "No VRF.ID", + data: testVRFWithTenant(0, nil), + want: []byte(`{"name":"VRF 0","rd":"vrf-0","enforce_unique":true,"description":"VRF 0 Description"}`), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + result, err := json.Marshal(tt.data) + if err != nil { + t.Fatalf("unexpected error from VRF.MarshalJSON: %v", err) + } + + if want, got := tt.want, result; bytes.Compare(want, got) != 0 { + t.Fatalf("unexpected VRF:\n- want: %s\n- got: %s", want, got) + } + }) + } +} + +func TestListVRFOptions(t *testing.T) { + enforceUniqueTrue := true + enforceUniqueFalse := false + var tests = []struct { + desc string + o *ListVRFOptions + v url.Values + }{ + { + desc: "empty options", + }, + { + desc: "full options", + o: &ListVRFOptions{ + Name: "Hello", + RD: "hello", + EnforceUnique: &enforceUniqueTrue, + IDIn: []uint64{1, 2, 3}, + TenantID: 1, + Query: "World", + }, + v: url.Values{ + "name": []string{"Hello"}, + "rd": []string{"hello"}, + "id__in": []string{"1,2,3"}, + "enforce_unique": []string{"True"}, + "tenant_id": []string{"1"}, + "q": []string{"World"}, + }, + }, + { + desc: "tenant vs tenant_id", + o: &ListVRFOptions{ + TenantID: 1, + Tenant: "Tenant 1", + }, + v: url.Values{ + "tenant_id": []string{"1"}, + }, + }, + { + desc: "tenant name", + o: &ListVRFOptions{ + Tenant: "Tenant 1", + }, + v: url.Values{ + "tenant": []string{"Tenant 1"}, + }, + }, + { + desc: "enforce_unique default value", + o: &ListVRFOptions{}, + v: url.Values{}, + }, + { + desc: "enforce_unique true", + o: &ListVRFOptions{ + EnforceUnique: &enforceUniqueTrue, + }, + v: url.Values{ + "enforce_unique": []string{"True"}, + }, + }, + { + desc: "enforce_unique false", + o: &ListVRFOptions{ + EnforceUnique: &enforceUniqueFalse, + }, + v: url.Values{ + "enforce_unique": []string{"False"}, + }, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + v, err := tt.o.Values() + if err != nil { + t.Fatalf("unexpected Values error: %v", err) + } + + if want, got := tt.v, v; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected url.Values map:\n- want: %v\n- got: %v", want, got) + } + }) + } +} diff --git a/netbox/ipam_vrfs_types.go b/netbox/ipam_vrfs_types.go new file mode 100644 index 0000000000000000000000000000000000000000..f699efc851cedb6a57d29c7cd181db976594a602 --- /dev/null +++ b/netbox/ipam_vrfs_types.go @@ -0,0 +1,121 @@ +// Copyright 2017 The go-netbox Authors. +// +// Licensed 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 netbox + +import ( + "encoding/json" + "net/url" + "strconv" + "strings" +) + +// A VRF is a representation of netbox vrf +type VRF struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + RD string `json:"rd"` + EnforceUnique bool `json:"enforce_unique"` + Description string `json:"description"` + Tenant *Tenant `json:"tenant,omitempty"` +} + +// A writableVRF corresponds to the Netbox API's +// writable serializer for an VRF. It is used transparently +// when VRF are serialized in to JSON. +type writableVRF struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + RD string `json:"rd"` + EnforceUnique bool `json:"enforce_unique"` + Description string `json:"description"` + Tenant *int `json:"tenant,omitempty"` +} + +// MarshalJSON marshals an VRF into JSON bytes, +// and is used by the standard json package. +func (v *VRF) MarshalJSON() ([]byte, error) { + var tenantID *int + if v.Tenant != nil { + tenantID = &v.Tenant.ID + } + return json.Marshal(writableVRF{ + ID: v.ID, + Name: v.Name, + RD: v.RD, + EnforceUnique: v.EnforceUnique, + Description: v.Description, + Tenant: tenantID, + }) +} + +// ListVRFOptions is used as an argument for Client.IPAM.VRFs.List. +type ListVRFOptions struct { + Name string + RD string + EnforceUnique *bool + IDIn []uint64 + TenantID int + Tenant string + Query string +} + +// Values generates a url.Values map from the data in ListVRFOptions. +func (o *ListVRFOptions) Values() (url.Values, error) { + if o == nil { + return nil, nil + } + + v := url.Values{} + + if o.Name != "" { + v.Set("name", o.Name) + } + + if o.RD != "" { + v.Set("rd", o.RD) + } + + if o.EnforceUnique != nil { + if *o.EnforceUnique { + v.Set("enforce_unique", "True") + } else { + v.Set("enforce_unique", "False") + } + } + + if len(o.IDIn) > 0 { + vals := make([]string, len(o.IDIn)) + for i, k := range o.IDIn { + vals[i] = strconv.FormatUint(k, 10) + } + v.Set("id__in", strings.Join(vals, ",")) + } + + switch { + case o.TenantID != 0: + v.Set("tenant_id", strconv.Itoa(o.TenantID)) + case o.Tenant != "": + v.Set("tenant", o.Tenant) + } + + if o.Query != "" { + v.Set("q", o.Query) + } + + return v, nil +} + +//go:generate go run generate_functions.go -type-name VRF -update-type-name writableVRF -service-name VRFsService -endpoint ipam -service vrfs +//go:generate go run generate_basic_tests.go -type-name VRF -service-name IpamVRFService -endpoint ipam -service vrfs -client-endpoint IPAM -client-service VRFs