diff --git a/.travis.yml b/.travis.yml index 0112c2173a0f131d6d65d80ea0c428ebcf3698ab..28d41082267ceefa2ab179666d9389d6640015a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: - - 1.6.2 + - 1.x before_install: - go get github.com/golang/lint/golint before_script: diff --git a/README.md b/README.md index e80e3a6f242d2281692cd94392113cb212d13039..93027bfd4be180eb8fbfb0bef38a02baa5cdcb4e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ netbox [](http://godoc.org/github.com/digitalocean/go-netbox) [](https://travis-ci.org/digitalocean/go-netbox) [](https://goreportcard.com/report/github.com/digitalocean/go-netbox) ====== -**Note**: the existing code is "frozen" and will be updated when NetBox API -2.0 is released. - -Package `netbox` provides an API client for [DigitalOcean's NetBox](https://github.com/digitalocean/netbox) +Package `netbox` provides an API 2.0 client for [DigitalOcean's NetBox](https://github.com/digitalocean/netbox) IPAM and DCIM service. + +This package assumes you are using NetBox 2.0, as the NetBox 1.0 API no longer exists. diff --git a/asn.go b/asn.go deleted file mode 100644 index 7f84641a35ab744604f2ef3432d84004ce79856b..0000000000000000000000000000000000000000 --- a/asn.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2016 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 - -// An ASN is an Autonomous System Number, used to designate that a collection of -// Internet Protocol routing prefixes are under the control of one or more -// network operators. -type ASN int - -// Reserved and private ASN ranges, used in filters below. -// Reference: http://www.iana.org/assignments/as-numbers/as-numbers.xhtml. -const ( - reservedStart16Bit = 0 - reservedMin16Bit = 64297 - reservedMax16Bit = 64495 - reservedDocMin16Bit = 64496 - reservedDocMax16Bit = 64511 - reservedEnd16Bit = 65535 - - reservedDocMin32Bit = 65536 - reservedDocMax32Bit = 65551 - reservedMin32Bit = 65552 - reservedMax32Bit = 131071 - reservedEnd32Bit = 4294967295 - - privateMin16Bit = 64512 - privateMax16Bit = 65534 - - privateMin32Bit = 4200000000 - privateMax32Bit = 4294967294 - - // RFC 6793, Section 9, AS_TRANS, reserved - asTrans = 23456 -) - -// Private determines if an ASN resides within a private range. -func (a ASN) Private() bool { - switch { - case a >= privateMin16Bit && a <= privateMax16Bit: - return true - case a >= privateMin32Bit && a <= privateMax32Bit: - return true - default: - return false - } -} - -// Public determines if an ASN does not reside within a private or reserved -// range. -func (a ASN) Public() bool { - if !a.Valid() { - return false - } - - return !a.Private() && !a.Reserved() -} - -// Reserved determines if an ASN resides within a reserved range. -func (a ASN) Reserved() bool { - switch { - case a == reservedStart16Bit: - return true - case a == asTrans: - return true - case a >= reservedMin16Bit && a <= reservedMax16Bit: - return true - case a >= reservedDocMin16Bit && a <= reservedDocMax16Bit: - return true - case a == reservedEnd16Bit: - return true - case a >= reservedDocMin32Bit && a <= reservedDocMax32Bit: - return true - case a >= reservedMin32Bit && a <= reservedMax32Bit: - return true - case a == reservedEnd32Bit: - return true - default: - return false - } -} - -// Valid determines if an ASN is valid. -func (a ASN) Valid() bool { - return a >= reservedStart16Bit && a <= reservedEnd32Bit -} diff --git a/asn_test.go b/asn_test.go deleted file mode 100644 index 118406f514128021140355f21483d292a6b852e9..0000000000000000000000000000000000000000 --- a/asn_test.go +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2016 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 ( - "math" - "testing" -) - -func TestASNPrivate(t *testing.T) { - var tests = []struct { - a ASN - ok bool - }{ - { - a: 0, - }, - { - a: math.MinInt64, - }, - { - a: math.MaxInt64, - }, - { - a: privateMin16Bit, - ok: true, - }, - { - a: privateMin16Bit + 1, - ok: true, - }, - { - a: privateMax16Bit - 1, - ok: true, - }, - { - a: privateMax16Bit, - ok: true, - }, - { - a: privateMin32Bit, - ok: true, - }, - { - a: privateMax32Bit, - ok: true, - }, - } - - for _, tt := range tests { - if want, got := tt.ok, tt.a.Private(); want != got { - t.Fatalf("unexpected ASN(%d).Private():\n- want: %v\n- got: %v", - tt.a, want, got) - } - } -} - -func TestASNPublic(t *testing.T) { - var tests = []struct { - a ASN - ok bool - }{ - { - a: math.MinInt64, - }, - { - a: math.MaxInt64, - }, - { - a: privateMin16Bit, - }, - { - a: privateMin16Bit + 1, - }, - { - a: privateMax16Bit - 1, - }, - { - a: privateMax16Bit, - }, - { - a: privateMin32Bit, - }, - { - a: privateMax32Bit, - }, - { - a: reservedStart16Bit, - }, - { - a: reservedMin16Bit, - }, - { - a: reservedMin16Bit + 1, - }, - { - a: reservedMax16Bit - 1, - }, - { - a: reservedMax16Bit, - }, - { - a: reservedDocMin16Bit, - }, - { - a: reservedDocMin16Bit + 1, - }, - { - a: reservedDocMax16Bit - 1, - }, - { - a: reservedDocMax16Bit, - }, - { - a: reservedEnd16Bit, - }, - { - a: reservedDocMin32Bit, - }, - { - a: reservedDocMin32Bit + 1, - }, - { - a: reservedDocMax32Bit - 1, - }, - { - a: reservedDocMax32Bit, - }, - { - a: reservedMin32Bit, - }, - { - a: reservedMin32Bit + 1, - }, - { - a: reservedMax32Bit - 1, - }, - { - a: reservedMax32Bit, - }, - { - a: reservedEnd32Bit, - }, - { - a: asTrans, - }, - { - a: reservedStart16Bit + 1, - ok: true, - }, - { - a: reservedMin16Bit - 1, - ok: true, - }, - { - a: reservedMax32Bit + 1, - ok: true, - }, - { - a: privateMin32Bit - 1, - ok: true, - }, - } - - for _, tt := range tests { - if want, got := tt.ok, tt.a.Public(); want != got { - t.Fatalf("unexpected ASN(%d).Public():\n- want: %v\n- got: %v", - tt.a, want, got) - } - } -} - -func TestASNReserved(t *testing.T) { - var tests = []struct { - a ASN - ok bool - }{ - { - a: math.MinInt64, - }, - { - a: math.MaxInt64, - }, - { - a: reservedStart16Bit, - ok: true, - }, - { - a: reservedMin16Bit, - ok: true, - }, - { - a: reservedMin16Bit + 1, - ok: true, - }, - { - a: reservedMax16Bit - 1, - ok: true, - }, - { - a: reservedMax16Bit, - ok: true, - }, - { - a: reservedDocMin16Bit, - ok: true, - }, - { - a: reservedDocMin16Bit + 1, - ok: true, - }, - { - a: reservedDocMax16Bit - 1, - ok: true, - }, - { - a: reservedDocMax16Bit, - ok: true, - }, - { - a: reservedEnd16Bit, - ok: true, - }, - { - a: reservedDocMin32Bit, - ok: true, - }, - { - a: reservedDocMin32Bit + 1, - ok: true, - }, - { - a: reservedDocMax32Bit - 1, - ok: true, - }, - { - a: reservedDocMax32Bit, - ok: true, - }, - { - a: reservedMin32Bit, - ok: true, - }, - { - a: reservedMin32Bit + 1, - ok: true, - }, - { - a: reservedMax32Bit - 1, - ok: true, - }, - { - a: reservedMax32Bit, - ok: true, - }, - { - a: reservedEnd32Bit, - ok: true, - }, - { - a: asTrans, - ok: true, - }, - } - - for _, tt := range tests { - if want, got := tt.ok, tt.a.Reserved(); want != got { - t.Fatalf("unexpected ASN(%d).Reserved():\n- want: %v\n- got: %v", - tt.a, want, got) - } - } -} diff --git a/client.go b/client.go deleted file mode 100644 index 6e1f36f2ee20086103803e55401f8fe89b5bad18..0000000000000000000000000000000000000000 --- a/client.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2016 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/http" - "net/url" - "path" -) - -// A Client is a NetBox client. It can be used to retrieve network and -// datacenter infrastructure information from a NetBox server. -type Client struct { - // DCIM provides access to methods in NetBox's DCIM API. - DCIM *DCIMService - - // IPAM provides access to methods in NetBox's IPAM API. - IPAM *IPAMService - - u *url.URL - client *http.Client -} - -// NewClient returns a new instance of a NetBox client. addr specifies the address -// of the NetBox server, and client specifies an optional HTTP client to use -// for requests. -// -// If client is nil, a default HTTP client will be used. -func NewClient(addr string, client *http.Client) (*Client, error) { - if client == nil { - client = &http.Client{} - } - - u, err := url.Parse(addr) - if err != nil { - return nil, err - } - - c := &Client{ - u: u, - client: client, - } - - c.DCIM = &DCIMService{c: c} - c.IPAM = &IPAMService{c: c} - - return c, nil -} - -// NewRequest creates a HTTP request using the input HTTP method, URL -// endpoint, and a Valuer which creates URL parameters for the request. -// -// If a nil Valuer is specified, no query parameters will be sent with the -// request. -func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) { - rel, err := url.Parse(endpoint) - if err != nil { - return nil, err - } - - // Allow specifying a base path for API requests, so if a NetBox server - // resides at a path like http://example.com/netbox/, API requests will - // be sent to http://example.com/netbox/api/... - // - // Enables support of: https://github.com/digitalocean/netbox/issues/212. - if c.u.Path != "" { - rel.Path = path.Join(c.u.Path, rel.Path) - } - - u := c.u.ResolveReference(rel) - - // If no valuer specified, create a request with no query parameters - if options == nil { - return http.NewRequest(method, u.String(), nil) - } - - values, err := options.Values() - if err != nil { - return nil, err - } - u.RawQuery = values.Encode() - - return http.NewRequest(method, u.String(), nil) -} - -// Do executes an HTTP request and if v is not nil, Do unmarshals result -// JSON onto v. -func (c *Client) Do(req *http.Request, v interface{}) error { - res, err := c.client.Do(req) - if err != nil { - return err - } - defer func() { - _ = res.Body.Close() - }() - - if v == nil { - return nil - } - - return json.NewDecoder(res.Body).Decode(v) -} diff --git a/client_test.go b/client_test.go deleted file mode 100644 index d94fb981d643904563666e700786182222cd1cb6..0000000000000000000000000000000000000000 --- a/client_test.go +++ /dev/null @@ -1,486 +0,0 @@ -// Copyright 2016 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" - "fmt" - "net" - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "testing" - "time" -) - -func TestClientBadJSON(t *testing.T) { - c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("foo")) - }) - defer done() - - req, err := c.NewRequest(http.MethodGet, "/", nil) - if err != nil { - t.Fatal("expected an error, but no error returned") - } - - // Pass empty struct to trigger JSON unmarshaling path - var v struct{} - - err = c.Do(req, &v) - if _, ok := err.(*json.SyntaxError); !ok { - t.Fatalf("unexpected error type: %T", err) - } -} - -func TestClientQueryParameters(t *testing.T) { - c := &Client{ - u: &url.URL{}, - client: &http.Client{}, - } - - const ( - wantFoo = "foo" - wantBar = 1 - ) - - req, err := c.NewRequest(http.MethodGet, "/", testValuer{ - Foo: wantFoo, - Bar: wantBar, - }) - if err != nil { - t.Fatal("expected an error, but no error returned") - } - - q := req.URL.Query() - if want, got := 2, len(q); want != got { - t.Fatalf("unexpected number of query parameters:\n- want: %v\n- got: %v", - want, got) - } - - if want, got := wantFoo, q.Get("foo"); want != got { - t.Fatalf("unexpected foo:\n- want: %v\n- got: %v", want, got) - } - - if want, got := strconv.Itoa(wantBar), q.Get("bar"); want != got { - t.Fatalf("unexpected bar:\n- want: %v\n- got: %v", want, got) - } -} - -func TestClientPrependBaseURLPath(t *testing.T) { - u, err := url.Parse("http://example.com/netbox/") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - c := &Client{ - u: u, - client: &http.Client{}, - } - - req, err := c.NewRequest(http.MethodGet, "/api/ipam/vlans", nil) - if err != nil { - t.Fatal("expected an error, but no error returned") - } - - if want, got := "/netbox/api/ipam/vlans", req.URL.Path; want != got { - t.Fatalf("unexpected URL path:\n- want: %q\n- got: %q", - want, got) - } -} - -type testValuer struct { - Foo string - Bar int -} - -func (q testValuer) Values() (url.Values, error) { - v := url.Values{} - - if q.Foo != "" { - v.Set("foo", q.Foo) - } - - if q.Bar != 0 { - v.Set("bar", strconv.Itoa(q.Bar)) - } - - return v, nil -} - -func testClient(t *testing.T, fn func(w http.ResponseWriter, r *http.Request)) (*Client, func()) { - s := httptest.NewServer(http.HandlerFunc(fn)) - - c, err := NewClient(s.URL, nil) - if err != nil { - t.Fatalf("error creating Client: %v", err) - } - - return c, func() { s.Close() } -} - -func testHandler(t *testing.T, method string, path string, v interface{}) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if want, got := method, r.Method; want != got { - t.Fatalf("unexpected HTTP method:\n- want: %v\n- got: %v", want, got) - } - - if want, got := path, r.URL.Path; want != got { - t.Fatalf("unexpected URL path:\n- want: %v\n- got: %v", want, got) - } - - if err := json.NewEncoder(w).Encode(v); err != nil { - t.Fatalf("error while encoding JSON: %v", err) - } - } -} - -// Test helpers for generating mock data - -func testAggregate(family Family, n int) *Aggregate { - prefix := &net.IPNet{ - IP: net.IPv4(8, 0, 0, 0), - Mask: net.CIDRMask(8, 32), - } - if family == FamilyIPv6 { - prefix = &net.IPNet{ - IP: net.ParseIP("2001::"), - Mask: net.CIDRMask(16, 128), - } - } - - return &Aggregate{ - ID: n, - Family: family, - Prefix: prefix, - RIR: testRIRIdentifier(n), - DateAdded: time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC), - Description: fmt.Sprintf("description %d", n), - } -} - -func testConsolePort(n int) *ConsolePort { - return &ConsolePort{ - ID: n, - Device: testDeviceIdentifier(n), - Name: fmt.Sprintf("deviceport %d", n), - CSPort: testConsoleServerPort(n), - ConnectionStatus: true, - } -} - -func testConsolePortIdentifier(n int) *ConsolePortIdentifier { - return &ConsolePortIdentifier{ - Device: fmt.Sprintf("device %d", n), - Name: fmt.Sprintf("rc console port %d", n), - Port: fmt.Sprintf("port %d", n), - } -} - -func testConsoleServerPort(n int) *ConsoleServerPort { - return &ConsoleServerPort{ - ID: n, - Device: testDeviceIdentifier(n), - Name: fmt.Sprintf("consoleserverport %d", n), - } -} - -func testDevice(n int) *Device { - return &Device{ - ID: n, - Name: fmt.Sprintf("device %d", n), - DisplayName: fmt.Sprintf("Device %d", n), - DeviceType: testDeviceTypeIdentifier(n), - DeviceRole: testSimpleIdentifier(n), - Platform: testSimpleIdentifier(n), - Serial: fmt.Sprintf("relatedconnection%d", n), - Rack: testRackIdentifier(n), - Position: n, - Face: n, - ParentDevice: testDeviceIdentifier(n), - Status: true, - PrimaryIP: testIPAddressIdentifier(FamilyIPv4, n), - PrimaryIP4: testIPAddressIdentifier(FamilyIPv4, n), - PrimaryIP6: testIPAddressIdentifier(FamilyIPv6, n), - Comments: "", - } -} - -func testDeviceIdentifier(n int) *DeviceIdentifier { - return &DeviceIdentifier{ - ID: n, - Name: fmt.Sprintf("DeviceIdentifier %d", n), - } -} - -func testDeviceTypeIdentifier(n int) *DeviceTypeIdentifier { - return &DeviceTypeIdentifier{ - ID: n, - Manufacturer: testSimpleIdentifier(n), - Model: fmt.Sprintf("device model %d", n), - Slug: fmt.Sprintf("devicetype%d", n), - } -} - -func testInterface(n int) *Interface { - return &Interface{ - ID: n, - Name: fmt.Sprintf("interface %d", n), - FormFactor: fmt.Sprintf("form factor %d", n), - MacAddress: fmt.Sprintf("f4:9d:82:9e:34:c%d", n), - MgmtOnly: true, - Description: fmt.Sprintf("Description %d", n), - IsConnected: true, - ConnectedInterface: testInterfaceDetail(n), - } -} - -func testInterfaceDetail(n int) *InterfaceDetail { - return &InterfaceDetail{ - ID: n, - Device: testDeviceIdentifier(n), - Name: fmt.Sprintf("interfacedetail %d", n), - FormFactor: fmt.Sprintf("form factor %d", n), - MacAddress: fmt.Sprintf("f4:9d:82:9e:34:c%d", n), - MgmtOnly: true, - Description: fmt.Sprintf("Description %d", n), - IsConnected: true, - } -} - -func testInterfaceIdentifier(n int) *InterfaceIdentifier { - return &InterfaceIdentifier{ - ID: n, - Device: testDeviceIdentifier(n), - Name: fmt.Sprintf("InterfaceIdentifier %d", n), - } -} - -func testIPAddress(family Family, n int) *IPAddress { - address := &net.IPNet{ - IP: net.IPv4(8, 8, 8, 0), - Mask: net.CIDRMask(24, 32), - } - if family == FamilyIPv6 { - address = &net.IPNet{ - IP: net.ParseIP("2001:4860:4860::"), - Mask: net.CIDRMask(48, 128), - } - } - - return &IPAddress{ - ID: n, - Family: family, - Address: address, - VRF: testVRFIdentifier(n), - Interface: testInterfaceIdentifier(n), - Description: fmt.Sprintf("description %d", n), - NATInside: testIPAddressIdentifier(family, n), - NATOutside: testIPAddressIdentifier(family, n), - } -} - -func testIPAddressIdentifier(family Family, n int) *IPAddressIdentifier { - address := &net.IPNet{ - IP: net.IPv4(8, 8, 8, 0), - Mask: net.CIDRMask(24, 32), - } - if family == FamilyIPv6 { - address = &net.IPNet{ - IP: net.ParseIP("2001:4860:4860::"), - Mask: net.CIDRMask(48, 128), - } - } - - return &IPAddressIdentifier{ - ID: n, - Family: family, - Address: address, - } -} - -func testPrefix(family Family, n int) *Prefix { - prefix := &net.IPNet{ - IP: net.IPv4(8, 8, 0, 0), - Mask: net.CIDRMask(16, 32), - } - if family == FamilyIPv6 { - prefix = &net.IPNet{ - IP: net.ParseIP("2001:4860::"), - Mask: net.CIDRMask(32, 128), - } - } - - return &Prefix{ - ID: n, - Family: family, - Prefix: prefix, - Site: testSiteIdentifier(n), - VRF: testVRFIdentifier(n), - VLAN: testVLANIdentifier(n), - Status: StatusActive, - Role: testRoleIdentifier(n), - Description: fmt.Sprintf("description %d", n), - } -} - -func testRackIdentifier(n int) *RackIdentifier { - return &RackIdentifier{ - ID: n, - Name: fmt.Sprintf("rack %d", n), - FacilityID: fmt.Sprintf("facility%d", n), - DisplayName: fmt.Sprintf("Rack %d", n), - } -} - -func testPowerOutlet(n int) *PowerOutletIdentifier { - return &PowerOutletIdentifier{ - ID: n, - Device: testDeviceIdentifier(n), - Name: fmt.Sprintf("poweroutlet %d", n), - } -} - -func testPowerPort(n int) *PowerPort { - return &PowerPort{ - ID: n, - Name: fmt.Sprintf("powerport %d", n), - PowerOutlet: testPowerOutlet(n), - ConnectionStatus: true, - } -} - -func testPowerPortIdentifier(n int) *PowerPortIdentifier { - return &PowerPortIdentifier{ - Device: fmt.Sprintf("device %d", n), - Name: fmt.Sprintf("rc power port %d", n), - Outlet: fmt.Sprintf("outlet %d", n), - } -} - -func testRelatedConnection(n int) *RelatedConnection { - return &RelatedConnection{ - Device: testDevice(n), - ConsolePorts: []*ConsolePort{testConsolePort(n)}, - Interfaces: []*Interface{testInterface(n)}, - PowerPorts: []*PowerPort{testPowerPort(n)}, - } -} - -func testRIR(n int) *RIR { - return &RIR{ - ID: n, - Name: fmt.Sprintf("RIR %d", n), - Slug: fmt.Sprintf("rir%d", n), - } -} - -func testRIRIdentifier(n int) *RIRIdentifier { - return &RIRIdentifier{ - ID: n, - Name: fmt.Sprintf("RIRIdentifier %d", n), - Slug: fmt.Sprintf("riridentifier%d", n), - } -} - -func testRole(n int) *Role { - return &Role{ - ID: n, - Name: fmt.Sprintf("Role %d", n), - Slug: fmt.Sprintf("role%d", n), - Weight: n, - } -} - -func testRoleIdentifier(n int) *RoleIdentifier { - return &RoleIdentifier{ - ID: n, - Name: fmt.Sprintf("RoleIdentifier %d", n), - Slug: fmt.Sprintf("roleidentifier%d", n), - } -} - -func testSimpleIdentifier(n int) *SimpleIdentifier { - return &SimpleIdentifier{ - ID: n, - Name: fmt.Sprintf("simple %d", n), - Slug: fmt.Sprintf("simple%d", n), - } -} - -func testSite(n int) *Site { - return &Site{ - ID: n, - Name: fmt.Sprintf("Site %d", n), - Slug: fmt.Sprintf("site%d", n), - Facility: fmt.Sprintf("Facility %d", n), - ASN: ASN(n), - PhysicalAddress: fmt.Sprintf("%d Facility Street, City, State 12345", n), - ShippingAddress: fmt.Sprintf("%d Facility Street, ATTN: Shipping & Receiving, City, State 12345", n), - Comments: fmt.Sprintf("comment %d", n), - CountPrefixes: n, - CountVLANs: n, - CountRacks: n, - CountDevices: n, - CountCircuits: n, - } -} - -func testSiteIdentifier(n int) *SiteIdentifier { - return &SiteIdentifier{ - ID: n, - Name: fmt.Sprintf("SiteIdentifier %d", n), - Slug: fmt.Sprintf("siteidentifier%d", n), - } -} - -func testVLAN(n int) *VLAN { - return &VLAN{ - ID: n, - Site: testSiteIdentifier(n), - VID: VLANID(n), - Name: fmt.Sprintf("vlan %d", n), - Status: StatusActive, - Role: testRoleIdentifier(n), - DisplayName: fmt.Sprintf("VLAN %d", n), - } -} - -func testVLANIdentifier(n int) *VLANIdentifier { - return &VLANIdentifier{ - ID: n, - VID: VLANID(n), - Name: fmt.Sprintf("vlanidentifier %d", n), - DisplayName: fmt.Sprintf("VLANIdentifier %d", n), - } -} - -func testVRF(n int) *VRF { - return &VRF{ - ID: n, - Name: fmt.Sprintf("VRF %d", n), - RD: fmt.Sprintf("rd %d", n), - Description: fmt.Sprintf("description %d", n), - } -} - -func testVRFIdentifier(n int) *VRFIdentifier { - return &VRFIdentifier{ - ID: n, - Name: fmt.Sprintf("VRFIdentifier %d", n), - RD: fmt.Sprintf("rd %d", n), - } -} diff --git a/dcim_consoleports.go b/dcim_consoleports.go deleted file mode 100644 index 63791c2bce9cd1a0ffc5841bd8e3cb80ff43fe36..0000000000000000000000000000000000000000 --- a/dcim_consoleports.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2016 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 - -// ConsolePort represents a console port object. -type ConsolePort struct { - ID int `json:"id"` - Device *DeviceIdentifier `json:"device"` - Name string `json:"name"` - CSPort *ConsoleServerPort `json:"cs_port"` - ConnectionStatus bool `json:"connection_status"` -} - -// ConsoleServerPort represents a console server port object. -type ConsoleServerPort struct { - ID int `json:"id"` - Device *DeviceIdentifier `json:"device"` - Name string `json:"name"` -} diff --git a/dcim_devices.go b/dcim_devices.go deleted file mode 100644 index d7adcd2133c64f8f67a32f8eac0e1c441b40f146..0000000000000000000000000000000000000000 --- a/dcim_devices.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2016 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 - -// Device is a network device. -type Device struct { - ID int `json:"id"` - Name string `json:"name"` - DisplayName string `json:"display_name"` - DeviceType *DeviceTypeIdentifier `json:"device_type"` - DeviceRole *SimpleIdentifier `json:"device_role"` - Platform *SimpleIdentifier `json:"platform"` - Serial string `json:"serial"` - Rack *RackIdentifier `json:"rack"` - Position int `json:"position"` - Face int `json:"face"` - ParentDevice *DeviceIdentifier `json:"parent_device"` - Status bool `json:"status"` - PrimaryIP *IPAddressIdentifier `json:"primary_ip"` - PrimaryIP4 *IPAddressIdentifier `json:"primary_ip4"` - PrimaryIP6 *IPAddressIdentifier `json:"primary_ip6"` - Comments string `json:"comments"` -} - -// A DeviceIdentifier is a reduced version of a Device, returned as a nested -// object in some top-level objects. It contains information which can -// be used in subsequent API calls to identify and retrieve a full Device. -type DeviceIdentifier struct { - ID int `json:"id"` - Name string `json:"name"` -} - -// A DeviceTypeIdentifier indicates the device type of a network device. -type DeviceTypeIdentifier struct { - ID int `json:"id"` - Manufacturer *SimpleIdentifier `json:"manufacturer"` - Model string `json:"model"` - Slug string `json:"slug"` -} - -// RackIdentifier represents a server rack. -type RackIdentifier struct { - ID int `json:"id"` - Name string `json:"name"` - FacilityID string `json:"facility_id"` - DisplayName string `json:"display_name"` -} diff --git a/dcim_devices_test.go b/dcim_devices_test.go deleted file mode 100644 index df3dc6bbd5987df9982aa291c147749b0c1975c5..0000000000000000000000000000000000000000 --- a/dcim_devices_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2016 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 "reflect" - -func deviceEqual(a, b *Device) bool { - var obsAB = []struct { - a interface{} - b interface{} - }{ - {a: a.DeviceType, b: b.DeviceType}, - {a: a.DeviceRole, b: b.DeviceRole}, - {a: a.Platform, b: b.Platform}, - {a: a.Rack, b: b.Rack}, - {a: a.ParentDevice, b: b.ParentDevice}, - {a: a.ID, b: b.ID}, - {a: a.Name, b: b.Name}, - {a: a.DisplayName, b: b.DisplayName}, - {a: a.Serial, b: b.Serial}, - {a: a.Position, b: b.Position}, - {a: a.Face, b: b.Face}, - {a: a.Status, b: b.Status}, - {a: a.Comments, b: b.Comments}, - {a: a.PrimaryIP, b: b.PrimaryIP}, - {a: a.PrimaryIP4, b: b.PrimaryIP4}, - {a: a.PrimaryIP6, b: b.PrimaryIP6}, - } - for _, o := range obsAB { - - switch o.a.(type) { - case *IPAddressIdentifier: - i, j := o.a.(*IPAddressIdentifier), o.b.(*IPAddressIdentifier) - if ok := ipAddressIdentifiersEqual(*i, *j); !ok { - return false - } - default: - if !reflect.DeepEqual(o.a, o.b) { - return false - } - } - } - - return true -} diff --git a/dcim_interfaces.go b/dcim_interfaces.go deleted file mode 100644 index e1758e0f5e75675b2a992aafc231d179c3b3a537..0000000000000000000000000000000000000000 --- a/dcim_interfaces.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2016 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 - -// Interface represents an interface object. -type Interface struct { - ID int `json:"id"` - Name string `json:"name"` - FormFactor string `json:"form_factor"` - MacAddress string `json:"mac_address"` - MgmtOnly bool `json:"mgmt_only"` - Description string `json:"description"` - IsConnected bool `json:"is_connected"` - ConnectedInterface *InterfaceDetail `json:"connected_interface"` -} - -// InterfaceDetail represents an interface-detail object. -type InterfaceDetail struct { - ID int `json:"id"` - Device *DeviceIdentifier `json:"device"` - Name string `json:"name"` - FormFactor string `json:"form_factor"` - MacAddress string `json:"mac_address"` - MgmtOnly bool `json:"mgmt_only"` - Description string `json:"description"` - IsConnected bool `json:"is_connected"` -} - -// An InterfaceIdentifier is a reduced version of an Interface, returned as a -// nested object in some top-level objects. It contains information which can -// be used in subsequent API calls to identify and retrieve a full Interface. -type InterfaceIdentifier struct { - ID int `json:"id"` - Device *DeviceIdentifier `json:"device"` - Name string `json:"name"` -} diff --git a/dcim_related-connections.go b/dcim_related-connections.go deleted file mode 100644 index 5a75f1fd5abe9ccbcdc7584652c1fd3a85e2c45e..0000000000000000000000000000000000000000 --- a/dcim_related-connections.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2016 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 ( - "errors" - "net/http" - "net/url" -) - -// RelatedConnection represents components that have a related peer-device and -// peer-interface. -type RelatedConnection struct { - Device *Device `json:"device"` - ConsolePorts []*ConsolePort `json:"console-ports"` - Interfaces []*Interface `json:"interfaces"` - PowerPorts []*PowerPort `json:"power-ports"` -} - -// ConsolePortIdentifier represents a reduced version of a console port. -type ConsolePortIdentifier struct { - Device string `json:"device"` - Name string `json:"name"` - Port string `json:"port"` -} - -// PowerPortIdentifier represents a reduced version of a single power port. -type PowerPortIdentifier struct { - Device string `json:"device"` - Name string `json:"name"` - Outlet string `json:"outlet"` -} - -// GetRelatedConnections retrieves a RelatedConnection object from NetBox. -func (s *DCIMService) GetRelatedConnections( - peerDevice, peerInterface string, -) (*RelatedConnection, error) { - req, err := s.c.NewRequest( - http.MethodGet, - "/api/dcim/related-connections/", - &relatedConnectionsOptions{ - peerDevice, - peerInterface, - }, - ) - if err != nil { - return nil, err - } - - rc := new(RelatedConnection) - err = s.c.Do(req, rc) - if err != nil { - return nil, err - } - return rc, nil -} - -// relatedConnectionsOptions is used as an argument for -// Client.DCIM.GetRelatedConnections. -type relatedConnectionsOptions struct { - PeerDevice string - PeerInterface string -} - -func (o *relatedConnectionsOptions) Values() (url.Values, error) { - err := errors.New( - "must provide non-zero values for both peer-device and peer-interface", - ) - if o == nil { - return nil, err - } - - if o.PeerDevice == "" || o.PeerInterface == "" { - return nil, err - } - v := url.Values{} - - v.Set("peer-device", o.PeerDevice) - - v.Set("peer-interface", o.PeerInterface) - - return v, nil -} diff --git a/dcim_related-connections_test.go b/dcim_related-connections_test.go deleted file mode 100644 index 8c8f515c5c722eb3004532fbc5f9ec07f692132f..0000000000000000000000000000000000000000 --- a/dcim_related-connections_test.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2016 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 ( - "net/http" - "net/url" - "reflect" - "testing" -) - -func TestGetRelatedConnection(t *testing.T) { - want := testRelatedConnection(1) - - c, done := testClient( - t, - testHandler(t, http.MethodGet, "/api/dcim/related-connections/", want), - ) - defer done() - - got, err := c.DCIM.GetRelatedConnections("device1", "interface1") - if err != nil { - t.Fatalf("unexpected error from Client.DCIM.GetRelatedConnections: %v", err) - } - - if !rcEqual(want, got) { - t.Fatalf( - "unexpected related-connections payload:\n- want: %v\n- got: %v", - *want, - *got, - ) - } -} - -func TestGetRelatedConnectionWithOptions(t *testing.T) { - var happyPathTests = []struct { - desc string - o *relatedConnectionsOptions - want url.Values - }{ - { - desc: "normal string length", - o: &relatedConnectionsOptions{ - PeerDevice: "test-abc_def-0123456789", - PeerInterface: "test-zyx-wvu-987654321", - }, - want: url.Values{ - "peer-device": []string{"test-abc_def-0123456789"}, - "peer-interface": []string{"test-zyx-wvu-987654321"}, - }, - }, - { - desc: "super long string", - o: &relatedConnectionsOptions{ - PeerDevice: "test-abc_def-01234567890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - PeerInterface: "test-zyx-wvu-98765432100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - }, - want: url.Values{ - "peer-device": []string{"test-abc_def-01234567890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, - "peer-interface": []string{"test-zyx-wvu-98765432100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, - }, - }, - { - desc: "super short string", - o: &relatedConnectionsOptions{ - PeerDevice: "a", - PeerInterface: "z", - }, - want: url.Values{ - "peer-device": []string{"a"}, - "peer-interface": []string{"z"}, - }, - }, - } - for i, test := range happyPathTests { - t.Logf("[%02d] happy path test %q", i, test.desc) - - got, err := test.o.Values() - if err != nil { - t.Fatalf("unexpected Values error: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Fatalf( - "unexpected url.Values map:\n- want: %v\n- got: %v", - test.want, - got, - ) - } - } - - var negativeTests = []struct { - desc string - o *relatedConnectionsOptions - }{ - { - desc: "only peer-device", - o: &relatedConnectionsOptions{ - PeerDevice: "test-abc_def-0123456789", - }, - }, - { - desc: "only peer-interface", - o: &relatedConnectionsOptions{ - PeerInterface: "test-zyx-wvu-987654321", - }, - }, - { - desc: "no peer-device or peer-interface", - o: &relatedConnectionsOptions{}, - }, - } - for i, test := range negativeTests { - t.Logf("[%02d] negative path test %q", i, test.desc) - - if _, err := test.o.Values(); err == nil { - t.Fatal("expected Error but got nil") - } - } -} - -func rcEqual(a, b *RelatedConnection) bool { - if ok := deviceEqual(a.Device, b.Device); !ok { - return false - } - - if !reflect.DeepEqual(a.ConsolePorts, b.ConsolePorts) { - return false - } - - if !reflect.DeepEqual(a.Interfaces, b.Interfaces) { - return false - } - - if !reflect.DeepEqual(a.PowerPorts, b.PowerPorts) { - return false - } - - return true -} diff --git a/dcim_sites.go b/dcim_sites.go deleted file mode 100644 index 11d995d1a26e4fa9f6cb6511fbe814cae6eb7f1f..0000000000000000000000000000000000000000 --- a/dcim_sites.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2016 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 ( - "fmt" - "net/http" -) - -// A Site is a physical location where devices may reside. -type Site struct { - ID int `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Facility string `json:"facility"` - ASN ASN `json:"asn"` - PhysicalAddress string `json:"physical_address"` - ShippingAddress string `json:"shipping_address"` - Comments string `json:"comments"` - CountPrefixes int `json:"count_prefixes"` - CountVLANs int `json:"count_vlans"` - CountRacks int `json:"count_racks"` - CountDevices int `json:"count_devices"` - CountCircuits int `json:"count_circuits"` -} - -// A SiteIdentifier is a reduced version of a Site, returned as a nested -// object in some top-level objects. It contains information which can -// be used in subsequent API calls to identify and retrieve a full Site. -type SiteIdentifier struct { - ID int `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` -} - -// GetSite retrieves a Site object from NetBox by its ID. -func (s *DCIMService) GetSite(id int) (*Site, error) { - req, err := s.c.NewRequest( - http.MethodGet, - fmt.Sprintf("/api/dcim/sites/%d", id), - nil, - ) - if err != nil { - return nil, err - } - - st := new(Site) - err = s.c.Do(req, st) - return st, err -} - -// ListSites retrives a list of Site objects from NetBox. -func (s *DCIMService) ListSites() ([]*Site, error) { - req, err := s.c.NewRequest(http.MethodGet, "/api/dcim/sites/", nil) - if err != nil { - return nil, err - } - - var sts []*Site - err = s.c.Do(req, &sts) - return sts, err -} diff --git a/dcim_sites_test.go b/dcim_sites_test.go deleted file mode 100644 index 4a169b5667f069b0526c301491fb94740c1d9a31..0000000000000000000000000000000000000000 --- a/dcim_sites_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2016 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 ( - "net/http" - "reflect" - "testing" -) - -func TestClientDCIMGetSite(t *testing.T) { - wantSite := testSite(1) - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/dcim/sites/1", wantSite)) - defer done() - - gotSite, err := c.DCIM.GetSite(wantSite.ID) - if err != nil { - t.Fatalf("unexpected error from Client.DCIM.GetSite: %v", err) - } - - if want, got := *wantSite, *gotSite; !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected Site:\n- want: %v\n- got: %v", want, got) - } -} - -func TestClientDCIMListSites(t *testing.T) { - wantSites := []*Site{ - testSite(1), - testSite(2), - testSite(3), - } - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/dcim/sites/", wantSites)) - defer done() - - gotSites, err := c.DCIM.ListSites() - if err != nil { - t.Fatalf("unexpected error from Client.DCIM.ListSites: %v", err) - } - - if want, got := derefSites(wantSites), derefSites(gotSites); !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected Sites:\n- want: %v\n- got: %v", want, got) - } -} - -// derefSites is used to print values of Sites in slice, instead of memory addresses. -func derefSites(sites []*Site) []Site { - s := make([]Site, len(sites)) - for i := range sites { - s[i] = *sites[i] - } - - return s -} diff --git a/ipam_aggregates.go b/ipam_aggregates.go deleted file mode 100644 index 7433bf80604013b99c7b57e065fb63e97820cf02..0000000000000000000000000000000000000000 --- a/ipam_aggregates.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright 2016 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" - "fmt" - "net" - "net/http" - "net/url" - "strconv" - "time" -) - -// An Aggregate is an IPv4 or IPv6 address aggregate. -type Aggregate struct { - ID int - Family Family - Prefix *net.IPNet - RIR *RIRIdentifier - DateAdded time.Time - Description string -} - -// GetAggregate retrieves an Aggregate object from NetBox by its ID. -func (s *IPAMService) GetAggregate(id int) (*Aggregate, error) { - req, err := s.c.NewRequest( - http.MethodGet, - fmt.Sprintf("/api/ipam/aggregates/%d", id), - nil, - ) - if err != nil { - return nil, err - } - - a := new(Aggregate) - err = s.c.Do(req, a) - return a, err -} - -// ListAggregates retrives a list of Aggregate objects from NetBox, filtered -// according to the parameters specified in options. -// -// If options is nil, all Aggregates will be retrieved. -func (s *IPAMService) ListAggregates(options *ListAggregatesOptions) ([]*Aggregate, error) { - req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/aggregates/", options) - if err != nil { - return nil, err - } - - var as []*Aggregate - err = s.c.Do(req, &as) - return as, err -} - -// ListAggregatesOptions is a used as an argument for Client.IPAM.ListAggregates. -// Integer fields with an *ID suffix are preferred over their string counterparts, -// and if both are set, only the *ID field will be used. -type ListAggregatesOptions struct { - Family Family - RIRID []int - RIR []string - DateAdded time.Time -} - -// Values generates a url.Values map from the data in ListAggregatesOptions. -func (o *ListAggregatesOptions) Values() (url.Values, error) { - if o == nil { - return nil, nil - } - - v := url.Values{} - - if o.Family != 0 { - v.Set("family", strconv.Itoa(int(o.Family))) - } - - // IDs should always be preferred over string names - - switch { - case len(o.RIRID) > 0: - for _, r := range o.RIRID { - v.Add("rir_id", strconv.Itoa(r)) - } - case len(o.RIR) > 0: - for _, r := range o.RIR { - v.Add("rir", r) - } - } - - if !o.DateAdded.IsZero() { - v.Set("date_added", o.DateAdded.Format(dateFormat)) - } - - return v, nil -} - -// dateFormat is the package time equivalent of the date format used by NetBox. -const dateFormat = "2006-01-02" - -// An aggregate is the raw JSON representation of an Aggregate. -type aggregate struct { - ID int `json:"id"` - Family Family `json:"family"` - Prefix string `json:"prefix"` - RIR *RIRIdentifier `json:"rir"` - DateAdded string `json:"date_added"` - Description string `json:"description"` -} - -// MarshalJSON marshals an Aggregate into JSON bytes. -func (a *Aggregate) MarshalJSON() ([]byte, error) { - var date string - if !a.DateAdded.IsZero() { - date = a.DateAdded.Format(dateFormat) - } - - return json.Marshal(aggregate{ - ID: a.ID, - Family: a.Family, - Prefix: a.Prefix.String(), - RIR: a.RIR, - DateAdded: date, - Description: a.Description, - }) -} - -// UnmarshalJSON unmarshals JSON bytes into an Aggregate, and verifies that -// the contained IP address and date are valid. -func (a *Aggregate) UnmarshalJSON(b []byte) error { - var raw aggregate - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - - _, prefix, err := net.ParseCIDR(raw.Prefix) - if err != nil { - return err - } - - *a = Aggregate{ - ID: raw.ID, - Family: raw.Family, - Prefix: prefix, - RIR: raw.RIR, - Description: raw.Description, - } - - if raw.DateAdded == "" { - return nil - } - - t, err := time.Parse(dateFormat, raw.DateAdded) - if err != nil { - return err - } - a.DateAdded = t - - return nil -} diff --git a/ipam_aggregates_test.go b/ipam_aggregates_test.go deleted file mode 100644 index 9366f6486f19359d35d0626b67b25585268f624e..0000000000000000000000000000000000000000 --- a/ipam_aggregates_test.go +++ /dev/null @@ -1,307 +0,0 @@ -// Copyright 2016 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" - "net" - "net/http" - "net/url" - "reflect" - "strconv" - "testing" - "time" -) - -func TestClientIPAMGetAggregate(t *testing.T) { - wantAggregate := testAggregate(FamilyIPv4, 1) - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/aggregates/1", wantAggregate)) - defer done() - - gotAggregate, err := c.IPAM.GetAggregate(wantAggregate.ID) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.GetAggregate: %v", err) - } - - if want, got := *wantAggregate, *gotAggregate; !aggregatesEqual(want, got) { - t.Fatalf("unexpected Aggregate:\n- want: %v\n- got: %v", want, got) - } -} - -func TestClientIPAMListAggregates(t *testing.T) { - wantAggregates := []*Aggregate{ - testAggregate(FamilyIPv4, 1), - testAggregate(FamilyIPv6, 2), - } - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/aggregates/", wantAggregates)) - defer done() - - gotAggregates, err := c.IPAM.ListAggregates(nil) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.ListAggregates: %v", err) - } - - want := derefAggregates(wantAggregates) - got := derefAggregates(gotAggregates) - if !aggregateSlicesEqual(want, got) { - t.Fatalf("unexpected Aggregates:\n- want: %v\n- got: %v", want, got) - } -} - -func TestListAggregatesOptionsValues(t *testing.T) { - var tests = []struct { - desc string - o *ListAggregatesOptions - v url.Values - }{ - { - desc: "empty options", - }, - { - desc: "family only", - o: &ListAggregatesOptions{ - Family: FamilyIPv4, - }, - v: url.Values{ - "family": []string{strconv.Itoa(int(FamilyIPv4))}, - }, - }, - { - desc: "1 rir_id only", - o: &ListAggregatesOptions{ - RIRID: []int{1}, - }, - v: url.Values{ - "rir_id": []string{"1"}, - }, - }, - { - desc: "3 rir_ids only", - o: &ListAggregatesOptions{ - RIRID: []int{1, 2, 3}, - }, - v: url.Values{ - "rir_id": []string{"1", "2", "3"}, - }, - }, - { - desc: "1 rir only", - o: &ListAggregatesOptions{ - RIR: []string{"rir"}, - }, - v: url.Values{ - "rir": []string{"rir"}, - }, - }, - { - desc: "3 rirs only", - o: &ListAggregatesOptions{ - RIR: []string{"rirfoo", "rirbar", "rirbaz"}, - }, - v: url.Values{ - "rir": []string{"rirfoo", "rirbar", "rirbaz"}, - }, - }, - { - desc: "rir and rir_id, rir_id preferred", - o: &ListAggregatesOptions{ - RIR: []string{"rir"}, - RIRID: []int{1}, - }, - v: url.Values{ - "rir_id": []string{"1"}, - }, - }, - { - desc: "date_added only", - o: &ListAggregatesOptions{ - DateAdded: time.Date(2016, time.January, 22, 0, 0, 0, 0, time.UTC), - }, - v: url.Values{ - "date_added": []string{"2016-01-22"}, - }, - }, - { - desc: "all options", - o: &ListAggregatesOptions{ - Family: FamilyIPv4, - RIRID: []int{1}, - RIR: []string{"rir"}, - DateAdded: time.Date(2016, time.January, 22, 0, 0, 0, 0, time.UTC), - }, - v: url.Values{ - "family": []string{strconv.Itoa(int(FamilyIPv4))}, - "rir_id": []string{"1"}, - "date_added": []string{"2016-01-22"}, - }, - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - 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) - } - } -} - -func TestAggregateMarshalJSON(t *testing.T) { - var tests = []struct { - desc string - a *Aggregate - b []byte - }{ - { - desc: "IPv4 aggregate", - a: testAggregate(FamilyIPv4, 1), - b: []byte(`{"id":1,"family":4,"prefix":"8.0.0.0/8","rir":{"id":1,"name":"RIRIdentifier 1","slug":"riridentifier1"},"date_added":"2016-01-01","description":"description 1"}`), - }, - { - desc: "IPv6 aggregate", - a: testAggregate(FamilyIPv6, 2), - b: []byte(`{"id":2,"family":6,"prefix":"2001::/16","rir":{"id":2,"name":"RIRIdentifier 2","slug":"riridentifier2"},"date_added":"2016-01-01","description":"description 2"}`), - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - b, err := json.Marshal(tt.a) - if err != nil { - t.Fatalf("unexpected JSON marshal error: %v", err) - } - - if want, got := tt.b, b; !bytes.Equal(want, got) { - t.Fatalf("unexpected JSON bytes:\n- want: %v\n- got: %v", - string(want), string(got)) - } - } -} - -func TestAggregateUnmarshalJSON(t *testing.T) { - var tests = []struct { - desc string - b []byte - a *Aggregate - err error - }{ - { - desc: "invalid aggregate due to prefix", - b: []byte(`{"prefix":"foo"}`), - err: &net.ParseError{ - Type: "CIDR address", - Text: "foo", - }, - }, - { - desc: "invalid aggregate due to date_added", - b: []byte(`{"prefix":"5.101.96.0/20","date_added":"foo"}`), - err: &time.ParseError{ - Layout: dateFormat, - Value: "foo", - LayoutElem: "2006", - ValueElem: "foo", - }, - }, - { - desc: "IPv4 aggregate", - b: []byte(`{"id":1,"family":4,"prefix":"8.0.0.0/8","rir":{"id":1,"name":"RIRIdentifier 1","slug":"riridentifier1"},"date_added":"2016-01-01","description":"description 1"}`), - a: testAggregate(FamilyIPv4, 1), - }, - { - desc: "IPv6 aggregate", - b: []byte(`{"id":2,"family":6,"prefix":"2001::/16","rir":{"id":2,"name":"RIRIdentifier 2","slug":"riridentifier2"},"date_added":"2016-01-01","description":"description 2"}`), - a: testAggregate(FamilyIPv6, 2), - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - a := new(Aggregate) - err := json.Unmarshal(tt.b, a) - - if want, got := tt.err, err; !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected error:\n- want: %v\n- got: %v", - want, got) - } - if err != nil { - continue - } - - if want, got := *tt.a, *a; !aggregatesEqual(want, got) { - t.Fatalf("unexpected Aggregate:\n- want: %v\n- got: %v", - want, got) - } - } -} - -func aggregateSlicesEqual(a, b []Aggregate) bool { - if len(a) != len(b) { - return false - } - - for i := range a { - if !aggregatesEqual(a[i], b[i]) { - return false - } - } - - return true -} - -func aggregatesEqual(a, b Aggregate) bool { - if a.ID != b.ID { - return false - } - - if a.Family != b.Family { - return false - } - - if a.Prefix.String() != b.Prefix.String() { - return false - } - - if !a.DateAdded.Equal(b.DateAdded) { - return false - } - - if a.Description != b.Description { - return false - } - - return true -} - -// Used to print values of Aggregates in slice, instead of memory addresses. -func derefAggregates(aggregates []*Aggregate) []Aggregate { - a := make([]Aggregate, len(aggregates)) - for i := range aggregates { - a[i] = *aggregates[i] - } - - return a -} diff --git a/ipam_ipaddresses.go b/ipam_ipaddresses.go deleted file mode 100644 index 0c7dddd47220a7408869f12a42a1463f6d7bb01a..0000000000000000000000000000000000000000 --- a/ipam_ipaddresses.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2016 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" - "fmt" - "net" - "net/http" - "net/url" - "strconv" -) - -// An IPAddress is an IPv4 or IPv6 address. -type IPAddress struct { - ID int - Family Family - Address *net.IPNet - VRF *VRFIdentifier - Interface *InterfaceIdentifier - Description string - NATInside *IPAddressIdentifier - NATOutside *IPAddressIdentifier -} - -// An IPAddressIdentifier is a reduced version of a IPAddress, returned as -// nested object in some top-level objects. It contains information which can -// be used in subsequent API calls to identify and retrieve a full IPAddress. -type IPAddressIdentifier struct { - ID int - Family Family - Address *net.IPNet -} - -// GetIPAddress retrieves an IPAddress object from NetBox by its ID. -func (s *IPAMService) GetIPAddress(id int) (*IPAddress, error) { - req, err := s.c.NewRequest( - http.MethodGet, - fmt.Sprintf("/api/ipam/ip-addresses/%d", id), - nil, - ) - if err != nil { - return nil, err - } - - ip := new(IPAddress) - err = s.c.Do(req, ip) - return ip, err -} - -// ListIPAddresses retrives a list of IPAddress objects from NetBox, filtered according -// to the parameters specified in options. -// -// If options is nil, all IPAddresses will be retrieved. -func (s *IPAMService) ListIPAddresses(options *ListIPAddressesOptions) ([]*IPAddress, error) { - req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/ip-addresses/", options) - if err != nil { - return nil, err - } - - var ips []*IPAddress - err = s.c.Do(req, &ips) - return ips, err -} - -// An ipAddress is the raw JSON representation of an IPAddress. -type ipAddress struct { - ID int `json:"id"` - Family Family `json:"family"` - Address string `json:"address"` - VRF *VRFIdentifier `json:"vrf"` - Interface *InterfaceIdentifier `json:"interface"` - Description string `json:"description"` - NATInside *IPAddressIdentifier `json:"nat_inside"` - NATOutside *IPAddressIdentifier `json:"nat_outside"` -} - -// MarshalJSON marshals an IPAddress into JSON bytes. -func (ip *IPAddress) MarshalJSON() ([]byte, error) { - return json.Marshal(ipAddress{ - ID: ip.ID, - Family: ip.Family, - Address: ip.Address.String(), - VRF: ip.VRF, - Interface: ip.Interface, - Description: ip.Description, - NATInside: ip.NATInside, - NATOutside: ip.NATOutside, - }) -} - -// UnmarshalJSON unmarshals JSON bytes into an IPAddress, and verifies that -// the contained IP address is valid. -func (ip *IPAddress) UnmarshalJSON(b []byte) error { - var raw ipAddress - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - - _, ipNet, err := net.ParseCIDR(raw.Address) - if err != nil { - return err - } - - *ip = IPAddress{ - ID: raw.ID, - Family: raw.Family, - Address: ipNet, - VRF: raw.VRF, - Interface: raw.Interface, - Description: raw.Description, - NATInside: raw.NATInside, - NATOutside: raw.NATOutside, - } - return nil -} - -// An ipAddressIdentifier is the raw JSON representation of an IPAddressIdentifier. -type ipAddressIdentifier struct { - ID int `json:"id"` - Family Family `json:"family"` - Address string `json:"address"` -} - -// MarshalJSON marshals an IPAddressIdentifier into JSON bytes. -func (ip *IPAddressIdentifier) MarshalJSON() ([]byte, error) { - return json.Marshal(ipAddressIdentifier{ - ID: ip.ID, - Family: ip.Family, - Address: ip.Address.String(), - }) -} - -// UnmarshalJSON unmarshals JSON bytes into an IPAddressIdentifier, and verifies that -// the contained IP address is valid. -func (ip *IPAddressIdentifier) UnmarshalJSON(b []byte) error { - var raw ipAddressIdentifier - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - - _, ipNet, err := net.ParseCIDR(raw.Address) - if err != nil { - return err - } - - *ip = IPAddressIdentifier{ - ID: raw.ID, - Family: raw.Family, - Address: ipNet, - } - return nil -} - -// ListIPAddressesOptions is used as an argument for Client.IPAM.ListIPAddresses. -// Integer fields with an *ID suffix are preferred over their string -// counterparts, and if both are set, only the *ID field will be used. -type ListIPAddressesOptions struct { - Family Family - VRFID []int - VRF string - InterfaceID []int - DeviceID []int - Device []string - - // Query is a special option which enables free-form search. - // For example, Query could be an IP address such as "8.8.8.8". - Query string -} - -// Values generates a url.Values map from the data in ListIPAddressesOptions. -func (o *ListIPAddressesOptions) Values() (url.Values, error) { - if o == nil { - return nil, nil - } - - v := url.Values{} - - if o.Family != 0 { - v.Set("family", strconv.Itoa(int(o.Family))) - } - - for _, i := range o.InterfaceID { - v.Add("interface_id", strconv.Itoa(i)) - } - - // IDs should always be preferred over string names - - switch { - case len(o.VRFID) > 0: - for _, vid := range o.VRFID { - v.Add("vrf_id", strconv.Itoa(vid)) - } - case o.VRF != "": - v.Set("vrf", o.VRF) - } - - switch { - case len(o.DeviceID) > 0: - for _, d := range o.DeviceID { - v.Add("device_id", strconv.Itoa(d)) - } - case len(o.Device) > 0: - for _, d := range o.Device { - v.Add("device", d) - } - } - - if o.Query != "" { - v.Set("q", o.Query) - } - - return v, nil -} diff --git a/ipam_ipaddresses_test.go b/ipam_ipaddresses_test.go deleted file mode 100644 index e761fd34b40cd855fac4b0738587e78067ff4881..0000000000000000000000000000000000000000 --- a/ipam_ipaddresses_test.go +++ /dev/null @@ -1,457 +0,0 @@ -// Copyright 2016 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" - "net" - "net/http" - "net/url" - "reflect" - "strconv" - "testing" -) - -func TestClientIPAMGetIPAddress(t *testing.T) { - wantIP := testIPAddress(FamilyIPv4, 1) - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/ip-addresses/1", wantIP)) - defer done() - - gotIP, err := c.IPAM.GetIPAddress(wantIP.ID) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.GetIPAddress: %v", err) - } - - if want, got := *wantIP, *gotIP; !ipAddressesEqual(want, got) { - t.Fatalf("unexpected IPAddress:\n- want: %v\n- got: %v", want, got) - } -} - -func TestClientIPAMListIPAddresses(t *testing.T) { - wantIPs := []*IPAddress{ - testIPAddress(FamilyIPv4, 1), - testIPAddress(FamilyIPv6, 2), - } - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/ip-addresses/", wantIPs)) - defer done() - - gotIPs, err := c.IPAM.ListIPAddresses(nil) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.ListIPAddresses: %v", err) - } - - want := derefIPAddresses(wantIPs) - got := derefIPAddresses(gotIPs) - if !ipAddressesSlicesEqual(want, got) { - t.Fatalf("unexpected IPs:\n- want: %v\n- got: %v", want, got) - } -} - -func TestListIPAddressesOptionsValues(t *testing.T) { - var tests = []struct { - desc string - o *ListIPAddressesOptions - v url.Values - }{ - { - desc: "empty options", - }, - { - desc: "family only", - o: &ListIPAddressesOptions{ - Family: FamilyIPv4, - }, - v: url.Values{ - "family": []string{strconv.Itoa(int(FamilyIPv4))}, - }, - }, - { - desc: "1 vrf_id only", - o: &ListIPAddressesOptions{ - VRFID: []int{1}, - }, - v: url.Values{ - "vrf_id": []string{"1"}, - }, - }, - { - desc: "3 vrf_ids only", - o: &ListIPAddressesOptions{ - VRFID: []int{1, 2, 3}, - }, - v: url.Values{ - "vrf_id": []string{"1", "2", "3"}, - }, - }, - { - desc: "vrf only", - o: &ListIPAddressesOptions{ - VRF: "vrf", - }, - v: url.Values{ - "vrf": []string{"vrf"}, - }, - }, - { - desc: "vrf and vrf_id, vrf_id preferred", - o: &ListIPAddressesOptions{ - VRF: "vrf", - VRFID: []int{1}, - }, - v: url.Values{ - "vrf_id": []string{"1"}, - }, - }, - { - desc: "1 interface_id only", - o: &ListIPAddressesOptions{ - InterfaceID: []int{2}, - }, - v: url.Values{ - "interface_id": []string{"2"}, - }, - }, - { - desc: "3 interface_ids only", - o: &ListIPAddressesOptions{ - InterfaceID: []int{2, 3, 4}, - }, - v: url.Values{ - "interface_id": []string{"2", "3", "4"}, - }, - }, - { - desc: "1 device_id only", - o: &ListIPAddressesOptions{ - DeviceID: []int{3}, - }, - v: url.Values{ - "device_id": []string{"3"}, - }, - }, - { - desc: "3 device_ids only", - o: &ListIPAddressesOptions{ - DeviceID: []int{3, 4, 5}, - }, - v: url.Values{ - "device_id": []string{"3", "4", "5"}, - }, - }, - { - desc: "1 device only", - o: &ListIPAddressesOptions{ - Device: []string{"device"}, - }, - v: url.Values{ - "device": []string{"device"}, - }, - }, - { - desc: "3 devices only", - o: &ListIPAddressesOptions{ - Device: []string{"a", "b", "c"}, - }, - v: url.Values{ - "device": []string{"a", "b", "c"}, - }, - }, - { - desc: "device and device_id, device_id preferred", - o: &ListIPAddressesOptions{ - Device: []string{"device"}, - DeviceID: []int{3}, - }, - v: url.Values{ - "device_id": []string{"3"}, - }, - }, - { - desc: "q only", - o: &ListIPAddressesOptions{ - Query: "query", - }, - v: url.Values{ - "q": []string{"query"}, - }, - }, - { - desc: "all options", - o: &ListIPAddressesOptions{ - Family: FamilyIPv4, - VRFID: []int{1}, - VRF: "vrf", - InterfaceID: []int{2}, - DeviceID: []int{3}, - Device: []string{"device"}, - Query: "query", - }, - v: url.Values{ - "family": []string{strconv.Itoa(int(FamilyIPv4))}, - "vrf_id": []string{"1"}, - "interface_id": []string{"2"}, - "device_id": []string{"3"}, - "q": []string{"query"}, - }, - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - 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) - } - } -} - -func TestIPAddressMarshalJSON(t *testing.T) { - var tests = []struct { - desc string - ip *IPAddress - b []byte - }{ - { - desc: "IPv4 address", - ip: testIPAddress(FamilyIPv4, 1), - b: []byte(`{"id":1,"family":4,"address":"8.8.8.0/24","vrf":{"id":1,"name":"VRFIdentifier 1","rd":"rd 1"},"interface":{"id":1,"device":{"id":1,"name":"DeviceIdentifier 1"},"name":"InterfaceIdentifier 1"},"description":"description 1","nat_inside":{"id":1,"family":4,"address":"8.8.8.0/24"},"nat_outside":{"id":1,"family":4,"address":"8.8.8.0/24"}}`), - }, - { - desc: "IPv6 address", - ip: testIPAddress(FamilyIPv6, 2), - b: []byte(`{"id":2,"family":6,"address":"2001:4860:4860::/48","vrf":{"id":2,"name":"VRFIdentifier 2","rd":"rd 2"},"interface":{"id":2,"device":{"id":2,"name":"DeviceIdentifier 2"},"name":"InterfaceIdentifier 2"},"description":"description 2","nat_inside":{"id":2,"family":6,"address":"2001:4860:4860::/48"},"nat_outside":{"id":2,"family":6,"address":"2001:4860:4860::/48"}}`), - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - b, err := json.Marshal(tt.ip) - if err != nil { - t.Fatalf("unexpected JSON marshal error: %v", err) - } - - if want, got := tt.b, b; !bytes.Equal(want, got) { - t.Fatalf("unexpected JSON bytes:\n- want: %v\n- got: %v", - string(want), string(got)) - } - } -} - -func TestIPAddressUnmarshalJSON(t *testing.T) { - var tests = []struct { - desc string - b []byte - ip *IPAddress - err error - }{ - { - desc: "invalid IP address", - b: []byte(`{"address":"foo"}`), - err: &net.ParseError{ - Type: "CIDR address", - Text: "foo", - }, - }, - { - desc: "IPv4 address", - b: []byte(`{"id":1,"family":4,"address":"8.8.8.8/24","vrf":{"id":1,"name":"VRFIdentifier 1","rd":"rd 1"},"interface":{"id":1,"device":{"id":1,"name":"DeviceIdentifier 1"},"name":"InterfaceIdentifier 1"}}`), - ip: testIPAddress(FamilyIPv4, 1), - }, - { - desc: "IPv6 address", - b: []byte(`{"id":2,"family":6,"address":"2001:4860:4860::8888/48","vrf":{"id":2,"name":"VRFIdentifier 2","rd":"rd 2"},"interface":{"id":2,"device":{"id":2,"name":"DeviceIdentifier 2"},"name":"InterfaceIdentifier 2"}}`), - ip: testIPAddress(FamilyIPv6, 2), - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - ip := new(IPAddress) - err := json.Unmarshal(tt.b, ip) - - if want, got := tt.err, err; !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected error:\n- want: %v\n- got: %v", - want, got) - } - if err != nil { - continue - } - - if want, got := *tt.ip, *ip; !ipAddressesEqual(want, got) { - t.Fatalf("unexpected IPAddress:\n- want: %v\n- got: %v", - want, got) - } - } -} - -func TestIPAddressIdentifierMarshalJSON(t *testing.T) { - var tests = []struct { - desc string - ip *IPAddressIdentifier - b []byte - }{ - { - desc: "IPv4 address", - ip: testIPAddressIdentifier(FamilyIPv4, 1), - b: []byte(`{"id":1,"family":4,"address":"8.8.8.0/24"}`), - }, - { - desc: "IPv6 address", - ip: testIPAddressIdentifier(FamilyIPv6, 2), - b: []byte(`{"id":2,"family":6,"address":"2001:4860:4860::/48"}`), - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - b, err := json.Marshal(tt.ip) - if err != nil { - t.Fatalf("unexpected JSON marshal error: %v", err) - } - - if want, got := tt.b, b; !bytes.Equal(want, got) { - t.Fatalf("unexpected JSON bytes:\n- want: %v\n- got: %v", - string(want), string(got)) - } - } -} - -func TestIPAddressIdentifierUnmarshalJSON(t *testing.T) { - var tests = []struct { - desc string - b []byte - ip *IPAddressIdentifier - err error - }{ - { - desc: "invalid IP address", - b: []byte(`{"address":"foo"}`), - err: &net.ParseError{ - Type: "CIDR address", - Text: "foo", - }, - }, - { - desc: "IPv4 address", - b: []byte(`{"id":1,"family":4,"address":"8.8.8.8/24"}`), - ip: testIPAddressIdentifier(FamilyIPv4, 1), - }, - { - desc: "IPv6 address", - b: []byte(`{"id":2,"family":6,"address":"2001:4860:4860::8888/48"}`), - ip: testIPAddressIdentifier(FamilyIPv6, 2), - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - ip := new(IPAddressIdentifier) - err := json.Unmarshal(tt.b, ip) - - if want, got := tt.err, err; !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected error:\n- want: %v\n- got: %v", - want, got) - } - if err != nil { - continue - } - - if want, got := *tt.ip, *ip; !ipAddressIdentifiersEqual(want, got) { - t.Fatalf("unexpected IPAddressIdentifier:\n- want: %v\n- got: %v", - want, got) - } - } -} - -func ipAddressesSlicesEqual(a, b []IPAddress) bool { - if len(a) != len(b) { - return false - } - - for i := range a { - if !ipAddressesEqual(a[i], b[i]) { - return false - } - } - - return true -} - -func ipAddressesEqual(a, b IPAddress) bool { - if a.ID != b.ID { - return false - } - - if a.Family != b.Family { - return false - } - - if !a.Address.IP.Equal(b.Address.IP) { - return false - } - - if a.Address.String() != b.Address.String() { - return false - } - - if !reflect.DeepEqual(a.VRF, b.VRF) { - return false - } - - if !reflect.DeepEqual(a.Interface, b.Interface) { - return false - } - - return true -} - -func ipAddressIdentifiersEqual(a, b IPAddressIdentifier) bool { - if a.ID != b.ID { - return false - } - - if a.Family != b.Family { - return false - } - - if !a.Address.IP.Equal(b.Address.IP) { - return false - } - - return true -} - -// Used to print values of IPAddresses in slice, instead of memory addresses. -func derefIPAddresses(ips []*IPAddress) []IPAddress { - ip := make([]IPAddress, len(ips)) - for i := range ips { - ip[i] = *ips[i] - } - - return ip -} diff --git a/ipam_prefixes.go b/ipam_prefixes.go deleted file mode 100644 index 37eea55c5d32481a2d8e5b7df0259311351814d6..0000000000000000000000000000000000000000 --- a/ipam_prefixes.go +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright 2016 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" - "fmt" - "net" - "net/http" - "net/url" - "strconv" -) - -// A Prefix is an IPv4 or IPv6 address prefix. -type Prefix struct { - ID int - Family Family - Prefix *net.IPNet - Site *SiteIdentifier - VRF *VRFIdentifier - VLAN *VLANIdentifier - Status Status - Role *RoleIdentifier - Description string -} - -// GetPrefix retrieves a Prefix object from NetBox by its ID. -func (s *IPAMService) GetPrefix(id int) (*Prefix, error) { - req, err := s.c.NewRequest( - http.MethodGet, - fmt.Sprintf("/api/ipam/prefixes/%d", id), - nil, - ) - if err != nil { - return nil, err - } - - p := new(Prefix) - err = s.c.Do(req, p) - return p, err -} - -// ListPrefixes retrives a list of Prefix objects from NetBox, filtered -// according to the parameters specified in options. -// -// If options is nil, all Prefixes will be retrieved. -func (s *IPAMService) ListPrefixes(options *ListPrefixesOptions) ([]*Prefix, error) { - req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/prefixes/", options) - if err != nil { - return nil, err - } - - var ps []*Prefix - err = s.c.Do(req, &ps) - return ps, err -} - -// ListPrefixesOptions is used as an argument for Client.IPAM.ListPrefixes. -// Integer fields with an *ID suffix are preferred over their string -// counterparts, and if both are set, only the *ID field will be used. -// In addition, VLANID is preferred over the site-specific VLANVID. -type ListPrefixesOptions struct { - Family Family - SiteID []int - Site []string - VRFID int - VRF string - VLANID []int - VLANVID VLANID - Status int - RoleID []int - Role []string - Parent *net.IPNet - - // Query is a special option which enables free-form search. - // For example, Query could be an IP address such as "8.8.8.8". - Query string -} - -// Values generates a url.Values map from the data in ListPrefixesOptions. -func (o *ListPrefixesOptions) Values() (url.Values, error) { - if o == nil { - return nil, nil - } - - v := url.Values{} - - if o.Family != 0 { - v.Set("family", strconv.Itoa(int(o.Family))) - } - - // IDs should always be preferred over string names - - switch { - case len(o.SiteID) > 0: - for _, s := range o.SiteID { - v.Add("site_id", strconv.Itoa(s)) - } - case len(o.Site) > 0: - for _, s := range o.Site { - v.Add("site", s) - } - } - - switch { - case o.VRFID != 0: - v.Set("vrf_id", strconv.Itoa(o.VRFID)) - case o.VRF != "": - v.Set("vrf", o.VRF) - } - - // Also prefer VLAN's NetBox ID over its VLAN VID - switch { - case len(o.VLANID) > 0: - for _, vid := range o.VLANID { - v.Add("vlan_id", strconv.Itoa(vid)) - } - case o.VLANVID != 0: - v.Set("vlan_vid", strconv.Itoa(int(o.VLANVID))) - } - - if o.Status != 0 { - v.Set("status", strconv.Itoa(o.Status)) - } - - switch { - case len(o.RoleID) > 0: - for _, r := range o.RoleID { - v.Add("role_id", strconv.Itoa(r)) - } - case len(o.Role) > 0: - for _, r := range o.Role { - v.Add("role", r) - } - } - - if o.Parent != nil { - v.Set("parent", o.Parent.String()) - } - - if o.Query != "" { - v.Set("q", o.Query) - } - - return v, nil -} - -// A prefix is the raw JSON representation of a Prefix. -type prefix struct { - ID int `json:"id"` - Family Family `json:"family"` - Prefix string `json:"prefix"` - Site *SiteIdentifier `json:"site"` - VRF *VRFIdentifier `json:"vrf"` - VLAN *VLANIdentifier `json:"vlan"` - Status Status `json:"status"` - Role *RoleIdentifier `json:"role"` - Description string `json:"description"` -} - -// MarshalJSON marshals an Prefix into JSON bytes. -func (p *Prefix) MarshalJSON() ([]byte, error) { - return json.Marshal(prefix{ - ID: p.ID, - Family: p.Family, - Prefix: p.Prefix.String(), - Site: p.Site, - VRF: p.VRF, - VLAN: p.VLAN, - Status: p.Status, - Role: p.Role, - Description: p.Description, - }) -} - -// UnmarshalJSON unmarshals JSON bytes into an Prefix, and verifies that -// the contained IP address is valid. -func (p *Prefix) UnmarshalJSON(b []byte) error { - var raw prefix - if err := json.Unmarshal(b, &raw); err != nil { - return err - } - - _, prefix, err := net.ParseCIDR(raw.Prefix) - if err != nil { - return err - } - - *p = Prefix{ - ID: raw.ID, - Family: raw.Family, - Prefix: prefix, - Site: raw.Site, - VRF: raw.VRF, - VLAN: raw.VLAN, - Status: raw.Status, - Role: raw.Role, - Description: raw.Description, - } - return nil -} diff --git a/ipam_prefixes_test.go b/ipam_prefixes_test.go deleted file mode 100644 index f431f1e1498f279ede1241332d75bffca1bacb30..0000000000000000000000000000000000000000 --- a/ipam_prefixes_test.go +++ /dev/null @@ -1,460 +0,0 @@ -// Copyright 2016 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" - "net" - "net/http" - "net/url" - "reflect" - "strconv" - "testing" -) - -func TestClientIPAMGetPrefix(t *testing.T) { - wantPrefix := testPrefix(FamilyIPv4, 1) - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/prefixes/1", wantPrefix)) - defer done() - - gotPrefix, err := c.IPAM.GetPrefix(wantPrefix.ID) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.GetPrefix: %v", err) - } - - if want, got := *wantPrefix, *gotPrefix; !prefixesEqual(want, got) { - t.Fatalf("unexpected Prefix:\n- want: %v\n- got: %v", want, got) - } -} - -func TestClientIPAMListPrefixes(t *testing.T) { - wantPrefixes := []*Prefix{ - testPrefix(FamilyIPv4, 1), - testPrefix(FamilyIPv6, 2), - } - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/prefixes/", wantPrefixes)) - defer done() - - gotPrefixes, err := c.IPAM.ListPrefixes(nil) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.ListPrefixes: %v", err) - } - - want := derefPrefixes(wantPrefixes) - got := derefPrefixes(gotPrefixes) - if !prefixSlicesEqual(want, got) { - t.Fatalf("unexpected Prefixes:\n- want: %v\n- got: %v", want, got) - } -} - -func TestListPrefixesOptionsValues(t *testing.T) { - var tests = []struct { - desc string - o *ListPrefixesOptions - v url.Values - }{ - { - desc: "empty options", - }, - { - desc: "family only", - o: &ListPrefixesOptions{ - Family: FamilyIPv4, - }, - v: url.Values{ - "family": []string{strconv.Itoa(int(FamilyIPv4))}, - }, - }, - { - desc: "1 site_id only", - o: &ListPrefixesOptions{ - SiteID: []int{1}, - }, - v: url.Values{ - "site_id": []string{"1"}, - }, - }, - { - desc: "3 site_ids only", - o: &ListPrefixesOptions{ - SiteID: []int{1, 2, 3}, - }, - v: url.Values{ - "site_id": []string{"1", "2", "3"}, - }, - }, - { - desc: "1 site only", - o: &ListPrefixesOptions{ - Site: []string{"site"}, - }, - v: url.Values{ - "site": []string{"site"}, - }, - }, - { - desc: "3 sites only", - o: &ListPrefixesOptions{ - Site: []string{"sitefoo", "sitebar", "sitebaz"}, - }, - v: url.Values{ - "site": []string{"sitefoo", "sitebar", "sitebaz"}, - }, - }, - { - desc: "site and site_id, site_id preferred", - o: &ListPrefixesOptions{ - Site: []string{"site"}, - SiteID: []int{1}, - }, - v: url.Values{ - "site_id": []string{"1"}, - }, - }, - { - desc: "vrf_id only", - o: &ListPrefixesOptions{ - VRFID: 2, - }, - v: url.Values{ - "vrf_id": []string{"2"}, - }, - }, - { - desc: "vrf only", - o: &ListPrefixesOptions{ - VRF: "vrf", - }, - v: url.Values{ - "vrf": []string{"vrf"}, - }, - }, - { - desc: "vrf and vrf_id, vrf_id preferred", - o: &ListPrefixesOptions{ - VRF: "vrf", - VRFID: 2, - }, - v: url.Values{ - "vrf_id": []string{"2"}, - }, - }, - { - desc: "1 vlan_id only", - o: &ListPrefixesOptions{ - VLANID: []int{3}, - }, - v: url.Values{ - "vlan_id": []string{"3"}, - }, - }, - { - desc: "3 vlan_ids only", - o: &ListPrefixesOptions{ - VLANID: []int{3, 4, 5}, - }, - v: url.Values{ - "vlan_id": []string{"3", "4", "5"}, - }, - }, - { - desc: "vlan_vid only", - o: &ListPrefixesOptions{ - VLANVID: 4094, - }, - v: url.Values{ - "vlan_vid": []string{"4094"}, - }, - }, - { - desc: "vlan and vlan_id, vlan_id preferred", - o: &ListPrefixesOptions{ - VLANVID: 4094, - VLANID: []int{3}, - }, - v: url.Values{ - "vlan_id": []string{"3"}, - }, - }, - { - desc: "status only", - o: &ListPrefixesOptions{ - Status: 4, - }, - v: url.Values{ - "status": []string{"4"}, - }, - }, - { - desc: "1 role_id only", - o: &ListPrefixesOptions{ - RoleID: []int{5}, - }, - v: url.Values{ - "role_id": []string{"5"}, - }, - }, - { - desc: "3 role_ids only", - o: &ListPrefixesOptions{ - RoleID: []int{5, 6, 7}, - }, - v: url.Values{ - "role_id": []string{"5", "6", "7"}, - }, - }, - { - desc: "1 role only", - o: &ListPrefixesOptions{ - Role: []string{"role"}, - }, - v: url.Values{ - "role": []string{"role"}, - }, - }, - { - desc: "3 roles only", - o: &ListPrefixesOptions{ - Role: []string{"rolefoo", "rolebar", "rolebaz"}, - }, - v: url.Values{ - "role": []string{"rolefoo", "rolebar", "rolebaz"}, - }, - }, - { - desc: "role and role_id, role_id preferred", - o: &ListPrefixesOptions{ - Role: []string{"role"}, - RoleID: []int{5}, - }, - v: url.Values{ - "role_id": []string{"5"}, - }, - }, - { - desc: "parent only", - o: &ListPrefixesOptions{ - Parent: &net.IPNet{ - IP: net.ParseIP("::1"), - Mask: net.CIDRMask(128, 128), - }, - }, - v: url.Values{ - "parent": []string{"::1/128"}, - }, - }, - { - desc: "q only", - o: &ListPrefixesOptions{ - Query: "query", - }, - v: url.Values{ - "q": []string{"query"}, - }, - }, - { - desc: "all options", - o: &ListPrefixesOptions{ - Family: FamilyIPv4, - SiteID: []int{1}, - Site: []string{"site"}, - VRFID: 2, - VRF: "vrf", - VLANID: []int{3}, - VLANVID: 4094, - Status: 4, - RoleID: []int{5}, - Role: []string{"role"}, - Parent: &net.IPNet{ - IP: net.ParseIP("::1"), - Mask: net.CIDRMask(128, 128), - }, - Query: "query", - }, - v: url.Values{ - "family": []string{strconv.Itoa(int(FamilyIPv4))}, - "site_id": []string{"1"}, - "vrf_id": []string{"2"}, - "vlan_id": []string{"3"}, - "status": []string{"4"}, - "role_id": []string{"5"}, - "parent": []string{"::1/128"}, - "q": []string{"query"}, - }, - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - 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) - } - } -} - -func TestPrefixMarshalJSON(t *testing.T) { - var tests = []struct { - desc string - p *Prefix - b []byte - }{ - { - desc: "IPv4 prefix", - p: testPrefix(FamilyIPv4, 1), - b: []byte(`{"id":1,"family":4,"prefix":"8.8.0.0/16","site":{"id":1,"name":"SiteIdentifier 1","slug":"siteidentifier1"},"vrf":{"id":1,"name":"VRFIdentifier 1","rd":"rd 1"},"vlan":{"id":1,"vid":1,"name":"vlanidentifier 1","display_name":"VLANIdentifier 1"},"status":1,"role":{"id":1,"name":"RoleIdentifier 1","slug":"roleidentifier1"},"description":"description 1"}`), - }, - { - desc: "IPv6 prefix", - p: testPrefix(FamilyIPv6, 2), - b: []byte(`{"id":2,"family":6,"prefix":"2001:4860::/32","site":{"id":2,"name":"SiteIdentifier 2","slug":"siteidentifier2"},"vrf":{"id":2,"name":"VRFIdentifier 2","rd":"rd 2"},"vlan":{"id":2,"vid":2,"name":"vlanidentifier 2","display_name":"VLANIdentifier 2"},"status":1,"role":{"id":2,"name":"RoleIdentifier 2","slug":"roleidentifier2"},"description":"description 2"}`), - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - b, err := json.Marshal(tt.p) - if err != nil { - t.Fatalf("unexpected JSON marshal error: %v", err) - } - - if want, got := tt.b, b; !bytes.Equal(want, got) { - t.Fatalf("unexpected JSON bytes:\n- want: %v\n- got: %v", - string(want), string(got)) - } - } -} - -func TestPrefixUnmarshalJSON(t *testing.T) { - var tests = []struct { - desc string - b []byte - p *Prefix - err error - }{ - { - desc: "invalid prefix", - b: []byte(`{"prefix":"foo"}`), - err: &net.ParseError{ - Type: "CIDR address", - Text: "foo", - }, - }, - { - desc: "IPv4 prefix", - b: []byte(`{"id":1,"family":4,"prefix":"8.8.0.0/16","site":{"id":1,"name":"SiteIdentifier 1","slug":"siteidentifier1"},"vrf":{"id":1,"name":"VRFIdentifier 1","rd":"rd 1"},"vlan":{"id":1,"vid":1,"name":"vlanidentifier 1","display_name":"VLANIdentifier 1"},"status":1,"role":{"id":1,"name":"RoleIdentifier 1","slug":"roleidentifier1"},"description":"description 1"}`), - p: testPrefix(FamilyIPv4, 1), - }, - { - desc: "IPv6 prefix", - b: []byte(`{"id":2,"family":6,"prefix":"2001:4860::/32","site":{"id":2,"name":"SiteIdentifier 2","slug":"siteidentifier2"},"vrf":{"id":2,"name":"VRFIdentifier 2","rd":"rd 2"},"vlan":{"id":2,"vid":2,"name":"vlanidentifier 2","display_name":"VLANIdentifier 2"},"status":1,"role":{"id":2,"name":"RoleIdentifier 2","slug":"roleidentifier2"},"description":"description 2"}`), - p: testPrefix(FamilyIPv6, 2), - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - p := new(Prefix) - err := json.Unmarshal(tt.b, p) - - if want, got := tt.err, err; !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected error:\n- want: %v\n- got: %v", - want, got) - } - if err != nil { - continue - } - - if want, got := *tt.p, *p; !prefixesEqual(want, got) { - t.Fatalf("unexpected Prefix:\n- want: %v\n- got: %v", - want, got) - } - } -} - -func prefixSlicesEqual(a, b []Prefix) bool { - if len(a) != len(b) { - return false - } - - for i := range a { - if !prefixesEqual(a[i], b[i]) { - return false - } - } - - return true -} - -func prefixesEqual(a, b Prefix) bool { - if a.ID != b.ID { - return false - } - - if a.Family != b.Family { - return false - } - - if a.Prefix.String() != b.Prefix.String() { - return false - } - - if !reflect.DeepEqual(a.Site, b.Site) { - return false - } - - if !reflect.DeepEqual(a.VRF, b.VRF) { - return false - } - - if !reflect.DeepEqual(a.VLAN, b.VLAN) { - return false - } - - if a.Status != b.Status { - return false - } - - if !reflect.DeepEqual(a.Role, b.Role) { - return false - } - - if a.Description != b.Description { - return false - } - - return true -} - -// Used to print values of Prefixs in slice, instead of memory addresses. -func derefPrefixes(prefixes []*Prefix) []Prefix { - p := make([]Prefix, len(prefixes)) - for i := range prefixes { - p[i] = *prefixes[i] - } - - return p -} diff --git a/ipam_rirs.go b/ipam_rirs.go deleted file mode 100644 index d9c19d6114beddca11f4409cdf4a99b7da345181..0000000000000000000000000000000000000000 --- a/ipam_rirs.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2016 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 ( - "fmt" - "net/http" -) - -// An RIR is a Regional Internet Registry which manages allocation of IP -// addresses. -type RIR struct { - ID int `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` -} - -// An RIRIdentifier is an RIR returned as a nested object in some top-level -// objects. Though RIR and RIRIdentifier currently share the same fields, -// this may not always be the case. It contains information which can be -// used in subsequent API calls to identify and retrieve a full RIR. -type RIRIdentifier struct { - ID int `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` -} - -// GetRIR retrieves an RIR object from NetBox by its ID. -func (s *IPAMService) GetRIR(id int) (*RIR, error) { - req, err := s.c.NewRequest( - http.MethodGet, - fmt.Sprintf("/api/ipam/rirs/%d", id), - nil, - ) - if err != nil { - return nil, err - } - - r := new(RIR) - err = s.c.Do(req, r) - return r, err -} - -// ListRIRs retrives a list of RIR objects from NetBox. -func (s *IPAMService) ListRIRs() ([]*RIR, error) { - req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/rirs/", nil) - if err != nil { - return nil, err - } - - var rs []*RIR - err = s.c.Do(req, &rs) - return rs, err -} diff --git a/ipam_rirs_test.go b/ipam_rirs_test.go deleted file mode 100644 index 1a0fd994d3ddb5f9bdb13a59bd4520ef37557f1f..0000000000000000000000000000000000000000 --- a/ipam_rirs_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2016 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 ( - "net/http" - "reflect" - "testing" -) - -func TestClientIPAMGetRIR(t *testing.T) { - wantRIR := testRIR(1) - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/rirs/1", wantRIR)) - defer done() - - gotRIR, err := c.IPAM.GetRIR(wantRIR.ID) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.GetRIR: %v", err) - } - - if want, got := *wantRIR, *gotRIR; !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected RIR:\n- want: %v\n- got: %v", want, got) - } -} - -func TestClientIPAMListRIRs(t *testing.T) { - wantRIRs := []*RIR{ - testRIR(1), - testRIR(2), - testRIR(3), - } - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/rirs/", wantRIRs)) - defer done() - - gotRIRs, err := c.IPAM.ListRIRs() - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.ListRIRs: %v", err) - } - - if want, got := derefRIRs(wantRIRs), derefRIRs(gotRIRs); !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected RIRs:\n- want: %v\n- got: %v", want, got) - } -} - -// derefRIRs is used to print values of RIRs in slice, instead of memory addresses. -func derefRIRs(rirs []*RIR) []RIR { - r := make([]RIR, len(rirs)) - for i := range rirs { - r[i] = *rirs[i] - } - - return r -} diff --git a/ipam_roles.go b/ipam_roles.go deleted file mode 100644 index 36b7aab86f2c4220be4416d457354cf288eae272..0000000000000000000000000000000000000000 --- a/ipam_roles.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2016 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 ( - "fmt" - "net/http" -) - -// A Role is a role tag which can be applied to an object such as a -// Prefix or VLAN. Role values are typically used to indicate the role -// of an object, such as infrastructure, management, customer, etc. -type Role struct { - ID int `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Weight int `json:"weight"` -} - -// A RoleIdentifier is a reduced version of a Role, returned as a nested -// object in some top-level objects. It contains information which can -// be used in subsequent API calls to identify and retrieve a full Role. -type RoleIdentifier struct { - ID int `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` -} - -// GetRole retrieves a Role object from NetBox by its ID. -func (s *IPAMService) GetRole(id int) (*Role, error) { - req, err := s.c.NewRequest( - http.MethodGet, - fmt.Sprintf("/api/ipam/roles/%d", id), - nil, - ) - if err != nil { - return nil, err - } - - r := new(Role) - err = s.c.Do(req, r) - return r, err -} - -// ListRoles retrieves a list of Role objects from NetBox. -func (s *IPAMService) ListRoles() ([]*Role, error) { - req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/roles/", nil) - if err != nil { - return nil, err - } - - var rs []*Role - err = s.c.Do(req, &rs) - return rs, err -} diff --git a/ipam_roles_test.go b/ipam_roles_test.go deleted file mode 100644 index 49e759b2457ef4c97f1ced20c3695c5044e41d91..0000000000000000000000000000000000000000 --- a/ipam_roles_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2016 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 ( - "net/http" - "reflect" - "testing" -) - -func TestClientIPAMGetRole(t *testing.T) { - wantRole := testRole(1) - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/roles/1", wantRole)) - defer done() - - gotRole, err := c.IPAM.GetRole(wantRole.ID) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.GetRole: %v", err) - } - - if want, got := *wantRole, *gotRole; !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected Role:\n- want: %v\n- got: %v", want, got) - } -} - -func TestClientIPAMListRoles(t *testing.T) { - wantRoles := []*Role{ - testRole(1), - testRole(2), - testRole(3), - } - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/roles/", wantRoles)) - defer done() - - gotRoles, err := c.IPAM.ListRoles() - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.ListRoles: %v", err) - } - - if want, got := derefRoles(wantRoles), derefRoles(gotRoles); !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected Roles:\n- want: %v\n- got: %v", want, got) - } -} - -// derefRoles is used to print values of Roles in slice, instead of memory addresses. -func derefRoles(roles []*Role) []Role { - r := make([]Role, len(roles)) - for i := range roles { - r[i] = *roles[i] - } - - return r -} diff --git a/ipam_vlans.go b/ipam_vlans.go deleted file mode 100644 index 39c912830eed80a341f19acf3b455c25252202bb..0000000000000000000000000000000000000000 --- a/ipam_vlans.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2016 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 ( - "fmt" - "net/http" - "net/url" - "strconv" -) - -// A VLANID is a 12 bit integer VLAN ID. Use its Valid method to determine -// if the contained value is a valid VLAN ID. -type VLANID int - -// Valid determines if a VLANID contains a valid VLAN ID. -func (v VLANID) Valid() bool { - // Cannot be less than 0, cannot be greater than 4096. - return v >= 0 && v <= 4096 -} - -// A VLAN is a Virtual LAN object which can be assigned to a Site. -type VLAN struct { - ID int `json:"id"` - Site *SiteIdentifier `json:"site"` - VID VLANID `json:"vid"` - Name string `json:"name"` - Status Status `json:"status"` - Role *RoleIdentifier `json:"role"` - DisplayName string `json:"display_name"` -} - -// A VLANIdentifier is a reduced version of a VLAN, returned as a nested -// object in some top-level objects. It contains information which can -// be used in subsequent API calls to identify and retrieve a full VLAN. -type VLANIdentifier struct { - ID int `json:"id"` - VID VLANID `json:"vid"` - Name string `json:"name"` - DisplayName string `json:"display_name"` -} - -// GetVLAN retrieves a VLAN object from NetBox by its ID. -func (s *IPAMService) GetVLAN(id int) (*VLAN, error) { - req, err := s.c.NewRequest( - http.MethodGet, - fmt.Sprintf("/api/ipam/vlans/%d", id), - nil, - ) - if err != nil { - return nil, err - } - - vlan := new(VLAN) - err = s.c.Do(req, vlan) - return vlan, err -} - -// ListVLANs retrives a list of VLAN objects from NetBox, filtered according -// to the parameters specified in options. -// -// If options is nil, all VLANs will be retrieved. -func (s *IPAMService) ListVLANs(options *ListVLANsOptions) ([]*VLAN, error) { - req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/vlans/", options) - if err != nil { - return nil, err - } - - var vlans []*VLAN - err = s.c.Do(req, &vlans) - return vlans, err -} - -// ListVLANsOptions is used as an argument for Client.IPAM.ListVLANs. -// Integer fields with an *ID suffix are preferred over their string -// counterparts, and if both are set, only the *ID field will be used. -type ListVLANsOptions struct { - Name string - Role []string - RoleID []int - Site []string - SiteID []int - Status []string - StatusID int - VID VLANID -} - -// Values generates a url.Values map from the data in ListVLANsOptions. -func (o *ListVLANsOptions) Values() (url.Values, error) { - if o == nil { - return nil, nil - } - - v := url.Values{} - - if o.Name != "" { - v.Set("name", o.Name) - } - - // IDs should always be preferred over string names - - switch { - case len(o.RoleID) > 0: - for _, r := range o.RoleID { - v.Add("role_id", strconv.Itoa(r)) - } - case len(o.Role) > 0: - for _, r := range o.Role { - v.Add("role", r) - } - } - - switch { - case len(o.SiteID) > 0: - for _, s := range o.SiteID { - v.Add("site_id", strconv.Itoa(s)) - } - case len(o.Site) > 0: - for _, s := range o.Site { - v.Add("site", s) - } - } - - switch { - case o.StatusID != 0: - v.Set("status_id", strconv.Itoa(o.StatusID)) - case len(o.Status) > 0: - for _, s := range o.Status { - v.Add("status", s) - } - } - - if o.VID != 0 { - v.Set("vid", strconv.Itoa(int(o.VID))) - } - - return v, nil -} diff --git a/ipam_vlans_test.go b/ipam_vlans_test.go deleted file mode 100644 index 468b122a9845f87957c6300fb9334ce52c9312aa..0000000000000000000000000000000000000000 --- a/ipam_vlans_test.go +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright 2016 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 ( - "math" - "net/http" - "net/url" - "reflect" - "testing" -) - -func TestVLANIDValid(t *testing.T) { - var tests = []struct { - id VLANID - ok bool - }{ - { - id: math.MinInt64, - }, - { - id: -1, - }, - { - id: 4097, - }, - { - id: math.MaxInt64, - }, - { - id: 0, - ok: true, - }, - { - id: 4096, - ok: true, - }, - } - - for _, tt := range tests { - if want, got := tt.ok, tt.id.Valid(); want != got { - t.Fatalf("unexpected VLANID(%d).Valid():\n- want: %v\n- got: %v", - tt.id, want, got) - } - } -} - -func TestClientIPAMGetVLAN(t *testing.T) { - wantVLAN := testVLAN(1) - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vlans/1", wantVLAN)) - defer done() - - gotVLAN, err := c.IPAM.GetVLAN(wantVLAN.ID) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.GetVLAN: %v", err) - } - - if want, got := *wantVLAN, *gotVLAN; !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected VLAN:\n- want: %v\n- got: %v", want, got) - } -} - -func TestClientIPAMListVLANs(t *testing.T) { - wantVLANs := []*VLAN{ - testVLAN(1), - testVLAN(2), - testVLAN(3), - } - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vlans/", wantVLANs)) - defer done() - - gotVLANs, err := c.IPAM.ListVLANs(nil) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.ListVLANs: %v", err) - } - - if want, got := derefVLANs(wantVLANs), derefVLANs(gotVLANs); !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected VLANs:\n- want: %v\n- got: %v", want, got) - } -} - -func TestListVLANsOptionsValues(t *testing.T) { - var tests = []struct { - desc string - o *ListVLANsOptions - v url.Values - }{ - { - desc: "empty options", - }, - { - desc: "name only", - o: &ListVLANsOptions{ - Name: "name", - }, - v: url.Values{ - "name": []string{"name"}, - }, - }, - { - desc: "1 role_id only", - o: &ListVLANsOptions{ - RoleID: []int{1}, - }, - v: url.Values{ - "role_id": []string{"1"}, - }, - }, - { - desc: "3 role_ids only", - o: &ListVLANsOptions{ - RoleID: []int{1, 2, 3}, - }, - v: url.Values{ - "role_id": []string{"1", "2", "3"}, - }, - }, - { - desc: "1 role only", - o: &ListVLANsOptions{ - Role: []string{"role"}, - }, - v: url.Values{ - "role": []string{"role"}, - }, - }, - { - desc: "3 roles only", - o: &ListVLANsOptions{ - Role: []string{"rolefoo", "rolebar", "rolebaz"}, - }, - v: url.Values{ - "role": []string{"rolefoo", "rolebar", "rolebaz"}, - }, - }, - { - desc: "role and role_id, role_id preferred", - o: &ListVLANsOptions{ - Role: []string{"role"}, - RoleID: []int{1}, - }, - v: url.Values{ - "role_id": []string{"1"}, - }, - }, - { - desc: "1 site_id only", - o: &ListVLANsOptions{ - SiteID: []int{2}, - }, - v: url.Values{ - "site_id": []string{"2"}, - }, - }, - { - desc: "3 site_ids only", - o: &ListVLANsOptions{ - SiteID: []int{2, 3, 4}, - }, - v: url.Values{ - "site_id": []string{"2", "3", "4"}, - }, - }, - { - desc: "1 site only", - o: &ListVLANsOptions{ - Site: []string{"site"}, - }, - v: url.Values{ - "site": []string{"site"}, - }, - }, - { - desc: "3 sites only", - o: &ListVLANsOptions{ - Site: []string{"sitefoo", "sitebar", "sitebaz"}, - }, - v: url.Values{ - "site": []string{"sitefoo", "sitebar", "sitebaz"}, - }, - }, - { - desc: "site and site_id, site_id preferred", - o: &ListVLANsOptions{ - Site: []string{"site"}, - SiteID: []int{2}, - }, - v: url.Values{ - "site_id": []string{"2"}, - }, - }, - { - desc: "status_id only", - o: &ListVLANsOptions{ - StatusID: 3, - }, - v: url.Values{ - "status_id": []string{"3"}, - }, - }, - { - desc: "1 status only", - o: &ListVLANsOptions{ - Status: []string{"status"}, - }, - v: url.Values{ - "status": []string{"status"}, - }, - }, - { - desc: "3 statuses only", - o: &ListVLANsOptions{ - Status: []string{"statusfoo", "statusbar", "statusbaz"}, - }, - v: url.Values{ - "status": []string{"statusfoo", "statusbar", "statusbaz"}, - }, - }, - { - desc: "status and status_id, status_id preferred", - o: &ListVLANsOptions{ - Status: []string{"status"}, - StatusID: 3, - }, - v: url.Values{ - "status_id": []string{"3"}, - }, - }, - { - desc: "vid only", - o: &ListVLANsOptions{ - VID: 4, - }, - v: url.Values{ - "vid": []string{"4"}, - }, - }, - { - desc: "all options", - o: &ListVLANsOptions{ - Name: "name", - Role: []string{"role"}, - RoleID: []int{1}, - Site: []string{"site"}, - SiteID: []int{2}, - Status: []string{"status"}, - StatusID: 3, - VID: 4, - }, - v: url.Values{ - "name": []string{"name"}, - "role_id": []string{"1"}, - "site_id": []string{"2"}, - "status_id": []string{"3"}, - "vid": []string{"4"}, - }, - }, - } - - for i, tt := range tests { - t.Logf("[%02d] test %q", i, tt.desc) - - 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) - } - } -} - -// Used to print values of VLANs in slice, instead of memory addresses. -func derefVLANs(vlans []*VLAN) []VLAN { - v := make([]VLAN, len(vlans)) - for i := range vlans { - v[i] = *vlans[i] - } - - return v -} diff --git a/ipam_vrfs.go b/ipam_vrfs.go deleted file mode 100644 index e414306448d4751864a4f720b26bf22e4310689d..0000000000000000000000000000000000000000 --- a/ipam_vrfs.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2016 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 ( - "fmt" - "net/http" -) - -// A VRF is a Virtual Routing and Forwarding device. -type VRF struct { - ID int `json:"id"` - Name string `json:"name"` - RD string `json:"rd"` - Description string `json:"description"` -} - -// A VRFIdentifier is a VRF returned as a nested object in some top-level -// objects. It contains information which can be used in subsequent API -// calls to identify and retrieve a full VRF. -type VRFIdentifier struct { - ID int `json:"id"` - Name string `json:"name"` - RD string `json:"rd"` -} - -// GetVRF retrieves a VRF object from NetBox by its ID. -func (s *IPAMService) GetVRF(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 - } - - v := new(VRF) - err = s.c.Do(req, v) - return v, err -} - -// ListVRFs retrives a list of VRF objects from NetBox. -func (s *IPAMService) ListVRFs() ([]*VRF, error) { - req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/vrfs/", nil) - if err != nil { - return nil, err - } - - var vs []*VRF - err = s.c.Do(req, &vs) - return vs, err -} diff --git a/ipam_vrfs_test.go b/ipam_vrfs_test.go deleted file mode 100644 index dc3159fb5e623a04e7c1f5a84cd42b4144cc56f1..0000000000000000000000000000000000000000 --- a/ipam_vrfs_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2016 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 ( - "net/http" - "reflect" - "testing" -) - -func TestClientIPAMGetVRF(t *testing.T) { - wantVRF := testVRF(1) - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vrfs/1", wantVRF)) - defer done() - - gotVRF, err := c.IPAM.GetVRF(wantVRF.ID) - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.GetVRF: %v", err) - } - - if want, got := *wantVRF, *gotVRF; !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected VRF:\n- want: %v\n- got: %v", want, got) - } -} - -func TestClientIPAMListVRFs(t *testing.T) { - wantVRFs := []*VRF{ - testVRF(1), - testVRF(2), - testVRF(3), - } - - c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vrfs/", wantVRFs)) - defer done() - - gotVRFs, err := c.IPAM.ListVRFs() - if err != nil { - t.Fatalf("unexpected error from Client.IPAM.ListVRFs: %v", err) - } - - if want, got := derefVRFs(wantVRFs), derefVRFs(gotVRFs); !reflect.DeepEqual(want, got) { - t.Fatalf("unexpected VRFs:\n- want: %v\n- got: %v", want, got) - } -} - -// derefVRFs is used to print values of VRFs in slice, instead of memory addresses. -func derefVRFs(vrfs []*VRF) []VRF { - v := make([]VRF, len(vrfs)) - for i := range vrfs { - v[i] = *vrfs[i] - } - - return v -} diff --git a/netbox/client.go b/netbox/client.go new file mode 100644 index 0000000000000000000000000000000000000000..194079a2cbebf2eac8414ee5895dcac28b69d4be --- /dev/null +++ b/netbox/client.go @@ -0,0 +1,212 @@ +// 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" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +// A Client is a NetBox client. It can be used to retrieve network and +// datacenter infrastructure information from a NetBox server. +type Client struct { + // DCIM provides access to methods in NetBox's DCIM API. + DCIM *DCIMService + + // IPAM provides access to methods in NetBox's IPAM API. + IPAM *IPAMService + + // Tenancy provides access to methods in NetBox's Tenancy API. + Tenancy *TenancyService + + u *url.URL + client *http.Client +} + +// NewClient returns a new instance of a NetBox client. addr specifies the address +// of the NetBox server, and client specifies an optional HTTP client to use +// for requests. +// +// If client is nil, a default HTTP client will be used. +func NewClient(addr string, client *http.Client) (*Client, error) { + if client == nil { + client = &http.Client{} + } + + // Append trailing slash there is none. This is necessary + // to be able to concat url parts in a correct manner. + // See NewRequest + if !strings.HasSuffix(addr, "/") { + addr = addr + "/" + } + + u, err := url.Parse(addr) + if err != nil { + return nil, err + } + + c := &Client{ + u: u, + client: client, + } + + c.DCIM = &DCIMService{c: c} + c.IPAM = &IPAMService{c: c} + c.Tenancy = NewTenancyService(c) + + return c, nil +} + +// NewRequest creates a HTTP request using the input HTTP method, URL +// endpoint, and a Valuer which creates URL parameters for the request. +// +// If a nil Valuer is specified, no query parameters will be sent with the +// request. +func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) { + return c.NewDataRequest(method, endpoint, options, nil) +} + +// NewDataRequest creates a HTTP request using the input HTTP method, URL +// endpoint, a Valuer which creates URL parameters for the request, and +// a io.Reader as the body of the request. +// +// If a nil Valuer is specified, no query parameters will be sent with the +// request. +// +// If a nil io.Reader is specified, no body will be sent with the request. +func (c *Client) NewDataRequest(method string, endpoint string, options Valuer, body io.Reader) (*http.Request, error) { + // Allow specifying a base path for API requests, so if a NetBox server + // resides at a path like http://example.com/netbox/, API requests will + // be sent to http://example.com/netbox/api/... + // + // Enables support of: https://github.com/digitalocean/netbox/issues/212. + // + // Remove leading slash if there is one. This is necessary to be able to + // concat url parts in a correct manner. We can not use path.Join here, + // because this always trims the trailing slash, which causes the + // Do function to always run into 301 and then retry the correct + // Location. With GET, it does work with one useless request, but it breaks + // each other http method. + // Doing this, because out-of-tree extensions are more robust. If someone + // implements an own API-call, we do not override parts of c.u, even if + // the caller uses "/api/...". + rel, err := url.Parse(strings.TrimLeft(endpoint, "/")) + if err != nil { + return nil, err + } + + u := c.u.ResolveReference(rel) + + // If no valuer specified, create a request with no query parameters + if options == nil { + return http.NewRequest(method, u.String(), body) + } + + values, err := options.Values() + if err != nil { + return nil, err + } + u.RawQuery = values.Encode() + + return http.NewRequest(method, u.String(), body) +} + +// NewJSONRequest creates a HTTP request using the input HTTP method, URL +// endpoint, a Valuer which creates URL parameters for the request, and +// an io.Reader as the body of the request. +// +// If a nil Valuer is specified, no query parameters will be sent with the +// request. +// +// The body parameter is marshaled to JSON and sent as a HTTP request body. +// Body must not be nil. +func (c *Client) NewJSONRequest(method string, endpoint string, options Valuer, body interface{}) (*http.Request, error) { + if body == nil { + return nil, errors.New("expected body to be not nil") + } + + b := new(bytes.Buffer) + err := json.NewEncoder(b).Encode(body) + if err != nil { + return nil, err + } + + req, err := c.NewDataRequest( + method, + endpoint, + options, + b, + ) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + return req, nil +} + +// Do executes an HTTP request and if v is not nil, Do unmarshals result +// JSON onto v. +func (c *Client) Do(req *http.Request, v interface{}) error { + res, err := c.client.Do(req) + if err != nil { + return err + } + defer func() { + _ = res.Body.Close() + }() + // Test if the request was successful. + // If not, do not try to decode the body. This would result in + // misleading error messages + err = httpStatusOK(res) + if err != nil { + return err + } + + if v == nil { + return nil + } + + return json.NewDecoder(res.Body).Decode(v) +} + +// httpStatusOK tests if the StatusCode of res is smaller than 300. Tries to +// Unmarshal the response into json, and returns only the detail +// if this exists. Otherwise returns the status code with the raw Body data. +func httpStatusOK(res *http.Response) error { + if res.StatusCode >= http.StatusMultipleChoices { + errDetail := struct { + Detail string `json:"detail"` + }{} + bodyData, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("%d - %v", res.StatusCode, err) + } + err = json.Unmarshal(bodyData, &errDetail) + if err == nil && errDetail.Detail != "" { + return fmt.Errorf("%d - %s", res.StatusCode, errDetail.Detail) + } + + return fmt.Errorf("%d - %s", res.StatusCode, bodyData) + } + return nil +} diff --git a/netbox/client_test.go b/netbox/client_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4170a338e1a19b4b25ca7353360e2c6cc3ea42d0 --- /dev/null +++ b/netbox/client_test.go @@ -0,0 +1,291 @@ +// 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" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "reflect" + "strconv" + "testing" +) + +func TestClientBadJSON(t *testing.T) { + c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("foo")) + }) + defer done() + + req, err := c.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal("expected no error, but an error returned") + } + + // Pass empty struct to trigger JSON unmarshaling path + var v struct{} + + err = c.Do(req, &v) + if _, ok := err.(*json.SyntaxError); !ok { + t.Fatalf("unexpected error type: %T", err) + } +} + +func TestClientBadStatusCode(t *testing.T) { + var tests = []struct { + desc string + data []byte + statusCode int + want error + }{ + { + desc: "403, but no json result", + data: []byte("foo"), + statusCode: http.StatusForbidden, + want: errors.New("403 - foo"), + }, + { + desc: "403, with json, but without detail", + data: []byte(`{"error_msg": "some error occurred"}`), + statusCode: http.StatusForbidden, + want: errors.New(`403 - {"error_msg": "some error occurred"}`), + }, + { + desc: "500, but correct json", + data: []byte(`{"detail": "some error occurred"}`), + statusCode: http.StatusInternalServerError, + want: errors.New("500 - some error occurred"), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + w.Write(tt.data) + }) + defer done() + + req, err := c.NewRequest(http.MethodGet, "/", nil) + if err != nil { + t.Fatal("expected no error, but an error returned") + } + + var v struct{} + err = c.Do(req, &v) + if want, got := tt.want, err; !reflect.DeepEqual(want, got) { + t.Fatalf("expected error:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestNewJSONRequest(t *testing.T) { + c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("foo")) + }) + defer done() + wantBody := "{\"id\":1,\"name\":\"Test 1\"}\n" + wantHeader := "application/json; charset=utf-8" + + req, err := c.NewJSONRequest(http.MethodPost, "/", nil, &struct { + ID int `json:"id"` + Name string `json:"name"` + }{ + ID: 1, + Name: "Test 1", + }) + if err != nil { + t.Fatal("expected no error, but an error returned") + } + + res, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatal("expected no error, but an error returned") + } + if want, got := wantBody, string(res); got != want { + t.Fatalf("unexpected body:\n- want: %v\n- got: %v", want, got) + } + + if want, got := wantHeader, req.Header.Get("Content-Type"); got != want { + t.Fatalf("unexpected body:\n- want: %v\n- got: %v", want, got) + } + + req, err = c.NewJSONRequest(http.MethodPost, "/", nil, nil) + if err == nil { + t.Fatal("expected an error, but there was none") + } + if req != nil { + t.Fatalf("expected a nil request, but got %v", req) + } +} + +func TestClientQueryParameters(t *testing.T) { + c := &Client{ + u: &url.URL{}, + client: &http.Client{}, + } + + const ( + wantFoo = "foo" + wantBar = 1 + ) + + req, err := c.NewRequest(http.MethodGet, "/", testValuer{ + Foo: wantFoo, + Bar: wantBar, + }) + if err != nil { + t.Fatal("expected an error, but no error returned") + } + + q := req.URL.Query() + if want, got := 2, len(q); want != got { + t.Fatalf("unexpected number of query parameters:\n- want: %v\n- got: %v", + want, got) + } + + if want, got := wantFoo, q.Get("foo"); want != got { + t.Fatalf("unexpected foo:\n- want: %v\n- got: %v", want, got) + } + + if want, got := strconv.Itoa(wantBar), q.Get("bar"); want != got { + t.Fatalf("unexpected bar:\n- want: %v\n- got: %v", want, got) + } +} + +func TestClientPrependBaseURLPath(t *testing.T) { + u, err := url.Parse("http://example.com/netbox/") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + c := &Client{ + u: u, + client: &http.Client{}, + } + + req, err := c.NewRequest(http.MethodGet, "/api/ipam/vlans", nil) + if err != nil { + t.Fatal("expected an error, but no error returned") + } + + if want, got := "/netbox/api/ipam/vlans", req.URL.Path; want != got { + t.Fatalf("unexpected URL path:\n- want: %q\n- got: %q", + want, got) + } +} + +type testValuer struct { + Foo string + Bar int +} + +func (q testValuer) Values() (url.Values, error) { + v := url.Values{} + + if q.Foo != "" { + v.Set("foo", q.Foo) + } + + if q.Bar != 0 { + v.Set("bar", strconv.Itoa(q.Bar)) + } + + return v, nil +} + +func testClient(t *testing.T, fn func(w http.ResponseWriter, r *http.Request)) (*Client, func()) { + s := httptest.NewServer(http.HandlerFunc(fn)) + + c, err := NewClient(s.URL, nil) + if err != nil { + t.Fatalf("error creating Client: %v", err) + } + + return c, func() { s.Close() } +} + +func testHandler(t *testing.T, method string, path string, v interface{}) http.HandlerFunc { + return testStatusHandler(t, method, path, v, 0) +} + +func testStatusHandler(t *testing.T, method string, path string, v interface{}, statusCode int) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if want, got := method, r.Method; want != got { + t.Fatalf("unexpected HTTP method:\n- want: %v\n- got: %v", want, got) + } + + if want, got := path, r.URL.Path; want != got { + t.Fatalf("unexpected URL path:\n- want: %v\n- got: %v", want, got) + } + + if statusCode > 0 { + w.WriteHeader(statusCode) + } + + if err := json.NewEncoder(w).Encode(v); err != nil { + t.Fatalf("error while encoding JSON: %v", err) + } + } +} + +func testTenantGroupCreate(n int) *TenantGroup { + return &TenantGroup{ + Name: fmt.Sprintf("Tenant Group %d", n), + Slug: fmt.Sprintf("tenant-group-%d", n), + } +} + +func testTenantGroup(n int) *TenantGroup { + return &TenantGroup{ + ID: n, + Name: fmt.Sprintf("Tenant Group %d", n), + Slug: fmt.Sprintf("tenant-group-%d", n), + } +} + +func testTenant(n int) *Tenant { + return testTenantWithGroup(n, testTenantGroup(n)) +} + +func testTenantCreate(n int) *Tenant { + return testTenantWithGroupCreate(n, testTenantGroup(n)) +} + +func testTenantWithGroupCreate(n int, t *TenantGroup) *Tenant { + return &Tenant{ + Name: fmt.Sprintf("Tenant %d", n), + Slug: fmt.Sprintf("tenant-%d", n), + Description: fmt.Sprintf("Tenant %d Description", n), + Comments: fmt.Sprintf("Tenant %d Comments", n), + Group: t, + } +} + +func testTenantWithGroup(n int, t *TenantGroup) *Tenant { + return &Tenant{ + ID: n, + Name: fmt.Sprintf("Tenant %d", n), + Slug: fmt.Sprintf("tenant-%d", n), + Description: fmt.Sprintf("Tenant %d Description", n), + Comments: fmt.Sprintf("Tenant %d Comments", n), + Group: t, + } +} diff --git a/dcim.go b/netbox/dcim.go similarity index 95% rename from dcim.go rename to netbox/dcim.go index 22fc5858bdfbbe6cc2b53803e37a2c61371d0494..a7f4dffd91eef822c77d72c6a028753fd08aec37 100644 --- a/dcim.go +++ b/netbox/dcim.go @@ -1,4 +1,4 @@ -// Copyright 2016 The go-netbox Authors. +// 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. diff --git a/dcim_poweroutlet.go b/netbox/doc.go similarity index 67% rename from dcim_poweroutlet.go rename to netbox/doc.go index e75b708bd5d0ed7c0c8d08f10a7719bf1169cbc2..558f7233145bec714e06b8eb29a7d28d999827fb 100644 --- a/dcim_poweroutlet.go +++ b/netbox/doc.go @@ -1,4 +1,4 @@ -// Copyright 2016 The go-netbox Authors. +// 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. @@ -12,11 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package netbox provides an API 2.0 client for DigitalOcean's NetBox IPAM +// and DCIM service. package netbox - -// PowerOutletIdentifier represents a reduced version of a power outlet object. -type PowerOutletIdentifier struct { - ID int `json:"id"` - Device *DeviceIdentifier `json:"device"` - Name string `json:"name"` -} diff --git a/netbox/generate_basic_tests.go b/netbox/generate_basic_tests.go new file mode 100644 index 0000000000000000000000000000000000000000..80d879b73416b7dbfdba24b4e68b82c80ef04028 --- /dev/null +++ b/netbox/generate_basic_tests.go @@ -0,0 +1,383 @@ +// 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. + +//+build ignore + +package main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "log" + "os" + "text/template" + "time" +) + +func main() { + typeName := flag.String("type-name", "Example", "Name of the type to use (e.g. TenantGroup).") + serviceName := flag.String("service-name", "ExampleService", "Name of the service to create (e.g. TenantGroupsService).") + endpoint := flag.String("endpoint", "tenancy", "Name of the endpoint (e.g. dcim, ipam, tenancy).") + service := flag.String("service", "example", "Name of the service below endpoint (e.g. tenant-groups).") + clientEndpoint := flag.String("client-endpoint", "Tenancy", "Name of the client endpoint (e.g. DCIM, IPAM, Tenancy).") + clientService := flag.String("client-service", "TenantGroups", "Name of the client service (e.g. TenantGroups, Tenants).") + withoutListOpts := flag.Bool("without-list-opts", false, "Disable list options for this endpoint.") + + flag.Parse() + + b := &bytes.Buffer{} + functionsTemplate.Execute(b, struct { + Timestamp time.Time + TypeName string + ServiceName string + Endpoint string + Service string + ClientEndpoint string + ClientService string + ListOpts bool + JSONTag func(string) string + }{ + Timestamp: time.Now(), + TypeName: *typeName, + ServiceName: *serviceName, + Endpoint: *endpoint, + Service: *service, + ClientEndpoint: *clientEndpoint, + ClientService: *clientService, + ListOpts: !*withoutListOpts, + JSONTag: func(name string) string { return "`json:\"" + name + "\"`" }, + }) + + // go fmt + res, err := format.Source(b.Bytes()) + if err != nil { + log.Fatal(err) + } + + f, err := os.Create(fmt.Sprintf("%s_%s_basic_test.go", *endpoint, *service)) + if err != nil { + log.Fatal(err) + } + defer f.Close() + _, err = f.Write(res) + if err != nil { + log.Fatal(err) + } +} + +var functionsTemplate = template.Must(template.New("").Parse(`// 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 {{ .TypeName }}.MarshalJSON is what you want, +// but not here as a return in testHandler +type serverData{{ .TypeName }} {{ .TypeName }} + +func convertToServerData{{ .TypeName }}(data []*{{ .TypeName }}) []*serverData{{ .TypeName }} { + dataWant := make([]*serverData{{ .TypeName }}, len(data)) + for i := range data { + tmp := serverData{{ .TypeName }}(*data[i]) + dataWant[i] = &tmp + } + return dataWant +} + +func TestBasic{{ .TypeName }}Get(t *testing.T) { + var tests = []struct { + desc string + want *{{ .TypeName }} + }{ + { + desc: "Simple {{ .TypeName }}", + want: test{{ .TypeName }}(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverData{{ .TypeName }}(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/{{ .Endpoint }}/{{ .Service }}/1/", &serverData)) + defer done() + + res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.{{ .ClientEndpoint }}.{{ .ClientService }}.Get: %v", err) + } + + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected {{ .TypeName }}:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasic{{ .TypeName }}Get404(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/{{ .Endpoint }}/{{ .Service }}/1/", &struct { + Detail string {{ call .JSONTag "detail" }} + }{ + Detail: "Not found.", + }, + http.StatusNotFound)) + defer done() + + res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Get(1) + errstr := "404 - Not found." + if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error from Client.{{ .ClientEndpoint }}.{{ .ClientService }}.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 TestBasicListExtract{{ .TypeName }}(t *testing.T) { + want := []*{{ .TypeName }}{ + test{{ .TypeName }}(1), + test{{ .TypeName }}(2), + } + serverWant := convertToServerData{{ .TypeName }}(want) + serverData, _ := json.Marshal(serverWant) + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/{{ .Endpoint }}/{{ .Service }}/", &pageData{ + Count: 2, + NextURL: "", + PreviousURL: "", + Results: serverData, + })) + defer done() + + {{ if .ListOpts -}} + page := c.{{ .ClientEndpoint }}.{{ .ClientService }}.List(nil) + {{ else }} + page := c.{{ .ClientEndpoint }}.{{ .ClientService }}.List() + {{ end }} + if page == nil { + t.Fatalf("unexpexted result from c.{{ .ClientEndpoint }}.{{ .ClientService }}.List.") + } + + got := []*{{ .TypeName }}{} + counter := 0 + for page.Next() { + var err error + got, err = c.{{ .ClientEndpoint }}.{{ .ClientService }}.Extract(page) + if err != nil { + t.Fatalf("unexpected error from c.{{ .ClientEndpoint }}.{{ .ClientService }}.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 TestBasicCreate{{ .TypeName }}(t *testing.T) { + var tests = []struct { + desc string + data *{{ .TypeName }} + want int + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 0", + data: test{{ .TypeName }}Create(1), + want: 1, + status: 0, + errstr: "", + serverData: test{{ .TypeName }}(1), + }, + { + desc: "Create duplicate", + data: test{{ .TypeName }}Create(1), + want: 0, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"{{ .ServiceName }} with this name already exists.\"]}\n", + serverData: &struct { + Name []string {{ call .JSONTag "name" }} + }{ + Name: []string{"{{ .ServiceName }} 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/{{ .Endpoint }}/{{ .Service }}/", 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.{{ .ClientEndpoint }}.{{ .ClientService }}.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 {{ .TypeName }}:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicUpdate{{ .TypeName }}(t *testing.T) { + var tests = []struct { + desc string + data *{{ .TypeName }} + want int + serverData interface{} + status int + errstr string + }{ + { + desc: "Update with ID 1", + data: test{{ .TypeName }}(1), + want: 1, + serverData: test{{ .TypeName }}(1), + status: 0, + errstr: "", + }, + { + desc: "Update not found", + data: test{{ .TypeName }}(1), + want: 0, + serverData: &struct { + Detail string + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + { + desc: "Update to duplicate", + data: test{{ .TypeName }}(1), + want: 0, + serverData: &struct { + Name []string {{ call .JSONTag "name" }} + }{ + Name: []string{"{{ .ServiceName }} with this name already exists."}, + }, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"{{ .ServiceName }} 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/{{ .Endpoint }}/{{ .Service }}/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.{{ .ClientEndpoint }}.{{ .ClientService }}.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 {{ .TypeName }}:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicDelete{{ .TypeName }}(t *testing.T) { + var tests = []struct { + desc string + data *{{ .TypeName }} + serverData interface{} + status int + errstr string + }{ + { + desc: "Delete ID 1", + data: test{{ .TypeName }}(1), + serverData: test{{ .TypeName }}(1), + status: 0, + errstr: "", + }, + { + desc: "Delete not Found", + data: test{{ .TypeName }}(1), + serverData: &struct { + Detail string {{ call .JSONTag "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/{{ .Endpoint }}/{{ .Service }}/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.{{ .ClientEndpoint }}.{{ .ClientService }}.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/generate_functions.go b/netbox/generate_functions.go new file mode 100644 index 0000000000000000000000000000000000000000..9411f8c3b8c218d65d1319d8d8bed90f6d907839 --- /dev/null +++ b/netbox/generate_functions.go @@ -0,0 +1,204 @@ +// 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. + +//+build ignore + +package main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "log" + "os" + "text/template" + "time" +) + +func main() { + typeName := flag.String("type-name", "Example", "Name of the type to use (e.g. TenantGroup).") + serviceName := flag.String("service-name", "ExampleService", "Name of the service to create (e.g. TenantGroupsService).") + endpoint := flag.String("endpoint", "tenancy", "Name of the endpoint (e.g. dcim, ipam, tenancy).") + service := flag.String("service", "example", "Name of the service below endpoint (e.g. tenant-groups).") + updateTypeName := flag.String("update-type-name", "", "Name of the type to use for creates and updates, to change the marshal behavior. Default typeName.") + withoutListOpts := flag.Bool("without-list-opts", false, "Disable list options for this endpoint.") + + flag.Parse() + + if *updateTypeName == "" { + *updateTypeName = *typeName + } + + b := &bytes.Buffer{} + functionsTemplate.Execute(b, struct { + Timestamp time.Time + TypeName string + UpdateTypeName string + ServiceName string + Endpoint string + Service string + ListOpts bool + }{ + Timestamp: time.Now(), + TypeName: *typeName, + UpdateTypeName: *updateTypeName, + ServiceName: *serviceName, + Endpoint: *endpoint, + Service: *service, + ListOpts: !*withoutListOpts, + }) + + // go fmt + res, err := format.Source(b.Bytes()) + if err != nil { + log.Fatal(err) + } + + f, err := os.Create(fmt.Sprintf("%s_%s.go", *endpoint, *service)) + if err != nil { + log.Fatal(err) + } + defer f.Close() + _, err = f.Write(res) + if err != nil { + log.Fatal(err) + } +} + +var functionsTemplate = template.Must(template.New("").Parse(`// 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" +) + +// {{ .ServiceName }} is used in a Client to access NetBox's {{ .Endpoint }}/{{ .Service }} API methods. +type {{ .ServiceName }} struct { + c *Client +} + +// Get retrieves an {{ .TypeName }} object from NetBox by its ID. +func (s *{{ .ServiceName }}) Get(id int) (*{{ .TypeName }}, error) { + req, err := s.c.NewRequest( + http.MethodGet, + fmt.Sprintf("api/{{ .Endpoint }}/{{ .Service }}/%d/", id), + nil, + ) + if err != nil { + return nil, err + } + + t := new({{ .TypeName }}) + 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. +{{ if .ListOpts -}} +func (s *{{ .ServiceName }}) List(options *List{{ .TypeName }}Options) *Page { + return NewPage(s.c, "api/{{ .Endpoint }}/{{ .Service }}/", options) +} +{{ else -}} +func (s *{{ .ServiceName }}) List() *Page { + return NewPage(s.c, "api/{{ .Endpoint }}/{{ .Service }}/", nil) +} +{{ end }} + +// Extract retrives a list of {{ .TypeName }} objects from page. +func (s *{{ .ServiceName }}) Extract(page *Page) ([]*{{ .TypeName }}, error) { + if err := page.Err(); err != nil { + return nil, err + } + + var groups []*{{ .TypeName }} + if err := json.Unmarshal(page.data.Results, &groups); err != nil { + return nil, err + } + return groups, nil +} + +// Create creates a new {{ .TypeName }} object in NetBox and returns the ID of the new object. +func (s *{{ .ServiceName }}) Create(data *{{ .TypeName }}) (int, error) { + req, err := s.c.NewJSONRequest(http.MethodPost, "api/{{ .Endpoint }}/{{ .Service }}/", nil, data) + if err != nil { + return 0, err + } + + g := new({{ .UpdateTypeName }}) + err = s.c.Do(req, g) + if err != nil { + return 0, err + } + return g.ID, nil +} + +// Update changes an existing {{ .TypeName }} object in NetBox, and returns the ID of the new object. +func (s *{{ .ServiceName }}) Update(data *{{ .TypeName }}) (int, error) { + req, err := s.c.NewJSONRequest( + http.MethodPatch, + fmt.Sprintf("api/{{ .Endpoint }}/{{ .Service }}/%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 {{ .UpdateTypeName }} correctly, + // everything went fine, and we do not need to update data. + g := new({{ .UpdateTypeName }}) + err = s.c.Do(req, g) + if err != nil { + return 0, err + } + return g.ID, nil +} + +// Delete deletes an existing {{ .TypeName }} object from NetBox. +func (s *{{ .ServiceName }}) Delete(data *{{ .TypeName }}) error { + req, err := s.c.NewRequest( + http.MethodDelete, + fmt.Sprintf("api/{{ .Endpoint }}/{{ .Service }}/%d/", data.ID), + nil, + ) + if err != nil { + return err + } + + return s.c.Do(req, nil) +} +`)) diff --git a/ipam.go b/netbox/ipam.go similarity index 94% rename from ipam.go rename to netbox/ipam.go index dabd3d07dcb196157c6b08f3278d5e8ee81d99e9..c61d3fc77a8a4a4b3d9c34bba1bde735680543da 100644 --- a/ipam.go +++ b/netbox/ipam.go @@ -1,4 +1,4 @@ -// Copyright 2016 The go-netbox Authors. +// 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. diff --git a/netbox.go b/netbox/netbox.go similarity index 98% rename from netbox.go rename to netbox/netbox.go index 0ff0fb8175615bdd1491937d51fa4598428fd68f..298eeebb1c66647ec22a54572630d37df9e5eadc 100644 --- a/netbox.go +++ b/netbox/netbox.go @@ -1,4 +1,4 @@ -// Copyright 2016 The go-netbox Authors. +// 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. diff --git a/netbox_test.go b/netbox/netbox_test.go similarity index 60% rename from netbox_test.go rename to netbox/netbox_test.go index f3bd44cf210e6b33f3db127fe067ec1b9d7187a8..f7e8d5dd6b8aaa33631d2a2af8bd87c152727a01 100644 --- a/netbox_test.go +++ b/netbox/netbox_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 The go-netbox Authors. +// 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. @@ -25,33 +25,46 @@ import ( func TestFamilyValid(t *testing.T) { var tests = []struct { - f Family - ok bool + desc string + f Family + ok bool }{ { - f: math.MinInt64, + desc: "Test math.MinInt64", + f: math.MinInt64, }, { - f: math.MaxInt64, + desc: "Test math.MaxInt64", + f: math.MaxInt64, }, { - f: FamilyIPv4, - ok: true, + desc: "Test FamilyIPv4", + f: FamilyIPv4, + ok: true, }, { - f: FamilyIPv6, - ok: true, + desc: "Test FamilyIPv6", + f: FamilyIPv6, + ok: true, }, } - for _, tt := range tests { - if want, got := tt.ok, tt.f.Valid(); want != got { - t.Fatalf("unexpected Family(%d).Valid():\n- want: %v\n- got: %v", - tt.f, want, got) - } + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + if want, got := tt.ok, tt.f.Valid(); want != got { + t.Fatalf("unexpected Family(%d).Valid():\n- want: %v\n- got: %v", + tt.f, want, got) + } + }) } } +type testSimple struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + // ExampleNewClient demonstrates usage of the Client type. func ExampleNewClient() { // Sets up a minimal, mocked NetBox server @@ -64,33 +77,31 @@ func ExampleNewClient() { panic(fmt.Sprintf("failed to create netbox.Client: %v", err)) } - // Retrieve an IPAddress with ID 1 - ip, err := c.IPAM.GetIPAddress(1) + res := testSimple{} + req, err := c.NewRequest(http.MethodGet, "/", nil) if err != nil { - panic(fmt.Sprintf("failed to retrieve IP address: %v", err)) + panic(err) } - fmt.Printf("IP #%03d: %s (%s)\n", ip.ID, ip.Address.String(), ip.Family) + err = c.Do(req, &res) + if err != nil { + panic(err) + } - // Output: - // IP #001: 192.168.1.1/32 (IPv4) + fmt.Printf("%v\n", res) } // exampleServer creates a test HTTP server which returns its address and // can be closed using the returned closure. func exampleServer() (string, func()) { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ip := struct { - ID int `json:"id"` - Family Family `json:"family"` - Address string `json:"address"` - }{ - ID: 1, - Family: FamilyIPv4, - Address: "192.168.1.1/32", + simple := testSimple{ + ID: 1, + Name: "Test 1", + Slug: "test-1", } - _ = json.NewEncoder(w).Encode(ip) + _ = json.NewEncoder(w).Encode(simple) })) return s.URL, func() { s.Close() } diff --git a/netbox/page.go b/netbox/page.go new file mode 100644 index 0000000000000000000000000000000000000000..23e2d5d152db0632b7ebd13d9f2ca6296f79491e --- /dev/null +++ b/netbox/page.go @@ -0,0 +1,174 @@ +// 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" + "errors" + "net/http" + "net/url" + "strconv" +) + +// A Page contains all necessary information about the current position +// in a paged result. It is used to walk over all pages from List calls. +type Page struct { + limit int + offset int + done bool + c *Client + data pageData + endpoint string + options Valuer + err error +} + +// NewPage Returns a new Page to walk over List calls. +func NewPage(client *Client, endpoint string, options Valuer) *Page { + return &Page{ + limit: 50, // netbox server default + c: client, + endpoint: endpoint, + options: options, + } +} + +// pageData is the internal representation of a page. +type pageData struct { + Count int `json:"count"` + NextURL string `json:"next"` + PreviousURL string `json:"previous"` + Results json.RawMessage `json:"results"` +} + +// Values implements the Valuer interface. One could pass options +// to a List call. These must be extended by limit and offset to +// get the appropriate next page. +func (p *Page) Values() (url.Values, error) { + if p == nil { + return nil, errors.New("page not defined") + } + v := url.Values{} + if p.options != nil { + opts, err := p.options.Values() + if err != nil { + return nil, err + } + if opts != nil { + v = opts + } + } + v.Set("limit", strconv.Itoa(p.limit)) + v.Set("offset", strconv.Itoa(p.offset)) + + return v, nil +} + +// Next advances to the next page of API results. When Next returns false, +// no more results are available. +func (p *Page) Next() bool { + if p == nil { + return false + } + if p.done { + return false + } + + req, err := p.c.NewRequest(http.MethodGet, p.endpoint, p) + if err != nil { + p.err = err + return false + } + + p.data = pageData{} + err = p.c.Do(req, &p.data) + if err != nil { + p.err = err + return false + } + + if p.data.NextURL == "" { + // We are on the last page, so we still need to return true to + // indicate that there is still data to process. But we set + // p.done to true, so the next "Next()" returns false. + p.done = true + } else { + p.setNextURL(p.data.NextURL) + } + return true + +} + +// setNext sets limit and offset parameter for the next page. +func (p *Page) setNext(limit int, offset int) { + p.limit = limit + p.offset = offset +} + +// setNextURL extracts limit and offset from the nextURL, obtained from the result. +// Under the hood, it uses setNext to finally set those parameters. +func (p *Page) setNextURL(urlStr string) { + nextURL, err := url.Parse(urlStr) + if err != nil { + // We dont want to cancel this run, since there is data, + // but do not want to run into Next + p.setErr(err) + return + } + + query, err := url.ParseQuery(nextURL.RawQuery) + if err != nil { + // Same like above + p.setErr(err) + return + } + + limits := query["limit"] + offsets := query["offset"] + if len(limits) == 0 { + p.setErr(errors.New("no such query parameter limit")) + return + } + if len(offsets) == 0 { + p.setErr(errors.New("no such query parameter offset")) + return + } + + limit, err := strconv.Atoi(limits[0]) + if err != nil { + p.setErr(err) + return + } + + offset, err := strconv.Atoi(offsets[0]) + if err != nil { + p.setErr(err) + return + } + p.setNext(limit, offset) +} + +// Err returns the internal err field. This should be called right after +// the for to get any errors occured during Next() +func (p *Page) Err() error { + return p.err +} + +// setErr sets an internal err field. +func (p *Page) setErr(err error) { + if p.err == nil { + p.err = err + } +} diff --git a/netbox/page_test.go b/netbox/page_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0af570f712ab175cbd5be1ac82a711bc40d141d2 --- /dev/null +++ b/netbox/page_test.go @@ -0,0 +1,244 @@ +// 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 ( + "errors" + "fmt" + "net/http" + "net/url" + "reflect" + "strconv" + "testing" +) + +type testOptions struct{} + +func (t *testOptions) Values() (url.Values, error) { + return url.Values{ + "Hello": []string{"World"}, + "limit": []string{"30"}, + }, nil +} + +func TestPageValues(t *testing.T) { + + var tests = []struct { + desc string + page *Page + want url.Values + err error + }{ + { + desc: "nil page", + page: nil, + want: nil, + err: errors.New("page not defined"), + }, + { + desc: "options nil", + page: NewPage(nil, "/", nil), + want: url.Values{ + "limit": []string{"50"}, + "offset": []string{"0"}, + }, + err: nil, + }, + { + desc: "options set", + page: NewPage(nil, "/", &testOptions{}), + want: url.Values{ + "Hello": []string{"World"}, + "limit": []string{"50"}, + "offset": []string{"0"}, + }, + err: nil, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + res, err := tt.page.Values() + if want, got := tt.err, err; !reflect.DeepEqual(want, got) { + t.Fatalf("unecpected error:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected values:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestNext(t *testing.T) { + var pages = []pageData{ + { + Count: 99, + NextURL: "http://example.com/?limit=50&offset=50", + PreviousURL: "", + Results: []byte("{\"Hello\": \"World\"}"), + }, + { + Count: 99, + NextURL: "", + PreviousURL: "http://example.com/?limit=50&offset=0", + Results: []byte("{\"Hello\": \"World\"}"), + }, + } + + c, done := testClient(t, testHandler(t, http.MethodGet, "/", pages[0])) + defer done() + + p := NewPage(c, "/", nil) + if !p.Next() { + t.Fatal("Expected another page, got none.") + } + p.c, done = testClient(t, testHandler(t, http.MethodGet, "/", pages[1])) + defer done() + if !p.Next() { + t.Fatal("Expected another page, got none.") + } + if p.Next() { + t.Fatal("Did not expect another page, but got one.") + } + if err := p.Err(); err != nil { + t.Fatalf("Did not expect an error: %v", err) + } +} + +func TestPageWithError(t *testing.T) { + c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("foo")) + }) + defer done() + + p := NewPage(c, "/", &testOptions{}) + if p.Next() { + t.Fatal("Did not expect any page, but got one.") + } + if want, got := errors.New("403 - foo"), p.Err(); !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } +} + +func TestErr(t *testing.T) { + var tests = []struct { + desc string + set error + want error + }{ + { + desc: "Err nil", + set: nil, + want: nil, + }, + { + desc: "Err set", + set: errors.New("This is broken"), + want: errors.New("This is broken"), + }, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + p := NewPage(nil, "/", nil) + p.setErr(tt.set) + if want, got := tt.want, p.Err(); !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestSetNext(t *testing.T) { + p := NewPage(nil, "/", nil) + p.setNext(99, 88) + + if want, got := 99, p.limit; want != got { + t.Fatalf("unexpected limit:\n- want: %v\n- got: %v", want, got) + } + if want, got := 88, p.offset; want != got { + t.Fatalf("unexpected offset:\n- want: %v\n- got: %v", want, got) + } +} + +func TestSetNextURL(t *testing.T) { + var tests = []struct { + desc string + url string + wantOffset int + wantLimit int + err error + }{ + { + desc: "No Params returned", + url: "http://example.com", + wantOffset: 0, + wantLimit: 50, + err: errors.New("no such query parameter limit"), + }, + { + desc: "No Offset returned", + url: "http://example.com?limit=20", + wantOffset: 0, + wantLimit: 50, + err: errors.New("no such query parameter offset"), + }, + { + desc: "Limit not an int", + url: "http://example.com?limit=hello&offset=world", + wantOffset: 0, + wantLimit: 50, + err: &strconv.NumError{ + Func: "Atoi", + Num: "hello", + Err: strconv.ErrSyntax, + }, + }, + { + desc: "Offset not an int", + url: "http://example.com?limit=50&offset=world", + wantOffset: 0, + wantLimit: 50, + err: &strconv.NumError{ + Func: "Atoi", + Num: "world", + Err: strconv.ErrSyntax, + }, + }, + { + desc: "Correct limit and offset", + url: "http://example.com?limit=99&offset=88", + wantOffset: 88, + wantLimit: 99, + err: nil, + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + p := NewPage(nil, "/", nil) + p.setNextURL(tt.url) + if want, got := tt.wantOffset, p.offset; want != got { + t.Fatalf("unexpected offset:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.wantLimit, p.limit; want != got { + t.Fatalf("unexpected limit:\n- want: %v\n- got: %v", want, got) + } + if want, got := tt.err, p.Err(); reflect.TypeOf(want) != reflect.TypeOf(got) { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) + } + }) + } +} diff --git a/dcim_powerports.go b/netbox/tenancy.go similarity index 52% rename from dcim_powerports.go rename to netbox/tenancy.go index 5e606263df7115976b1cf5a9cf585e6f596f3876..b44124f2a8a1211831becba9475d548d1cb2e81f 100644 --- a/dcim_powerports.go +++ b/netbox/tenancy.go @@ -1,4 +1,4 @@ -// Copyright 2016 The go-netbox Authors. +// 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. @@ -14,10 +14,22 @@ package netbox -// PowerPort represents a power port object. -type PowerPort struct { - ID int `json:"id"` - Name string `json:"name"` - PowerOutlet *PowerOutletIdentifier `json:"power_outlet"` - ConnectionStatus bool `json:"connection_status"` +// A TenancyService is udes in a Client to access NetBox's Tenancy API methods. +type TenancyService struct { + c *Client + TenantGroups *TenantGroupsService + Tenants *TenantsService +} + +// NewTenancyService returns a TenancyService initialized with all sub-services. +func NewTenancyService(client *Client) *TenancyService { + return &TenancyService{ + c: client, + TenantGroups: &TenantGroupsService{ + c: client, + }, + Tenants: &TenantsService{ + c: client, + }, + } } diff --git a/netbox/tenancy_tenant-groups.go b/netbox/tenancy_tenant-groups.go new file mode 100644 index 0000000000000000000000000000000000000000..8e1a62ad2a14fbf0aeb041c8f16596f2318da7b4 --- /dev/null +++ b/netbox/tenancy_tenant-groups.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" +) + +// TenantGroupsService is used in a Client to access NetBox's tenancy/tenant-groups API methods. +type TenantGroupsService struct { + c *Client +} + +// Get retrieves an TenantGroup object from NetBox by its ID. +func (s *TenantGroupsService) Get(id int) (*TenantGroup, error) { + req, err := s.c.NewRequest( + http.MethodGet, + fmt.Sprintf("api/tenancy/tenant-groups/%d/", id), + nil, + ) + if err != nil { + return nil, err + } + + t := new(TenantGroup) + 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 *TenantGroupsService) List() *Page { + return NewPage(s.c, "api/tenancy/tenant-groups/", nil) +} + +// Extract retrives a list of TenantGroup objects from page. +func (s *TenantGroupsService) Extract(page *Page) ([]*TenantGroup, error) { + if err := page.Err(); err != nil { + return nil, err + } + + var groups []*TenantGroup + if err := json.Unmarshal(page.data.Results, &groups); err != nil { + return nil, err + } + return groups, nil +} + +// Create creates a new TenantGroup object in NetBox and returns the ID of the new object. +func (s *TenantGroupsService) Create(data *TenantGroup) (int, error) { + req, err := s.c.NewJSONRequest(http.MethodPost, "api/tenancy/tenant-groups/", nil, data) + if err != nil { + return 0, err + } + + g := new(TenantGroup) + err = s.c.Do(req, g) + if err != nil { + return 0, err + } + return g.ID, nil +} + +// Update changes an existing TenantGroup object in NetBox, and returns the ID of the new object. +func (s *TenantGroupsService) Update(data *TenantGroup) (int, error) { + req, err := s.c.NewJSONRequest( + http.MethodPatch, + fmt.Sprintf("api/tenancy/tenant-groups/%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 TenantGroup correctly, + // everything went fine, and we do not need to update data. + g := new(TenantGroup) + err = s.c.Do(req, g) + if err != nil { + return 0, err + } + return g.ID, nil +} + +// Delete deletes an existing TenantGroup object from NetBox. +func (s *TenantGroupsService) Delete(data *TenantGroup) error { + req, err := s.c.NewRequest( + http.MethodDelete, + fmt.Sprintf("api/tenancy/tenant-groups/%d/", data.ID), + nil, + ) + if err != nil { + return err + } + + return s.c.Do(req, nil) +} diff --git a/netbox/tenancy_tenant-groups_basic_test.go b/netbox/tenancy_tenant-groups_basic_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2e3322e38d8a969f7887bec47ef8ac5ee61ed8c7 --- /dev/null +++ b/netbox/tenancy_tenant-groups_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_functions.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 TenantGroup.MarshalJSON is what you want, +// but not here as a return in testHandler +type serverDataTenantGroup TenantGroup + +func convertToServerDataTenantGroup(data []*TenantGroup) []*serverDataTenantGroup { + dataWant := make([]*serverDataTenantGroup, len(data)) + for i := range data { + tmp := serverDataTenantGroup(*data[i]) + dataWant[i] = &tmp + } + return dataWant +} + +func TestBasicTenantGroupGet(t *testing.T) { + var tests = []struct { + desc string + want *TenantGroup + }{ + { + desc: "Simple TenantGroup", + want: testTenantGroup(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverDataTenantGroup(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenant-groups/1/", &serverData)) + defer done() + + res, err := c.Tenancy.TenantGroups.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.Tenancy.TenantGroups.Get: %v", err) + } + + if want, got := tt.want, res; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicTenantGroupGet404(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/tenancy/tenant-groups/1/", &struct { + Detail string `json:"detail"` + }{ + Detail: "Not found.", + }, + http.StatusNotFound)) + defer done() + + res, err := c.Tenancy.TenantGroups.Get(1) + errstr := "404 - Not found." + if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error from Client.Tenancy.TenantGroups.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 TestBasicListExtractTenantGroup(t *testing.T) { + want := []*TenantGroup{ + testTenantGroup(1), + testTenantGroup(2), + } + serverWant := convertToServerDataTenantGroup(want) + serverData, _ := json.Marshal(serverWant) + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenant-groups/", &pageData{ + Count: 2, + NextURL: "", + PreviousURL: "", + Results: serverData, + })) + defer done() + + page := c.Tenancy.TenantGroups.List() + + if page == nil { + t.Fatalf("unexpexted result from c.Tenancy.TenantGroups.List.") + } + + got := []*TenantGroup{} + counter := 0 + for page.Next() { + var err error + got, err = c.Tenancy.TenantGroups.Extract(page) + if err != nil { + t.Fatalf("unexpected error from c.Tenancy.TenantGroups.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 TestBasicCreateTenantGroup(t *testing.T) { + var tests = []struct { + desc string + data *TenantGroup + want int + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 0", + data: testTenantGroupCreate(1), + want: 1, + status: 0, + errstr: "", + serverData: testTenantGroup(1), + }, + { + desc: "Create duplicate", + data: testTenantGroupCreate(1), + want: 0, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"TenantGroupsService with this name already exists.\"]}\n", + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"TenantGroupsService 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/tenancy/tenant-groups/", 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.Tenancy.TenantGroups.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 TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicUpdateTenantGroup(t *testing.T) { + var tests = []struct { + desc string + data *TenantGroup + want int + serverData interface{} + status int + errstr string + }{ + { + desc: "Update with ID 1", + data: testTenantGroup(1), + want: 1, + serverData: testTenantGroup(1), + status: 0, + errstr: "", + }, + { + desc: "Update not found", + data: testTenantGroup(1), + want: 0, + serverData: &struct { + Detail string + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + { + desc: "Update to duplicate", + data: testTenantGroup(1), + want: 0, + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"TenantGroupsService with this name already exists."}, + }, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"TenantGroupsService 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/tenancy/tenant-groups/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.Tenancy.TenantGroups.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 TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicDeleteTenantGroup(t *testing.T) { + var tests = []struct { + desc string + data *TenantGroup + serverData interface{} + status int + errstr string + }{ + { + desc: "Delete ID 1", + data: testTenantGroup(1), + serverData: testTenantGroup(1), + status: 0, + errstr: "", + }, + { + desc: "Delete not Found", + data: testTenantGroup(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/tenancy/tenant-groups/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.Tenancy.TenantGroups.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/tenancy_tenant-groups_test.go b/netbox/tenancy_tenant-groups_test.go new file mode 100644 index 0000000000000000000000000000000000000000..351110344a90abdfce71e44f165528b4c22c95a8 --- /dev/null +++ b/netbox/tenancy_tenant-groups_test.go @@ -0,0 +1,83 @@ +// 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" + "reflect" + "testing" +) + +func TestTenantGroupUnmarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data []byte + want *TenantGroup + }{ + { + desc: "full", + data: []byte(`{ "id": 1, "name": "Tenant Group 1", "slug": "tenant-group-1", "custom_fields": {} }`), + want: testTenantGroup(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + result := new(TenantGroup) + err := json.Unmarshal(tt.data, result) + if err != nil { + t.Fatalf("unexpected error from TenantGroup.UnmarshalJSON: %v", err) + } + + if want, got := tt.want, result; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestTenantGroupMarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data *TenantGroup + want []byte + }{ + { + desc: "With TenantGroup.ID", + data: testTenantGroup(1), + want: []byte(`{"id":1,"name":"Tenant Group 1","slug":"tenant-group-1"}`), + }, + { + desc: "No TenantGroup.ID", + data: testTenantGroup(0), + want: []byte(`{"name":"Tenant Group 0","slug":"tenant-group-0"}`), + }, + } + + 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 TenantGroup.MarshalJSON: %v", err) + } + + if want, got := tt.want, result; bytes.Compare(want, got) != 0 { + t.Fatalf("unexpected TenantGroup:\n- want: %s\n- got: %s", want, got) + } + }) + } +} diff --git a/netbox/tenancy_tenant-groups_types.go b/netbox/tenancy_tenant-groups_types.go new file mode 100644 index 0000000000000000000000000000000000000000..813ef83d5829e3894af75bf591d3016cb6bcaa8f --- /dev/null +++ b/netbox/tenancy_tenant-groups_types.go @@ -0,0 +1,25 @@ +// 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 + +// A TenantGroup is a representation of netbox tenant-groups +type TenantGroup struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +//go:generate go run generate_functions.go -type-name TenantGroup -service-name TenantGroupsService -endpoint tenancy -service tenant-groups -without-list-opts +//go:generate go run generate_basic_tests.go -type-name TenantGroup -service-name TenantGroupsService -endpoint tenancy -service tenant-groups -client-endpoint Tenancy -client-service TenantGroups -without-list-opts diff --git a/netbox/tenancy_tenants.go b/netbox/tenancy_tenants.go new file mode 100644 index 0000000000000000000000000000000000000000..0c265acf8db817f25fa03143c90327f1bdfa3def --- /dev/null +++ b/netbox/tenancy_tenants.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" +) + +// TenantsService is used in a Client to access NetBox's tenancy/tenants API methods. +type TenantsService struct { + c *Client +} + +// Get retrieves an Tenant object from NetBox by its ID. +func (s *TenantsService) Get(id int) (*Tenant, error) { + req, err := s.c.NewRequest( + http.MethodGet, + fmt.Sprintf("api/tenancy/tenants/%d/", id), + nil, + ) + if err != nil { + return nil, err + } + + t := new(Tenant) + 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 *TenantsService) List(options *ListTenantOptions) *Page { + return NewPage(s.c, "api/tenancy/tenants/", options) +} + +// Extract retrives a list of Tenant objects from page. +func (s *TenantsService) Extract(page *Page) ([]*Tenant, error) { + if err := page.Err(); err != nil { + return nil, err + } + + var groups []*Tenant + if err := json.Unmarshal(page.data.Results, &groups); err != nil { + return nil, err + } + return groups, nil +} + +// Create creates a new Tenant object in NetBox and returns the ID of the new object. +func (s *TenantsService) Create(data *Tenant) (int, error) { + req, err := s.c.NewJSONRequest(http.MethodPost, "api/tenancy/tenants/", nil, data) + if err != nil { + return 0, err + } + + g := new(updateTenant) + err = s.c.Do(req, g) + if err != nil { + return 0, err + } + return g.ID, nil +} + +// Update changes an existing Tenant object in NetBox, and returns the ID of the new object. +func (s *TenantsService) Update(data *Tenant) (int, error) { + req, err := s.c.NewJSONRequest( + http.MethodPatch, + fmt.Sprintf("api/tenancy/tenants/%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 updateTenant correctly, + // everything went fine, and we do not need to update data. + g := new(updateTenant) + err = s.c.Do(req, g) + if err != nil { + return 0, err + } + return g.ID, nil +} + +// Delete deletes an existing Tenant object from NetBox. +func (s *TenantsService) Delete(data *Tenant) error { + req, err := s.c.NewRequest( + http.MethodDelete, + fmt.Sprintf("api/tenancy/tenants/%d/", data.ID), + nil, + ) + if err != nil { + return err + } + + return s.c.Do(req, nil) +} diff --git a/netbox/tenancy_tenants_basic_test.go b/netbox/tenancy_tenants_basic_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e26756f54541e2f987bbaf66b60977033db5b968 --- /dev/null +++ b/netbox/tenancy_tenants_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_functions.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 Tenant.MarshalJSON is what you want, +// but not here as a return in testHandler +type serverDataTenant Tenant + +func convertToServerDataTenant(data []*Tenant) []*serverDataTenant { + dataWant := make([]*serverDataTenant, len(data)) + for i := range data { + tmp := serverDataTenant(*data[i]) + dataWant[i] = &tmp + } + return dataWant +} + +func TestBasicTenantGet(t *testing.T) { + var tests = []struct { + desc string + want *Tenant + }{ + { + desc: "Simple Tenant", + want: testTenant(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverDataTenant(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenants/1/", &serverData)) + defer done() + + res, err := c.Tenancy.Tenants.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.Tenancy.Tenants.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 TestBasicTenantGet404(t *testing.T) { + c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/tenancy/tenants/1/", &struct { + Detail string `json:"detail"` + }{ + Detail: "Not found.", + }, + http.StatusNotFound)) + defer done() + + res, err := c.Tenancy.Tenants.Get(1) + errstr := "404 - Not found." + if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected error from Client.Tenancy.Tenants.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 TestBasicListExtractTenant(t *testing.T) { + want := []*Tenant{ + testTenant(1), + testTenant(2), + } + serverWant := convertToServerDataTenant(want) + serverData, _ := json.Marshal(serverWant) + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenants/", &pageData{ + Count: 2, + NextURL: "", + PreviousURL: "", + Results: serverData, + })) + defer done() + + page := c.Tenancy.Tenants.List(nil) + + if page == nil { + t.Fatalf("unexpexted result from c.Tenancy.Tenants.List.") + } + + got := []*Tenant{} + counter := 0 + for page.Next() { + var err error + got, err = c.Tenancy.Tenants.Extract(page) + if err != nil { + t.Fatalf("unexpected error from c.Tenancy.Tenants.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 TestBasicCreateTenant(t *testing.T) { + var tests = []struct { + desc string + data *Tenant + want int + serverData interface{} + status int + errstr string + }{ + { + desc: "Create with ID 0", + data: testTenantCreate(1), + want: 1, + status: 0, + errstr: "", + serverData: testTenant(1), + }, + { + desc: "Create duplicate", + data: testTenantCreate(1), + want: 0, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"TenantsService with this name already exists.\"]}\n", + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"TenantsService 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/tenancy/tenants/", 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.Tenancy.Tenants.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 Tenant:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicUpdateTenant(t *testing.T) { + var tests = []struct { + desc string + data *Tenant + want int + serverData interface{} + status int + errstr string + }{ + { + desc: "Update with ID 1", + data: testTenant(1), + want: 1, + serverData: testTenant(1), + status: 0, + errstr: "", + }, + { + desc: "Update not found", + data: testTenant(1), + want: 0, + serverData: &struct { + Detail string + }{ + Detail: "Not found.", + }, + status: http.StatusNotFound, + errstr: "404 - Not found.", + }, + { + desc: "Update to duplicate", + data: testTenant(1), + want: 0, + serverData: &struct { + Name []string `json:"name"` + }{ + Name: []string{"TenantsService with this name already exists."}, + }, + status: http.StatusBadRequest, + errstr: "400 - {\"name\":[\"TenantsService 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/tenancy/tenants/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.Tenancy.Tenants.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 Tenant:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestBasicDeleteTenant(t *testing.T) { + var tests = []struct { + desc string + data *Tenant + serverData interface{} + status int + errstr string + }{ + { + desc: "Delete ID 1", + data: testTenant(1), + serverData: testTenant(1), + status: 0, + errstr: "", + }, + { + desc: "Delete not Found", + data: testTenant(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/tenancy/tenants/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.Tenancy.Tenants.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/tenancy_tenants_test.go b/netbox/tenancy_tenants_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b46f9ead3bf34212a9b2ea5b7c1e0471825a3cde --- /dev/null +++ b/netbox/tenancy_tenants_test.go @@ -0,0 +1,194 @@ +// 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 TestTenantGet(t *testing.T) { + var tests = []struct { + desc string + want *Tenant + wantGroup *TenantGroup + }{ + { + desc: "Without TenantGroup", + want: testTenantWithGroup(1, nil), + wantGroup: nil, + }, + { + desc: "With TenantGroup", + want: testTenantWithGroup(1, testTenantGroup(1)), + wantGroup: testTenantGroup(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + serverData := serverDataTenant(*tt.want) + + c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenants/1/", &serverData)) + defer done() + + res, err := c.Tenancy.Tenants.Get(1) + if err != nil { + t.Fatalf("unexpected error from Client.Tenancy.Tenants.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) + } + if want, got := tt.wantGroup, res.Group; !reflect.DeepEqual(want, got) { + t.Fatalf("unexpected TenantGroup:\n- want: %v\n- got: %v", want, got) + } + }) + } +} + +func TestTenantUnmarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data []byte + want *Tenant + }{ + { + desc: "Nil Group", + data: []byte(`{ "id": 1, "name": "Tenant 1", "slug": "tenant-1", "group": null, "description": "Tenant 1 Description", "comments": "Tenant 1 Comments", "custom_fields": {} }`), + want: testTenantWithGroup(1, nil), + }, + { + desc: "With Group", + data: []byte(`{ "id": 1, "name": "Tenant 1", "slug": "tenant-1", "group": {"id": 1, "name": "Tenant Group 1", "slug": "tenant-group-1"}, "description": "Tenant 1 Description", "comments": "Tenant 1 Comments", "custom_fields": {} }`), + want: testTenant(1), + }, + } + + for i, tt := range tests { + t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) { + result := new(Tenant) + 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 TestTenantMarshalJSON(t *testing.T) { + var tests = []struct { + desc string + data *Tenant + want []byte + }{ + { + desc: "Nil Group", + data: testTenantWithGroup(1, nil), + want: []byte(`{"id":1,"name":"Tenant 1","slug":"tenant-1","description":"Tenant 1 Description","comments":"Tenant 1 Comments"}`), + }, + { + desc: "With Group", + data: testTenant(1), + want: []byte(`{"id":1,"name":"Tenant 1","slug":"tenant-1","description":"Tenant 1 Description","comments":"Tenant 1 Comments","group":1}`), + }, + { + desc: "No Tenant.ID", + data: testTenantWithGroup(0, nil), + want: []byte(`{"name":"Tenant 0","slug":"tenant-0","description":"Tenant 0 Description","comments":"Tenant 0 Comments"}`), + }, + } + + 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 Tenant.MarshalJSON: %v", err) + } + + if want, got := tt.want, result; bytes.Compare(want, got) != 0 { + t.Fatalf("unexpected Tenant:\n- want: %s\n- got: %s", want, got) + } + }) + } +} + +func TestListTenantOptions(t *testing.T) { + var tests = []struct { + desc string + o *ListTenantOptions + v url.Values + }{ + { + desc: "empty options", + }, + { + desc: "full options", + o: &ListTenantOptions{ + Name: "Hello", + IDIn: "1,2,3", + GroupID: 1, + Query: "World", + }, + v: url.Values{ + "name": []string{"Hello"}, + "id__in": []string{"1,2,3"}, + "group_id": []string{"1"}, + "q": []string{"World"}, + }, + }, + { + desc: "group vs group_id", + o: &ListTenantOptions{ + GroupID: 1, + Group: "Group1", + }, + v: url.Values{ + "group_id": []string{"1"}, + }, + }, + { + desc: "group name", + o: &ListTenantOptions{ + Group: "Group 1", + }, + v: url.Values{ + "group": []string{"Group 1"}, + }, + }, + } + + 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/tenancy_tenants_types.go b/netbox/tenancy_tenants_types.go new file mode 100644 index 0000000000000000000000000000000000000000..ea64d3de37e63a22a20e0a8c569eb76452e09eec --- /dev/null +++ b/netbox/tenancy_tenants_types.go @@ -0,0 +1,103 @@ +// 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" +) + +// A Tenant is a representation of netbox tenants +type Tenant struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Comments string `json:"comments"` + Group *TenantGroup `json:"Group"` +} + +// A updateTenant is the internal representation of tenants +// needed to POST/PUT/PATCH to Netbox API +type updateTenant struct { + ID int `json:"id,omitempty"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Comments string `json:"comments"` + Group int `json:"group,omitempty"` +} + +// MarshalJSON marshals an Tenant into JSON bytes. +func (t *Tenant) MarshalJSON() ([]byte, error) { + group := 0 + if t.Group != nil { + group = t.Group.ID + } + return json.Marshal(updateTenant{ + ID: t.ID, + Name: t.Name, + Slug: t.Slug, + Description: t.Description, + Comments: t.Comments, + Group: group, + }) +} + +// ListTenantOptions is used as an argument for Client.Tenancy.Tenant.List. +// Integer fields with an *ID suffix are preferred over their string +// counterparts, and if both are set, only the *ID field will be used. +type ListTenantOptions struct { + Name string + IDIn string + GroupID int + Group string + + Query string +} + +// Values generates a url.Values map from the data in ListTenantOptions. +func (o *ListTenantOptions) Values() (url.Values, error) { + if o == nil { + return nil, nil + } + + v := url.Values{} + + switch { + case o.GroupID != 0: + v.Set("group_id", strconv.Itoa(o.GroupID)) + case o.Group != "": + v.Set("group", o.Group) + } + + if o.Name != "" { + v.Set("name", o.Name) + } + + if o.IDIn != "" { + v.Set("id__in", o.IDIn) + } + + if o.Query != "" { + v.Set("q", o.Query) + } + + return v, nil +} + +//go:generate go run generate_functions.go -type-name Tenant -update-type-name updateTenant -service-name TenantsService -endpoint tenancy -service tenants +//go:generate go run generate_basic_tests.go -type-name Tenant -service-name TenantsService -endpoint tenancy -service tenants -client-endpoint Tenancy -client-service Tenants diff --git a/scripts/golint.sh b/scripts/golint.sh index 54696d72fd6450d3365a0c7a1049c528302acb4a..ae7c53f46d933964f2f0396a94031e96487dc83a 100755 --- a/scripts/golint.sh +++ b/scripts/golint.sh @@ -1,12 +1,14 @@ #!/bin/bash # Verify that all files are correctly golint'd. -EXIT=0 -GOLINT=$(golint ./...) +set -e -o nounset -o pipefail +counter=0 +while read -r line; do + echo $line + : $((counter++)) +done < <(golint ./...) -if [[ ! -z $GOLINT ]]; then - echo $GOLINT - EXIT=1 +if ((counter == 0)); then + exit 0 fi - -exit $EXIT +exit 1 diff --git a/scripts/licensecheck.sh b/scripts/licensecheck.sh index 2230a8874d84d084ceb86c1c94e9f22b9406c8e9..ab31f0a75c236a8eee486f72b590cf848baa9183 100755 --- a/scripts/licensecheck.sh +++ b/scripts/licensecheck.sh @@ -2,8 +2,8 @@ # Verify that the correct license block is present in all Go source # files. -read -r -d '' EXPECTED <<EndOfLicense -// Copyright 2016 The go-netbox Authors. +IFS=$'\n' read -r -d '' -a EXPECTED <<EndOfLicense +// 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. @@ -17,15 +17,22 @@ read -r -d '' EXPECTED <<EndOfLicense // See the License for the specific language governing permissions and // limitations under the License. EndOfLicense +AUTHOR_REGEX='^// Copyright 20[0-9][0-9] The go-netbox Authors\.$' # Scan each Go source file for license. EXIT=0 GOFILES=$(find . -name "*.go") for FILE in $GOFILES; do - BLOCK=$(head -n 14 $FILE) + IFS=$'\n' read -r -d '' -a BLOCK < <(head -n 14 $FILE) - if [ "$BLOCK" != "$EXPECTED" ]; then + tmp_block=${BLOCK[@]:1} + tmp_expected=${EXPECTED[@]:1} + if [[ $tmp_block != $tmp_expected ]]; then + echo "file missing license: $FILE" + EXIT=1 + fi + if ! [[ "${BLOCK[0]}" =~ $AUTHOR_REGEX ]]; then echo "file missing license: $FILE" EXIT=1 fi