From f1eeb3c7ec5501600d9435b28e32561d31658fda Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:12:13 +0000 Subject: [PATCH 01/10] Initial commit for data uploader --- data_uploader/netbox_api/__init__.py | 0 data_uploader/parsers/__init__.py | 0 data_uploader/requirements.txt | 6 ++++++ data_uploader/test/__init__.py | 0 4 files changed, 6 insertions(+) create mode 100644 data_uploader/netbox_api/__init__.py create mode 100644 data_uploader/parsers/__init__.py create mode 100644 data_uploader/requirements.txt create mode 100644 data_uploader/test/__init__.py diff --git a/data_uploader/netbox_api/__init__.py b/data_uploader/netbox_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data_uploader/parsers/__init__.py b/data_uploader/parsers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data_uploader/requirements.txt b/data_uploader/requirements.txt new file mode 100644 index 0000000..0c905f6 --- /dev/null +++ b/data_uploader/requirements.txt @@ -0,0 +1,6 @@ +pynetbox +pytest +argparse +logger +requests +nose \ No newline at end of file diff --git a/data_uploader/test/__init__.py b/data_uploader/test/__init__.py new file mode 100644 index 0000000..e69de29 From e895de5bf49c5fb2ccb962984e1f13aa08e9681e Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:23:30 +0000 Subject: [PATCH 02/10] Add class for creating netbox api object and unit test --- data_uploader/exceptions/__init__.py | 0 .../missing_mandatory_param_error.py | 2 + data_uploader/netbox_api/netbox_connection.py | 33 +++++++++++ data_uploader/test/test_netbox_connection.py | 58 +++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 data_uploader/exceptions/__init__.py create mode 100644 data_uploader/exceptions/missing_mandatory_param_error.py create mode 100644 data_uploader/netbox_api/netbox_connection.py create mode 100644 data_uploader/test/test_netbox_connection.py diff --git a/data_uploader/exceptions/__init__.py b/data_uploader/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data_uploader/exceptions/missing_mandatory_param_error.py b/data_uploader/exceptions/missing_mandatory_param_error.py new file mode 100644 index 0000000..d4749dd --- /dev/null +++ b/data_uploader/exceptions/missing_mandatory_param_error.py @@ -0,0 +1,2 @@ +class MissingMandatoryParamError(ValueError): + pass \ No newline at end of file diff --git a/data_uploader/netbox_api/netbox_connection.py b/data_uploader/netbox_api/netbox_connection.py new file mode 100644 index 0000000..b171c98 --- /dev/null +++ b/data_uploader/netbox_api/netbox_connection.py @@ -0,0 +1,33 @@ +from data_uploader.exceptions.missing_mandatory_param_error import MissingMandatoryParamError +import requests +from pynetbox import api + + +class NetboxApi: + """ + Wraps a netbox connection as a context manager + + """ + def __init__(self): + pass + + @staticmethod + def api_object(netbox_url: str, token: str, cert=None): + if not netbox_url: + raise MissingMandatoryParamError( + "NetBox URL is required but not provided." + ) + if not token: + raise MissingMandatoryParamError( + "A token is required but not provided." + ) + session = requests.Session() + if not cert: + session.verify = False + else: + session.verify = cert + + connection = api(url=netbox_url, token=token) + connection.http_session = session + + return connection diff --git a/data_uploader/test/test_netbox_connection.py b/data_uploader/test/test_netbox_connection.py new file mode 100644 index 0000000..bd5c692 --- /dev/null +++ b/data_uploader/test/test_netbox_connection.py @@ -0,0 +1,58 @@ +import unittest +from unittest import mock +from unittest.mock import NonCallableMock, patch + +import data_uploader.netbox_api.netbox_connection +from nose.tools import raises +from data_uploader.exceptions.missing_mandatory_param_error import MissingMandatoryParamError +from data_uploader.netbox_api.netbox_connection import NetboxApi + + +class NetboxApiTests(unittest.TestCase): + @staticmethod + @patch('netbox_api.netbox_connection.api') + def test_api_object_create(mock_api): + """ + Tests that we do get an API object an + """ + mock_url = "test_netbox.rl.ac.uk" + mock_token = NonCallableMock + mock_cert = NonCallableMock + + api = NetboxApi.api_object(mock_url, mock_token, mock_cert) + assert mock_api.return_value == api + + @staticmethod + @patch('netbox_api.netbox_connection.api') + def test_api_no_cert(mock_api): + """ + Test that an api object is created even when we don't have a path for a certificate + """ + mock_url = "test_netbox.rl.ac.uk" + mock_token = NonCallableMock + mock_cert = None + + api = NetboxApi.api_object(mock_url, mock_token, mock_cert) + assert mock_api.return_value == api + + @raises(MissingMandatoryParamError) + def test_api_throws_for_no_url(self): + """ + Tests a None type will throw error if used as url + """ + missing_url = None + mock_token = NonCallableMock + mock_cert = NonCallableMock + api = NetboxApi.api_object(missing_url, mock_token, mock_cert) + api.assertRaises(MissingMandatoryParamError) + + @raises(MissingMandatoryParamError) + def test_api_throws_for_no_token(self): + """ + Tests a None type will throw if used as token + """ + mock_url = NonCallableMock + missing_token = None + mock_cert = NonCallableMock + NetboxApi.api_object(mock_url, missing_token, mock_cert) + self.assertRaises(MissingMandatoryParamError) From 024a917b4947cfb3976462858a65834e676f6303 Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:27:17 +0000 Subject: [PATCH 03/10] Add data class for data about systems --- data_uploader/system_data_class.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 data_uploader/system_data_class.py diff --git a/data_uploader/system_data_class.py b/data_uploader/system_data_class.py new file mode 100644 index 0000000..635914a --- /dev/null +++ b/data_uploader/system_data_class.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass + +@dataclass +class SystemDataClass: + """ + Class for keeping track of server properties + """ + manufacturer: str + model: str + primaryMAC: str + hostname: str + serviceTag: str + ipmiIPAddress: str + From 26917820a99f32392cf0321553b9be72f8ee70a4 Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:30:54 +0000 Subject: [PATCH 04/10] Add class for netbox dcim objects and unit test --- data_uploader/netbox_api/netbox_dcim.py | 104 ++++++++++++++++++++++++ data_uploader/test/test_netbox_dcim.py | 60 ++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 data_uploader/netbox_api/netbox_dcim.py create mode 100644 data_uploader/test/test_netbox_dcim.py diff --git a/data_uploader/netbox_api/netbox_dcim.py b/data_uploader/netbox_api/netbox_dcim.py new file mode 100644 index 0000000..2050f3f --- /dev/null +++ b/data_uploader/netbox_api/netbox_dcim.py @@ -0,0 +1,104 @@ +from data_uploader.netbox_api.netbox_connection import NetboxApi + + +class NetboxDcim(): + def __init__(self, url, token, cert=None): + self.url = url + self.token = token + self. cert = cert + #'= NetboxApi.api_object(url, token, cert) + + self._netbox_api = NetboxApi.api_object(url, token, cert) + + def get_device(self, hostname: str): + """ + Search for the name of a device (server) in Netbox by its name + :param netbox_url: Netbox url + :param token: Token to connect to url + :param cert_path: path to certificate (required for Production) + :param hostname: name of the server to search for in Netbox + :return: dict of record in netbox or None if not found + """ + netbox = self._netbox_api + + return netbox.dcim.devices.get(name=hostname) + + def get_device_types(self, model): + """ + Get device model type from netbox + :param model: model type to search for in netbox + :return: dict of record in netbox or None if not found + """ + netbox = self._netbox_api + + return netbox.dcim.device_types.get(slug=model) + + def get_interface(self, interface_name, hostname): + """ + Get an interface from Netbox + :param interface_name: Name of interface to search for + :param hostname: Name of device the interface is attached to + """ + netbox = self._netbox_api + return netbox.dcim.interfaces.get(name=interface_name, device=hostname) + + + def create_device(self, **kwargs): + """ + Create a new device in NetBox + :param kwargs: + :return: Netbox Record object + """ + netbox = self._netbox_api + + #netbox_device = netbox.dcim.devices.create( + # name=hostname, + # site=netbox.dcim.devices.get(name=site).id, + # location=netbox.dcim.locations.get(name=location).id, + # tenant=netbox.tenancy.tenants.get(name=tenant).id, + # manufacturer=netbox.dcim.manufacturers.get(name=manufacturer).id, # optional field + # rack=netbox.dcim.racks.get(name=rack).id, + # postion=rack_position, # optional field - omit this field if the device is unracked + # device_type=netbox.dcim.device_types.get(slug=device_type).id, + # serial=serial_no, # optional field + # custom_fields={'cf_1': 'Custom data 1'}, # optional field + # tags=[{"name": "Tag 1"}, {"name": "Tag 2"}] # optional field + #) + pass + + def create_device_type(self, **kwargs): + """ + Create a new device type in NetBox + :return: + """ + netbox = self._netbox_api + + #new_device_type = netbox.dcim.device_types.create( + # manufacturer=netbox.dcim.manufacturers.get(name="manufacturer-name").id, + # model="device-type-name", + # slug="device-type-slug", + # subdevice_role='child or parent', + # optional field - required if creating a device type to be used for a child device + # u_height=unit_height, + # Can only equal 0 if the device type is for a child device - requires subdevice_role='child' if that is the case + # custom_fields={'cf_1': 'Custom data 1'} # optional field + #) + pass + + + def create_interface(self, **kwargs): + """ + Create an interface for a device already in NetBox + :param kwargs: + :return: + """ + netbox = self._netbox_api + #netbox_interface = netbox.dcim.interfaces.create( + # name="interface-name", + # device=netbox.dcim.devices.get(name=hostname)]).id, + # type = "interface-type", + # description = "description", + # mac_address = "mac-address", + # tags = [{'name': 'Tag 1'}] + #) + pass \ No newline at end of file diff --git a/data_uploader/test/test_netbox_dcim.py b/data_uploader/test/test_netbox_dcim.py new file mode 100644 index 0000000..d2174da --- /dev/null +++ b/data_uploader/test/test_netbox_dcim.py @@ -0,0 +1,60 @@ +import unittest +from unittest.mock import NonCallableMock, Mock, MagicMock +from data_uploader.netbox_api.netbox_dcim import NetboxDcim +from data_uploader.netbox_api.netbox_connection import NetboxApi +from nose.tools import assert_true, assert_is_not_none + + +class NetboxDcimTests(unittest.TestCase): + """ + Runs tests to ensure we are interacting with the NetBox + API for dcim objects in the expected way + """ + + def test_get_device_response(self): + """ + Test that we can interact with netbox and get a response + :return: + """ + netbox = MagicMock() + mock_device_name = NonCallableMock + response = netbox.dcim.devices.get(name=mock_device_name) + + assert_true(response.ok) + assert_is_not_none(response) + + #error with line below + #test we do get a value returned + #assert response == NetboxDcim.get_device(hostname=mock_device_name).return_value + + def test_get_device_no_device_found(self): + """ + Test that if we get a + :return: + """ + self._netbox_api = MagicMock() + +# def test_get_device(self): +# +# mocked_url = NonCallableMock() +# mocked_token = NonCallableMock() +# mocked_device_name = NonCallableMock() +# mocked_hostname = NonCallableMock +# netbox = MagicMock +# device = NetboxDcim.get_device(self, hostname=mocked_hostname) +# +# assert device == self.api.get_device.return_value + + #self.api.get_device.assert_called_once_with() + #self.mocked_connection.assert_called_once_with(mocked_url, mocked_token) + + def test_get_device_type(self): + """ + Test that we can interact with NetBox to get a device type and + get a response back + :return: + """ + netbox = MagicMock() + mock_model = NonCallableMock + response = netbox.dcim.device_types.get(slug=NonCallableMock) + assert_true(response.ok) From af647a33c3d83596b4d127a62a9ca4bd54cdb082 Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Fri, 24 Mar 2023 15:34:03 +0000 Subject: [PATCH 05/10] Add files for netbox ipam class and unit test --- data_uploader/netbox_api/netbox_ipam.py | 52 +++++++++++++++++++++++++ data_uploader/test/test_netbox_ipam.py | 11 ++++++ 2 files changed, 63 insertions(+) create mode 100644 data_uploader/netbox_api/netbox_ipam.py create mode 100644 data_uploader/test/test_netbox_ipam.py diff --git a/data_uploader/netbox_api/netbox_ipam.py b/data_uploader/netbox_api/netbox_ipam.py new file mode 100644 index 0000000..e93a493 --- /dev/null +++ b/data_uploader/netbox_api/netbox_ipam.py @@ -0,0 +1,52 @@ +from data_uploader.netbox_api.netbox_dcim import NetboxDcim +from data_uploader.netbox_api.netbox_connection import NetboxApi +import pynetbox + +class NetboxIpam: + def __init__(self, url, token, cert): + self.url = url + self.token = token + self. cert = cert + self._netbox_api = NetboxApi.api_object(url, token, cert) + + def search_mac_address(self, mac_addr: str): + """ + Search Netbox for a mac address assigned to any interface + This will call a method from NetBox Dcim to do this + :return: Record object with interface the mac address is associated with or None if not found + """ + netbox = self._netbox_api + interface = netbox.dcim.interfaces.get(mac_address=mac_addr) + + return interface + + def get_ip_address(self, ip_addr: str): + """ + Search netbox for an IP Address + :param ip_addr: IP address to search for in Netbox + :return: Record object with IP Address or None if not found + """ + netbox = self._netbox_api + return netbox.ipam.ip_addresses.get(address=ip_addr) + + def create_ip_address(self, hostname: str, interface:str, ip_address: str, tenant: str): + """ + Create a new IP address in Netbox + :param hostname: Name of device in Netbox (must be present in Netbox) + :param interface: Interface attached to device (must be present in Netbox) + :param ip_address: IP address to create + :param tenant: Tenant IP address belongs to (must be present in Netbox) + """ + + interface = NetboxDcim.get_interface(name=interface, device=hostname) + netbox_ip = netbox.ipam.ip_addresses.create( + address="ip-address", + tenant=netbox.tenancy.tenants.get(name='tenant-name').id, + tags=[{'name': 'Tag 1'}], + ) + # assign IP Address to device's network interface + netbox_ip.assigned_object = interface + netbox_ip.assigned_object_id = interface.id + netbox_ip.assigned_object_type = 'dcim.interface' + # save changes to IP record in Netbox + netbox_ip.save() diff --git a/data_uploader/test/test_netbox_ipam.py b/data_uploader/test/test_netbox_ipam.py new file mode 100644 index 0000000..5b494d7 --- /dev/null +++ b/data_uploader/test/test_netbox_ipam.py @@ -0,0 +1,11 @@ +# WIP +import unittest +from unittest.mock import NonCallableMock, MagicMock + +class NetboxIpamTests(unittest.TestCase): + """ + Run tests to ensure that we are interacting with the Netbox API + for ipam objects in the expected way + """ + def test(self): + pass \ No newline at end of file From 77b096b4181b697a6905467546a40fce31d96d94 Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Mon, 27 Mar 2023 10:19:16 +0100 Subject: [PATCH 06/10] Update netbox connection and unit test with suggested changes --- data_uploader/netbox_api/netbox_connection.py | 5 +--- data_uploader/test/test_netbox_connection.py | 25 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/data_uploader/netbox_api/netbox_connection.py b/data_uploader/netbox_api/netbox_connection.py index b171c98..c1d510c 100644 --- a/data_uploader/netbox_api/netbox_connection.py +++ b/data_uploader/netbox_api/netbox_connection.py @@ -22,10 +22,7 @@ def api_object(netbox_url: str, token: str, cert=None): "A token is required but not provided." ) session = requests.Session() - if not cert: - session.verify = False - else: - session.verify = cert + session.verify = cert if cert else False connection = api(url=netbox_url, token=token) connection.http_session = session diff --git a/data_uploader/test/test_netbox_connection.py b/data_uploader/test/test_netbox_connection.py index bd5c692..7b76c5a 100644 --- a/data_uploader/test/test_netbox_connection.py +++ b/data_uploader/test/test_netbox_connection.py @@ -1,8 +1,5 @@ import unittest -from unittest import mock from unittest.mock import NonCallableMock, patch - -import data_uploader.netbox_api.netbox_connection from nose.tools import raises from data_uploader.exceptions.missing_mandatory_param_error import MissingMandatoryParamError from data_uploader.netbox_api.netbox_connection import NetboxApi @@ -10,26 +7,26 @@ class NetboxApiTests(unittest.TestCase): @staticmethod - @patch('netbox_api.netbox_connection.api') + @patch('data_uploader.netbox_api.netbox_connection.api') def test_api_object_create(mock_api): """ Tests that we do get an API object an """ - mock_url = "test_netbox.rl.ac.uk" - mock_token = NonCallableMock - mock_cert = NonCallableMock + mock_url = "example.com" + mock_token = NonCallableMock() + mock_cert = NonCallableMock() api = NetboxApi.api_object(mock_url, mock_token, mock_cert) assert mock_api.return_value == api @staticmethod - @patch('netbox_api.netbox_connection.api') + @patch('data_uploader.netbox_api.netbox_connection.api') def test_api_no_cert(mock_api): """ Test that an api object is created even when we don't have a path for a certificate """ - mock_url = "test_netbox.rl.ac.uk" - mock_token = NonCallableMock + mock_url = "example.com" + mock_token = NonCallableMock() mock_cert = None api = NetboxApi.api_object(mock_url, mock_token, mock_cert) @@ -41,8 +38,8 @@ def test_api_throws_for_no_url(self): Tests a None type will throw error if used as url """ missing_url = None - mock_token = NonCallableMock - mock_cert = NonCallableMock + mock_token = NonCallableMock() + mock_cert = NonCallableMock() api = NetboxApi.api_object(missing_url, mock_token, mock_cert) api.assertRaises(MissingMandatoryParamError) @@ -51,8 +48,8 @@ def test_api_throws_for_no_token(self): """ Tests a None type will throw if used as token """ - mock_url = NonCallableMock + mock_url = NonCallableMock() missing_token = None - mock_cert = NonCallableMock + mock_cert = NonCallableMock() NetboxApi.api_object(mock_url, missing_token, mock_cert) self.assertRaises(MissingMandatoryParamError) From f09fbef003cd8f35511ccf3d502dfc3af1ceacae Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Mon, 27 Mar 2023 16:15:23 +0100 Subject: [PATCH 07/10] Update and add new exceptions --- data_uploader/exceptions/ip_address_collision_error.py | 2 ++ data_uploader/exceptions/mac_address_collision_error.py | 2 ++ data_uploader/exceptions/missing_mandatory_param_error.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 data_uploader/exceptions/ip_address_collision_error.py create mode 100644 data_uploader/exceptions/mac_address_collision_error.py diff --git a/data_uploader/exceptions/ip_address_collision_error.py b/data_uploader/exceptions/ip_address_collision_error.py new file mode 100644 index 0000000..c4420ac --- /dev/null +++ b/data_uploader/exceptions/ip_address_collision_error.py @@ -0,0 +1,2 @@ +class IPAddressCollisionError(ValueError): + pass diff --git a/data_uploader/exceptions/mac_address_collision_error.py b/data_uploader/exceptions/mac_address_collision_error.py new file mode 100644 index 0000000..007813b --- /dev/null +++ b/data_uploader/exceptions/mac_address_collision_error.py @@ -0,0 +1,2 @@ +class MacAddressCollisionError(ValueError): + pass diff --git a/data_uploader/exceptions/missing_mandatory_param_error.py b/data_uploader/exceptions/missing_mandatory_param_error.py index d4749dd..0cd9208 100644 --- a/data_uploader/exceptions/missing_mandatory_param_error.py +++ b/data_uploader/exceptions/missing_mandatory_param_error.py @@ -1,2 +1,2 @@ class MissingMandatoryParamError(ValueError): - pass \ No newline at end of file + pass From 576bc1aa8cc3e8769c9327976ac86e2e8c9de36e Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Mon, 27 Mar 2023 16:20:09 +0100 Subject: [PATCH 08/10] Add NetboxBase class --- data_uploader/netbox_api/netbox_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 data_uploader/netbox_api/netbox_base.py diff --git a/data_uploader/netbox_api/netbox_base.py b/data_uploader/netbox_api/netbox_base.py new file mode 100644 index 0000000..4f51ddd --- /dev/null +++ b/data_uploader/netbox_api/netbox_base.py @@ -0,0 +1,9 @@ +from data_uploader.netbox_api.netbox_connection import NetboxApi + + +class NetboxBase: + def __init__(self, url, token, cert): + self.url = url + self.token = token + self.cert = cert + self._netbox_api = NetboxApi.api_object(url, token, cert) From 3e617ccfcc55a00c8179c596316d634d85af13de Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Mon, 27 Mar 2023 16:45:17 +0100 Subject: [PATCH 09/10] Update netbox_dcim.py and unit test --- data_uploader/netbox_api/netbox_dcim.py | 155 +++++++++++++++--------- data_uploader/test/test_netbox_dcim.py | 45 +++---- 2 files changed, 110 insertions(+), 90 deletions(-) diff --git a/data_uploader/netbox_api/netbox_dcim.py b/data_uploader/netbox_api/netbox_dcim.py index 2050f3f..ae213f2 100644 --- a/data_uploader/netbox_api/netbox_dcim.py +++ b/data_uploader/netbox_api/netbox_dcim.py @@ -1,104 +1,139 @@ -from data_uploader.netbox_api.netbox_connection import NetboxApi +from ctypes import Union +from xmlrpc.client import Boolean +from data_uploader.exceptions.mac_address_collision_error import MacAddressCollisionError +from data_uploader.netbox_api.netbox_base import NetboxBase +from pynetbox.core.api import Record -class NetboxDcim(): - def __init__(self, url, token, cert=None): - self.url = url - self.token = token - self. cert = cert - #'= NetboxApi.api_object(url, token, cert) +class NetboxDcim(NetboxBase): + """ + Class for retrieving or creating DCIM objects in Netbox + """ + def __init__(self, url, token, cert): + super().__init__(url, token, cert) - self._netbox_api = NetboxApi.api_object(url, token, cert) - - def get_device(self, hostname: str): + def get_device(self, hostname: str) -> Union[Record, None]: """ Search for the name of a device (server) in Netbox by its name - :param netbox_url: Netbox url - :param token: Token to connect to url - :param cert_path: path to certificate (required for Production) :param hostname: name of the server to search for in Netbox :return: dict of record in netbox or None if not found """ - netbox = self._netbox_api - return netbox.dcim.devices.get(name=hostname) + return self._netbox_api.dcim.devices.get(name=hostname) - def get_device_types(self, model): + def get_device_types(self, model: str) -> Union[Record, None]: """ Get device model type from netbox :param model: model type to search for in netbox :return: dict of record in netbox or None if not found """ - netbox = self._netbox_api - return netbox.dcim.device_types.get(slug=model) + return self._netbox_api.dcim.device_types.get(slug=model) - def get_interface(self, interface_name, hostname): + def get_interface(self, interface_name: str, hostname: str) -> Union[Record, None]: """ Get an interface from Netbox :param interface_name: Name of interface to search for :param hostname: Name of device the interface is attached to """ - netbox = self._netbox_api - return netbox.dcim.interfaces.get(name=interface_name, device=hostname) + return self._netbox_api.dcim.interfaces.get(name=interface_name, device=hostname) - def create_device(self, **kwargs): + def find_mac_addr(self, mac_address: str) -> Boolean: + """ + Checks whether there is an interface in Netbox that already uses a specific mac address + :param mac_address: mac address to search Netbox for + :returns: Boolean indicating whether an interface with that address exists (True) or not (False) """ - Create a new device in NetBox - :param kwargs: + check = self._netbox_api.interface.get(mac_address=mac_address) + + return True if check else False + + def create_device(self, hostname: str, site: str, location: str, tenant: str, manufacturer: str, + rack: str, rack_position: int, device_type:str, serial_no: str) -> Record: + """ + Create a new device in NetBox. + + :param hostname: Name of device + :param site: Building and Room the device is located in + :param location: Row the device is in + :param tenant: Tenant for the device + :param manufacturer: Device manufacturer + :param rack: Rack the device is in + :param rack_position: Rack position (Optional) + :param device_type: Device type + :param serial_no: serial number (optional) + :return: Netbox Record object """ netbox = self._netbox_api - #netbox_device = netbox.dcim.devices.create( - # name=hostname, - # site=netbox.dcim.devices.get(name=site).id, - # location=netbox.dcim.locations.get(name=location).id, - # tenant=netbox.tenancy.tenants.get(name=tenant).id, - # manufacturer=netbox.dcim.manufacturers.get(name=manufacturer).id, # optional field - # rack=netbox.dcim.racks.get(name=rack).id, - # postion=rack_position, # optional field - omit this field if the device is unracked - # device_type=netbox.dcim.device_types.get(slug=device_type).id, - # serial=serial_no, # optional field - # custom_fields={'cf_1': 'Custom data 1'}, # optional field - # tags=[{"name": "Tag 1"}, {"name": "Tag 2"}] # optional field - #) - pass - - def create_device_type(self, **kwargs): + netbox_device = netbox.dcim.devices.create( + name=hostname, + site=netbox.dcim.devices.get(name=site).id, + location=netbox.dcim.locations.get(name=location).id, + tenant=netbox.tenancy.tenants.get(name=tenant).id, + manufacturer=netbox.dcim.manufacturers.get(name=manufacturer).id, # optional field + rack=netbox.dcim.racks.get(name=rack).id, + postion=rack_position, # optional field - omit this field if the device is unracked + device_type=netbox.dcim.device_types.get(slug=device_type).id, + serial=serial_no, # optional field + #tags=[{"name": "Tag 1"}, {"name": "Tag 2"}] # optional field + ) + return netbox_device + + def create_device_type(self, model: str, manufacturer: str, slug: str, unit_height: int) -> None: """ Create a new device type in NetBox - :return: + :param model: Name of device type + :param manufacturer: name of manufacturer + :param slug: slug of model name + :param unit_height: height of devices of this type + + :return: Netbox record of new device type """ netbox = self._netbox_api - #new_device_type = netbox.dcim.device_types.create( - # manufacturer=netbox.dcim.manufacturers.get(name="manufacturer-name").id, - # model="device-type-name", - # slug="device-type-slug", - # subdevice_role='child or parent', + new_device_type = netbox.dcim.device_types.create( + manufacturer=netbox.dcim.manufacturers.get(name="manufacturer-name").id, + model="device-type-name", + slug="device-type-slug", + subdevice_role='child or parent', # optional field - required if creating a device type to be used for a child device - # u_height=unit_height, + u_height=unit_height, # Can only equal 0 if the device type is for a child device - requires subdevice_role='child' if that is the case # custom_fields={'cf_1': 'Custom data 1'} # optional field - #) - pass - + ) + return new_device_type - def create_interface(self, **kwargs): + def create_interface(self, interface_name: str, hostname: str, interface_type: str, description: str, + mac_address: str) -> Record: """ Create an interface for a device already in NetBox - :param kwargs: + :param interface_name: name of interface + :param hostname: name of device the interface is attached to + :param interface_type: interface type + :param description: description + :param mac_address: mac_address :return: """ + + # verify whether the mac address already exists in Netbox on a specific interface + mac_addr_match = NetboxDcim.find_mac_addr(mac_address) + + if mac_addr_match: + raise MacAddressCollisionError( + "MAC Address already exists in Netbox" + ) + netbox = self._netbox_api - #netbox_interface = netbox.dcim.interfaces.create( - # name="interface-name", - # device=netbox.dcim.devices.get(name=hostname)]).id, - # type = "interface-type", - # description = "description", - # mac_address = "mac-address", + + netbox_interface = netbox.dcim.interfaces.create( + name=interface_name, + device=netbox.dcim.devices.get(name=hostname).id, + type=interface_type, + description=description, + mac_address=mac_address, # tags = [{'name': 'Tag 1'}] - #) - pass \ No newline at end of file + ) + return netbox_interface \ No newline at end of file diff --git a/data_uploader/test/test_netbox_dcim.py b/data_uploader/test/test_netbox_dcim.py index d2174da..1cdb259 100644 --- a/data_uploader/test/test_netbox_dcim.py +++ b/data_uploader/test/test_netbox_dcim.py @@ -1,5 +1,5 @@ import unittest -from unittest.mock import NonCallableMock, Mock, MagicMock +from unittest.mock import NonCallableMock, MagicMock from data_uploader.netbox_api.netbox_dcim import NetboxDcim from data_uploader.netbox_api.netbox_connection import NetboxApi from nose.tools import assert_true, assert_is_not_none @@ -16,37 +16,22 @@ def test_get_device_response(self): Test that we can interact with netbox and get a response :return: """ - netbox = MagicMock() - mock_device_name = NonCallableMock - response = netbox.dcim.devices.get(name=mock_device_name) + self._netbox_api = MagicMock() + mock_device_name = NonCallableMock() + response = self._netbox_api.dcim.devices.get(name=mock_device_name) assert_true(response.ok) assert_is_not_none(response) - #error with line below - #test we do get a value returned - #assert response == NetboxDcim.get_device(hostname=mock_device_name).return_value - - def test_get_device_no_device_found(self): - """ - Test that if we get a - :return: - """ - self._netbox_api = MagicMock() - -# def test_get_device(self): -# -# mocked_url = NonCallableMock() -# mocked_token = NonCallableMock() -# mocked_device_name = NonCallableMock() -# mocked_hostname = NonCallableMock -# netbox = MagicMock -# device = NetboxDcim.get_device(self, hostname=mocked_hostname) -# -# assert device == self.api.get_device.return_value + # assert that a value is returned + # assert NetboxDcim.get_device(hostname=mock_device_name).return_value == response - #self.api.get_device.assert_called_once_with() - #self.mocked_connection.assert_called_once_with(mocked_url, mocked_token) + #def test_get_device_no_device_found(self): + # """ + # Test that if we get a + # :return: + # """ + # self._netbox_api = MagicMock() def test_get_device_type(self): """ @@ -54,7 +39,7 @@ def test_get_device_type(self): get a response back :return: """ - netbox = MagicMock() - mock_model = NonCallableMock - response = netbox.dcim.device_types.get(slug=NonCallableMock) + self._netbox_api = MagicMock() + mock_model = NonCallableMock() + response = self._netbox_api.dcim.device_types.get(slug=mock_model) assert_true(response.ok) From a1ff5fefd434fa006c208e92002761d6fe3c193a Mon Sep 17 00:00:00 2001 From: meoflynn <68336207+meoflynn@users.noreply.github.com> Date: Thu, 6 Apr 2023 16:04:56 +0100 Subject: [PATCH 10/10] Update netbox dcim and unit test script (work in progress) --- data_uploader/netbox_api/netbox_dcim.py | 33 +++--- data_uploader/test/test_netbox_dcim.py | 135 +++++++++++++++++++++--- 2 files changed, 130 insertions(+), 38 deletions(-) diff --git a/data_uploader/netbox_api/netbox_dcim.py b/data_uploader/netbox_api/netbox_dcim.py index ae213f2..3bd8b5b 100644 --- a/data_uploader/netbox_api/netbox_dcim.py +++ b/data_uploader/netbox_api/netbox_dcim.py @@ -1,8 +1,7 @@ -from ctypes import Union -from xmlrpc.client import Boolean +from typing import Union from data_uploader.exceptions.mac_address_collision_error import MacAddressCollisionError from data_uploader.netbox_api.netbox_base import NetboxBase -from pynetbox.core.api import Record +from pynetbox.core.response import Record class NetboxDcim(NetboxBase): @@ -18,7 +17,7 @@ def get_device(self, hostname: str) -> Union[Record, None]: :param hostname: name of the server to search for in Netbox :return: dict of record in netbox or None if not found """ - + self._netbox_api.dcim.devices.get(name=hostname) return self._netbox_api.dcim.devices.get(name=hostname) def get_device_types(self, model: str) -> Union[Record, None]: @@ -36,21 +35,19 @@ def get_interface(self, interface_name: str, hostname: str) -> Union[Record, Non :param interface_name: Name of interface to search for :param hostname: Name of device the interface is attached to """ - return self._netbox_api.dcim.interfaces.get(name=interface_name, device=hostname) - def find_mac_addr(self, mac_address: str) -> Boolean: + def find_mac_addr(self, mac_address: str) -> bool: """ Checks whether there is an interface in Netbox that already uses a specific mac address :param mac_address: mac address to search Netbox for :returns: Boolean indicating whether an interface with that address exists (True) or not (False) """ check = self._netbox_api.interface.get(mac_address=mac_address) - return True if check else False def create_device(self, hostname: str, site: str, location: str, tenant: str, manufacturer: str, - rack: str, rack_position: int, device_type:str, serial_no: str) -> Record: + rack: str, rack_position: int, device_type: str, serial_no: str) -> Record: """ Create a new device in NetBox. @@ -67,7 +64,6 @@ def create_device(self, hostname: str, site: str, location: str, tenant: str, ma :return: Netbox Record object """ netbox = self._netbox_api - netbox_device = netbox.dcim.devices.create( name=hostname, site=netbox.dcim.devices.get(name=site).id, @@ -82,7 +78,7 @@ def create_device(self, hostname: str, site: str, location: str, tenant: str, ma ) return netbox_device - def create_device_type(self, model: str, manufacturer: str, slug: str, unit_height: int) -> None: + def create_device_type(self, model: str, manufacturer: str, slug: str, unit_height: int) -> Record: """ Create a new device type in NetBox :param model: Name of device type @@ -93,15 +89,11 @@ def create_device_type(self, model: str, manufacturer: str, slug: str, unit_heig :return: Netbox record of new device type """ netbox = self._netbox_api - new_device_type = netbox.dcim.device_types.create( - manufacturer=netbox.dcim.manufacturers.get(name="manufacturer-name").id, - model="device-type-name", - slug="device-type-slug", - subdevice_role='child or parent', - # optional field - required if creating a device type to be used for a child device + manufacturer=netbox.dcim.manufacturers.get(name=manufacturer).id, + model=model, + slug=slug, u_height=unit_height, - # Can only equal 0 if the device type is for a child device - requires subdevice_role='child' if that is the case # custom_fields={'cf_1': 'Custom data 1'} # optional field ) return new_device_type @@ -115,11 +107,10 @@ def create_interface(self, interface_name: str, hostname: str, interface_type: s :param interface_type: interface type :param description: description :param mac_address: mac_address - :return: + :return: netbox_interface: Record object with details of newly created netbox interface """ - # verify whether the mac address already exists in Netbox on a specific interface - mac_addr_match = NetboxDcim.find_mac_addr(mac_address) + mac_addr_match = NetboxDcim.find_mac_addr(self, mac_address) if mac_addr_match: raise MacAddressCollisionError( @@ -136,4 +127,4 @@ def create_interface(self, interface_name: str, hostname: str, interface_type: s mac_address=mac_address, # tags = [{'name': 'Tag 1'}] ) - return netbox_interface \ No newline at end of file + return netbox_interface diff --git a/data_uploader/test/test_netbox_dcim.py b/data_uploader/test/test_netbox_dcim.py index 1cdb259..beb18a7 100644 --- a/data_uploader/test/test_netbox_dcim.py +++ b/data_uploader/test/test_netbox_dcim.py @@ -1,11 +1,13 @@ import unittest -from unittest.mock import NonCallableMock, MagicMock +from unittest.mock import NonCallableMock, MagicMock, patch from data_uploader.netbox_api.netbox_dcim import NetboxDcim -from data_uploader.netbox_api.netbox_connection import NetboxApi -from nose.tools import assert_true, assert_is_not_none - +from nose.tools import assert_true, assert_is_not_none, raises +from data_uploader.exceptions.mac_address_collision_error import MacAddressCollisionError +from pynetbox.core.response import Record +# WIP class NetboxDcimTests(unittest.TestCase): + """ Runs tests to ensure we are interacting with the NetBox API for dcim objects in the expected way @@ -14,24 +16,18 @@ class NetboxDcimTests(unittest.TestCase): def test_get_device_response(self): """ Test that we can interact with netbox and get a response - :return: """ self._netbox_api = MagicMock() mock_device_name = NonCallableMock() - response = self._netbox_api.dcim.devices.get(name=mock_device_name) - + response = NetboxDcim.get_device(self._netbox_api, hostname=mock_device_name) assert_true(response.ok) assert_is_not_none(response) - # assert that a value is returned - # assert NetboxDcim.get_device(hostname=mock_device_name).return_value == response - - #def test_get_device_no_device_found(self): - # """ - # Test that if we get a - # :return: - # """ - # self._netbox_api = MagicMock() + def test_get_device_none_found(self): + """ + Test that get_device does return None if a device is not in Netbox + """ + pass def test_get_device_type(self): """ @@ -41,5 +37,110 @@ def test_get_device_type(self): """ self._netbox_api = MagicMock() mock_model = NonCallableMock() - response = self._netbox_api.dcim.device_types.get(slug=mock_model) + response = NetboxDcim.get_device_types(model=mock_model) assert_true(response.ok) + assert_is_not_none(response) + # TO DO assert a value is actually returned by get device type + + def test_get_device_type_none_found(self): + """ + Test None is returned if device type not in Netbox + """ + pass + + def test_get_interface(self): + """ + Test get_interface method + """ + self._netbox_api = MagicMock() + mock_interface = NonCallableMock() + mock_hostname = NonCallableMock() + + response = NetboxDcim.get_interface(self, interface_name=mock_interface, hostname=mock_hostname) + assert_true(response.ok) + assert_is_not_none(response) + + def test_find_mac_addr(self): + """ + Test checking for a mac address in Netbox + """ + self._netbox_api = MagicMock() + mock_mac_addr = NonCallableMock() + + response = NetboxDcim.find_mac_addr(self, mac_address=mock_mac_addr) + assert_true(response) + + def test_no_mac_addr_found(self): + """ + Test False is returned when a mac address is missing in Netbox + """ + # WIP + pass + + def test_create_device(self): + """ + Test creating a device + """ + # WIP + self._netbox_api = MagicMock() + mock_hostname = NonCallableMock() + mock_site = NonCallableMock() + mock_location = NonCallableMock() + mock_tenant = NonCallableMock() + mock_manufacturer = NonCallableMock() + mock_rack = NonCallableMock() + mock_rack_position = NonCallableMock() + mock_device_type = NonCallableMock() + mock_serial_no = NonCallableMock() + + device = NetboxDcim.create_device(self, hostname=mock_hostname, site=mock_site, location=mock_location, tenant=mock_tenant, manufacturer=mock_manufacturer, + rack=mock_rack, rack_position=mock_rack_position, device_type=mock_device_type, serial_no=mock_serial_no) + + pass + + def test_create_device_type(self): + """ + Test creating a device type + """ + # WIP + self._netbox_api = MagicMock() + mock_model = NonCallableMock() + mock_manufacturer = NonCallableMock() + mock_slug = NonCallableMock() + mock_unit_height = NonCallableMock() + + device_type = NetboxDcim.create_device_type(self, model=mock_model, manufacturer=mock_manufacturer, slug=mock_slug, unit_height=mock_unit_height) + pass + + # currently failing test as raising MAC address error + def test_create_interface(self): + """ + Test interface creation + """ + # WIP + self._netbox_api = MagicMock() + mock_interface = NonCallableMock() + mock_hostname = NonCallableMock() + mock_interface_type = NonCallableMock() + mock_description = NonCallableMock() + mock_mac_addr = NonCallableMock() + mock_mac_addr_match = NetboxDcim.find_mac_addr(self._netbox_api, mac_address=mock_mac_addr) + + interface = NetboxDcim.create_interface(self._netbox_api, interface_name=mock_interface, hostname=mock_hostname, interface_type=mock_interface_type, + description=mock_description, mac_address=mock_mac_addr) + + # assert value is returned + + #@patch(NetboxDcim.find_mac_addr) + #@raises(MacAddressCollisionError) + def test_mac_address_collision(self): + """ + Test if interface creation is attempted + and mac address already exists then an error + is raised + """ + # WIP + self._netbox_api = MagicMock() + mac_addr_match = False + + pass \ No newline at end of file