def test_get_count(self): test_obj = Request( http_session=Mock(), base="http://localhost:8001/api/dcim/devices", filters={"q": "abcd"}, ) test_obj.http_session.get.return_value.json.return_value = { "count": 42, "next": "http://localhost:8001/api/dcim/devices?limit=1&offset=1&q=abcd", "previous": False, "results": [], } expected = call( "http://localhost:8001/api/dcim/devices/", params={ "q": "abcd", "limit": 1 }, headers={"accept": "application/json;"}, ) test_obj.http_session.get.ok = True test = test_obj.get_count() self.assertEqual(test, 42) test_obj.http_session.get.assert_called_with( "http://localhost:8001/api/dcim/devices/", params={ "q": "abcd", "limit": 1 }, headers={"accept": "application/json;"}, json=None, )
def all(self): """Queries the 'ListView' of a given endpoint. Returns all objects from an endpoint. :Returns: List of :py:class:`.Record` objects. :Examples: >>> nb.dcim.devices.all() [test1-a3-oobsw2, test1-a3-oobsw3, test1-a3-oobsw4] >>> """ req = Request( base="{}/".format(self.url), token=self.token, session_key=self.session_key, http_session=self.api.http_session, threading=self.api.threading, ) get_req = req.get() self.api.api_version = req.api_version return response_loader(get_req, self.return_obj, self)
def save(self): """Saves changes to an existing object. Takes a diff between the objects current state and its state at init and sends them as a dictionary to Request.patch(). :returns: True if PATCH request was successful. :example: >>> x = nb.dcim.devices.get(name='test1-a3-tor1b') >>> x.serial u'' >>> x.serial = '1234' >>> x.save() True >>> """ if self.id: diff = self._diff() if diff: serialized = self.serialize() req = Request( key=self.id, base=self.endpoint.url, token=self.api.token, session_key=self.api.session_key, http_session=self.api.http_session, ) if req.patch({i: serialized[i] for i in diff}): self.api.api_version = req.api_version return True return False
def test_get_openapi(self): test = Request("http://localhost:8080/api", Mock()) test.get_openapi() test.http_session.get.assert_called_with( "http://localhost:8080/api/docs/?format=openapi", headers={"Content-Type": "application/json;"}, )
def test_get_manual_pagination(self): test_obj = Request( http_session=Mock(), base="http://localhost:8001/api/dcim/devices", limit=10, offset=20, ) test_obj.http_session.get.return_value.json.return_value = { "count": 4, "next": "http://localhost:8001/api/dcim/devices?limit=10&offset=30", "previous": False, "results": [1, 2, 3, 4], } expected = call( "http://localhost:8001/api/dcim/devices/", params={ "offset": 20, "limit": 10 }, headers={"accept": "application/json;"}, ) test_obj.http_session.get.ok = True generator = test_obj.get() self.assertEqual(len(list(generator)), 4) test_obj.http_session.get.assert_called_with( "http://localhost:8001/api/dcim/devices/", params={ "offset": 20, "limit": 10 }, headers={"accept": "application/json;"}, json=None, )
def get(self, *args, **kwargs): r"""Queries the DetailsView of a given endpoint. :arg int,optional key: id for the item to be retrieved. :arg str,optional \**kwargs: Accepts the same keyword args as filter(). Any search argument the endpoint accepts can be added as a keyword arg. :returns: A single :py:class:`.Record` object or None :raises ValueError: if kwarg search return more than one value. :Examples: Referencing with a kwarg that only returns one value. >>> nb.dcim.devices.get(name='test1-a3-tor1b') test1-a3-tor1b >>> Referencing with an id. >>> nb.dcim.devices.get(1) test1-edge1 >>> """ try: key = args[0] except IndexError: key = None if not key: filter_lookup = self.filter(**kwargs) if filter_lookup: if len(filter_lookup) > 1: raise ValueError( "get() returned more than one result. " "Check that the kwarg(s) passed are valid for this " "endpoint or use filter() or all() instead." ) else: return filter_lookup[0] return None try: req = Request( key=key, base=self.url, token=self.token, session_key=self.session_key, http_session=self.api.http_session, ) except RequestError: return None return response_loader(req.get(), self.return_obj, self)
def delete(self, objects): r"""Bulk deletes objects on an endpoint. Allows for batch deletion of multiple objects from a single endpoint :arg list objects: A list of either ids or Records or a single RecordSet to delete. :returns: True if bulk DELETE operation was successful. :Examples: Deleting all `devices`: >>> netbox.dcim.devices.delete(netbox.dcim.devices.all(0)) >>> Use bulk deletion by passing a list of ids: >>> netbox.dcim.devices.delete([2, 243, 431, 700]) >>> Use bulk deletion to delete objects eg. when filtering on a `custom_field`: >>> netbox.dcim.devices.delete([ >>> d for d in netbox.dcim.devices.all(0) \ >>> if d.custom_fields.get('field', False) >>> ]) >>> """ cleaned_ids = [] if not isinstance(objects, list) and not isinstance( objects, RecordSet): raise ValueError("objects must be list[str|int|Record]" "|RecordSet - was " + str(type(objects))) for o in objects: if isinstance(o, int): cleaned_ids.append(o) elif isinstance(o, str) and o.isnumeric(): cleaned_ids.append(int(o)) elif isinstance(o, Record): if not hasattr(o, "id"): raise ValueError( "Record from '" + o.url + "' does not have an id and cannot be bulk deleted") cleaned_ids.append(o.id) else: raise ValueError("Invalid object in list of " "objects to delete: " + str(type(o))) req = Request( base=self.url, token=self.token, session_key=self.session_key, http_session=self.api.http_session, ) return True if req.delete(data=[{ "id": i } for i in cleaned_ids]) else False
def load_models(item): app, url = item appobj = getattr(self.netbox, app) models = Request(url, self.netbox.http_session).get() for model in models.keys(): if model[0] != '_': modelname = model.title().replace('-', '') modelobj = getattr(appobj, model.replace('-', '_')) if app == 'virtualization' and model == "interfaces": modelname = 'VirtualInterfaces' self.ns[modelname] = modelobj
def choices(self): """ Returns all choices from the endpoint. The returned dict is also saved in the endpoint object (in ``_choices`` attribute) so that later calls will return the same data without recurring requests to NetBox. When using ``.choices()`` in long-running applications, consider restarting them whenever NetBox is upgraded, to prevent using stale choices data. :Returns: Dict containing the available choices. :Example (from NetBox 2.8.x): >>> from pprint import pprint >>> pprint(nb.ipam.ip_addresses.choices()) {'role': [{'display_name': 'Loopback', 'value': 'loopback'}, {'display_name': 'Secondary', 'value': 'secondary'}, {'display_name': 'Anycast', 'value': 'anycast'}, {'display_name': 'VIP', 'value': 'vip'}, {'display_name': 'VRRP', 'value': 'vrrp'}, {'display_name': 'HSRP', 'value': 'hsrp'}, {'display_name': 'GLBP', 'value': 'glbp'}, {'display_name': 'CARP', 'value': 'carp'}], 'status': [{'display_name': 'Active', 'value': 'active'}, {'display_name': 'Reserved', 'value': 'reserved'}, {'display_name': 'Deprecated', 'value': 'deprecated'}, {'display_name': 'DHCP', 'value': 'dhcp'}]} >>> """ if self._choices: return self._choices req = Request( base=self.url, token=self.api.token, private_key=self.api.private_key, http_session=self.api.http_session, ) req.options() self.api.api_version = req.api_version try: post_data = req["actions"]["POST"] except KeyError: raise ValueError( "Unexpected format in the OPTIONS response at {}".format( self.url)) self._choices = {} for prop in post_data: if "choices" in post_data[prop]: self._choices[prop] = post_data[prop]["choices"] return self._choices
def count(self, *args, **kwargs): r"""Returns the count of objects in a query. Takes named arguments that match the usable filters on a given endpoint. If an argument is passed then it's used as a freeform search argument if the endpoint supports it. If no arguments are passed the count for all objects on an endpoint are returned. :arg str,optional \*args: Freeform search string that's accepted on given endpoint. :arg str,optional \**kwargs: Any search argument the endpoint accepts can be added as a keyword arg. :Returns: Integer with count of objects returns by query. :Examples: To return a count of objects matching a named argument filter. >>> nb.dcim.devices.count(site='tst1') 5827 >>> To return a count of objects on an entire endpoint. >>> nb.dcim.devices.count() 87382 >>> """ if args: kwargs.update({"q": args[0]}) if any(i in RESERVED_KWARGS for i in kwargs): raise ValueError( "A reserved {} kwarg was passed. Please remove it " "try again.".format(RESERVED_KWARGS) ) ret = Request( filters=kwargs, base=self.url, token=self.token, session_key=self.session_key, ssl_verify=self.ssl_verify, http_session=self.api.http_session, ) return ret.get_count()
def search(ep): req = Request(filters=dict(q=self.args.searchterm), base=ep.url, token=ep.token, session_key=ep.session_key, http_session=ep.api.http_session) rep = req._make_call(add_params=dict(limit=15)) result = list() if rep.get('results'): result = response_loader(rep['results'], ep.return_obj, ep) return result
def create(self, *args, **kwargs): r"""Creates an object on an endpoint. Allows for the creation of new objects on an endpoint. Named arguments are converted to json properties, and a single object is created. NetBox's bulk creation capabilities can be used by passing a list of dictionaries as the first argument. .. note: Any positional arguments will supercede named ones. :arg list \*args: A list of dictionaries containing the properties of the objects to be created. :arg str \**kwargs: key/value strings representing properties on a json object. :returns: A list or single :py:class:`.Record` object depending on whether a bulk creation was requested. :Examples: Creating an object on the `devices` endpoint: >>> device = netbox.dcim.devices.create( ... name='test', ... device_role=1, ... ) >>> Use bulk creation by passing a list of dictionaries: >>> nb.dcim.devices.create([ ... { ... "name": "test1-core3", ... "device_role": 3, ... "site": 1, ... "device_type": 1, ... "status": 1 ... }, ... { ... "name": "test1-core4", ... "device_role": 3, ... "site": 1, ... "device_type": 1, ... "status": 1 ... } ... ]) """ req = Request( base=self.url, token=self.token, session_key=self.session_key, http_session=self.api.http_session, ).post(args[0] if args else kwargs) if isinstance(req, list): return [self.return_obj(i, self.api, self) for i in req] return self.return_obj(req, self.api, self)
def all(self, limit=0, offset=None): """Queries the 'ListView' of a given endpoint. Returns all objects from an endpoint. :arg int,optional limit: Overrides the max page size on paginated returns. :arg int,optional offset: Overrides the offset on paginated returns. :Returns: A :py:class:`.RecordSet` object. :Examples: >>> devices = nb.dcim.devices.all() >>> for device in devices: ... print(device.name) ... test1-leaf1 test1-leaf2 test1-leaf3 >>> """ if limit == 0 and offset is not None: raise ValueError("offset requires a positive limit value") req = Request( base="{}/".format(self.url), token=self.token, session_key=self.session_key, http_session=self.api.http_session, threading=self.api.threading, limit=limit, offset=offset, ) return RecordSet(self, req)
def custom_choices(self): """ Returns _custom_field_choices response from app .. note:: This method only works with NetBox version 2.9.x or older. NetBox 2.10.0 introduced the ``/extras/custom-fields/`` endpoint that can be used f.ex. like ``nb.extras.custom_fields.all()``. :Returns: Raw response from NetBox's _custom_field_choices endpoint. :Raises: :py:class:`.RequestError` if called for an invalid endpoint. :Example: >>> nb.extras.custom_choices() {'Testfield1': {'Testvalue2': 2, 'Testvalue1': 1}, 'Testfield2': {'Othervalue2': 4, 'Othervalue1': 3}} """ custom_field_choices = Request( base="{}/{}/_custom_field_choices/".format( self.api.base_url, self.name, ), token=self.api.token, private_key=self.api.private_key, http_session=self.api.http_session, ).get() return custom_field_choices
def all(self, limit=0): """Queries the 'ListView' of a given endpoint. Returns all objects from an endpoint. :arg int,optional limit: Overrides the max page size on paginated returns. :Returns: A :py:class:`.RecordSet` object. :Examples: >>> devices = nb.dcim.devices.all() >>> for device in devices: ... print(device.name) ... test1-leaf1 test1-leaf2 test1-leaf3 >>> """ req = Request( base="{}/".format(self.url), token=self.token, session_key=self.session_key, http_session=self.api.http_session, threading=self.api.threading, limit=limit, ) return RecordSet(self, req)
def version(self): """ Gets the API version of NetBox. Can be used to check the NetBox API version if there are version-dependent features or syntaxes in the API. :Returns: Version number as a string. :Example: >>> import pynetbox >>> nb = pynetbox.api( ... 'http://localhost:8000', ... private_key_file='/path/to/private-key.pem', ... token='d6f4e314a5b5fefd164995169f28ae32d987704f' ... ) >>> nb.version '2.6' >>> """ version = Request( base=self.base_url, ssl_verify=self.ssl_verify, http_session=self.http_session, ).get_version() return version
def create(self, data=None): """The write operation for a detail endpoint. Creates objects on a detail endpoint in NetBox. :arg dict/list,optional data: A dictionary containing the key/value pair of the items you're creating on the parent object. Defaults to empty dict which will create a single item with default values. :returns: A dictionary or list of dictionaries its created in NetBox. """ if not data: return Request(**self.request_kwargs).post({}) return Request(**self.request_kwargs).post(data)
def list(self, **kwargs): r"""The view operation for a detail endpoint Returns the response from NetBox for a detail endpoint. :args \**kwargs: key/value pairs that get converted into url parameters when passed to the endpoint. E.g. ``.list(method='get_facts')`` would be converted to ``.../?method=get_facts``. :returns: A dictionary or list of dictionaries retrieved from NetBox. """ req = Request(**self.request_kwargs).get(add_params=kwargs) if self.custom_return: if isinstance(req, list): return [ self.custom_return( i, self.parent_obj.api, self.parent_obj.endpoint ) for i in req ] return self.custom_return( req, self.parent_obj.api, self.parent_obj.endpoint ) return req
def create(self, data=None): """The write operation for a detail endpoint. Creates objects on a detail endpoint in NetBox. :arg dict/list,optional data: A dictionary containing the key/value pair of the items you're creating on the parent object. Defaults to empty dict which will create a single item with default values. :returns: A :py:class:`.Record` object or list of :py:class:`.Record` objects created from data created in NetBox. """ data = data or {} req = Request(**self.request_kwargs).post(data) if self.custom_return: if isinstance(req, list): return [ self.custom_return(req_item, self.parent_obj.endpoint.api, self.parent_obj.endpoint) for req_item in req ] else: return self.custom_return(req, self.parent_obj.endpoint.api, self.parent_obj.endpoint) return req
def config(self): """ Returns config response from app :Returns: Raw response from NetBox's config endpoint. :Raises: :py:class:`.RequestError` if called for an invalid endpoint. :Example: >>> pprint.pprint(nb.users.config()) {'tables': {'DeviceTable': {'columns': ['name', 'status', 'tenant', 'device_role', 'site', 'primary_ip', 'tags']}}} """ config = Request( base="{}/{}/config/".format( self.api.base_url, self.name, ), token=self.api.token, private_key=self.api.private_key, http_session=self.api.http_session, ).get() return config
def status(self): """Gets the status information from NetBox. Available in NetBox 2.10.0 or newer. :Returns: Dictionary as returned by NetBox. :Raises: :py:class:`.RequestError` if the request is not successful. :Example: >>> pprint.pprint(nb.status()) {'django-version': '3.1.3', 'installed-apps': {'cacheops': '5.0.1', 'debug_toolbar': '3.1.1', 'django_filters': '2.4.0', 'django_prometheus': '2.1.0', 'django_rq': '2.4.0', 'django_tables2': '2.3.3', 'drf_yasg': '1.20.0', 'mptt': '0.11.0', 'rest_framework': '3.12.2', 'taggit': '1.3.0', 'timezone_field': '4.0'}, 'netbox-version': '2.10.2', 'plugins': {}, 'python-version': '3.7.3', 'rq-workers-running': 1} >>> """ status = Request( base=self.base_url, token=self.token, http_session=self.http_session, ).get_status() return status
def getnext(self, *args): try: offset = args[0] except IndexError: offset = 0 req = Request( offset=offset, base=self.url, token=self.token, session_key=self.session_key, http_session=self.api.http_session, ) return response_loader(req.get(), self.return_obj, self)
def update(self, objects): r"""Bulk updates existing objects on an endpoint. Allows for bulk updating of existing objects on an endpoint. Objects is a list whic contain either json/dicts or Record derived objects, which contain the updates to apply. If json/dicts are used, then the id of the object *must* be included :arg list objects: A list of dicts or Record. :returns: True if the update succeeded :Examples: Updating objects on the `devices` endpoint: >>> device = netbox.dcim.devices.update([ ... {'id': 1, 'name': 'test'}, ... {'id': 2, 'name': 'test2'}, ... ]) >>> True Use bulk update by passing a list of Records: >>> devices = nb.dcim.devices.all() >>> for d in devices: >>> d.name = d.name+'-test' >>> nb.dcim.devices.update(devices) >>> True """ series = [] if not isinstance(objects, list): raise ValueError( "Objects passed must be list[dict|Record] - was " + type(objects)) for o in objects: if isinstance(o, Record): data = o.updates() if data: data["id"] = o.id series.append(data) elif isinstance(o, dict): if "id" not in o: raise ValueError("id is missing from object: " + str(o)) series.append(o) else: raise ValueError("Object passed must be dict|Record - was " + type(objects)) req = Request( base=self.url, token=self.token, session_key=self.session_key, http_session=self.api.http_session, ).patch(series) if isinstance(req, list): return [self.return_obj(i, self.api, self) for i in req] return self.return_obj(req, self.api, self)
def __init__( self, url, token=None, private_key=None, private_key_file=None, ssl_verify=True, threading=False, ): if private_key and private_key_file: raise ValueError( '"private_key" and "private_key_file" cannot be used together.' ) base_url = "{}/api".format(url if url[-1] != "/" else url[:-1]) self.token = token self.private_key = private_key self.private_key_file = private_key_file self.base_url = base_url self.ssl_verify = ssl_verify self.session_key = None self.http_session = requests.Session() if threading and sys.version_info.major == 2: raise NotImplementedError("Threaded pynetbox calls not supported \ in Python 2") self.threading = threading if self.private_key_file: with open(self.private_key_file, "r") as kf: private_key = kf.read() self.private_key = private_key req = Request(base=base_url, token=token, private_key=private_key, ssl_verify=ssl_verify, http_session=self.http_session) if self.token and self.private_key: self.session_key = req.get_session_key() self.dcim = App(self, "dcim") self.ipam = App(self, "ipam") self.circuits = App(self, "circuits") self.secrets = App(self, "secrets") self.tenancy = App(self, "tenancy") self.extras = App(self, "extras") self.virtualization = App(self, "virtualization")
def delete(self): """Deletes an existing object. :returns: True if DELETE operation was successful. :example: >>> x = nb.dcim.devices.get(name='test1-a3-tor1b') >>> x.delete() True >>> """ req = Request( key=self.id, base=self.endpoint.url, token=self.api.token, session_key=self.api.session_key, http_session=self.api.http_session, ) return True if req.delete() else False
def _set_session_key(self): if getattr(self.api, "session_key"): return if self.api.token and self.api.private_key: self.api.session_key = Request( base=self.api.base_url, token=self.api.token, private_key=self.api.private_key, http_session=self.api.http_session, ).get_session_key()
def trace(self): req = Request( key=str(self.id) + "/trace", base=self.endpoint.url, token=self.api.token, session_key=self.api.session_key, http_session=self.api.http_session, ) uri_to_obj_class_map = { "dcim/cables": Cables, "dcim/front-ports": FrontPorts, "dcim/interfaces": Interfaces, "dcim/rear-ports": RearPorts, } ret = [] for (termination_a_data, cable_data, termination_b_data) in req.get(): this_hop_ret = [] for hop_item_data in (termination_a_data, cable_data, termination_b_data): # if not fully terminated then some items will be None if not hop_item_data: this_hop_ret.append(hop_item_data) continue # TODO: Move this to a more general function. app_endpoint = "/".join( urlsplit(hop_item_data["url"] [len(self.api.base_url):]).path.split("/")[1:3]) return_obj_class = uri_to_obj_class_map.get( app_endpoint, Record, ) this_hop_ret.append( return_obj_class(hop_item_data, self.endpoint.api, self.endpoint)) ret.append(this_hop_ret) return ret
def __init__( self, url, token=None, private_key=None, private_key_file=None, ssl_verify=True, ): if private_key and private_key_file: raise ValueError( '"private_key" and "private_key_file" cannot be used together.' ) base_url = "{}/api".format(url if url[-1] != "/" else url[:-1]) self.token = token self.private_key = private_key self.private_key_file = private_key_file self.base_url = base_url self.ssl_verify = ssl_verify self.session_key = None if self.private_key_file: with open(self.private_key_file, "r") as kf: private_key = kf.read() self.private_key = private_key req = Request( base=base_url, token=token, private_key=private_key, ssl_verify=ssl_verify, ) if self.token and self.private_key: self.session_key = req.get_session_key() self.dcim = App(self, "dcim") self.ipam = App(self, "ipam") self.circuits = App(self, "circuits") self.secrets = App(self, "secrets") self.tenancy = App(self, "tenancy") self.extras = App(self, "extras") self.virtualization = App(self, "virtualization")
def full_details(self): """Queries the hyperlinked endpoint if 'url' is defined. This method will populate the attributes from the detail endpoint when it's called. Sets the class-level `has_details` attribute when it's called to prevent being called more than once. :returns: True """ if self.url: req = Request( base=self.url, token=self.api.token, session_key=self.api.session_key, http_session=self.api.http_session, ) self._parse_values(next(req.get())) self.has_details = True return True return False
def choices(self): """ Returns all choices from the endpoint. The returned dict is also saved in the endpoint object (in ``_choices`` attribute) so that later calls will return the same data without recurring requests to NetBox. When using ``.choices()`` in long-running applications, consider restarting them whenever NetBox is upgraded, to prevent using stale choices data. :Returns: Dict containing the available choices. :Examples: >>> from pprint import pprint >>> pprint(nb.ipam.ip_addresses.choices()) {'role': [{'display_name': 'Secondary', 'value': 20}, {'display_name': 'VIP', 'value': 40}, {'display_name': 'VRRP', 'value': 41}, {'display_name': 'Loopback', 'value': 10}, {'display_name': 'GLBP', 'value': 43}, {'display_name': 'CARP', 'value': 44}, {'display_name': 'HSRP', 'value': 42}, {'display_name': 'Anycast', 'value': 30}], 'status': [{'display_name': 'Active', 'value': 1}, {'display_name': 'Reserved', 'value': 2}, {'display_name': 'Deprecated', 'value': 3}, {'display_name': 'DHCP', 'value': 5}]} >>> """ if self._choices: return self._choices req = Request( base=self.url, token=self.api.token, private_key=self.api.private_key, ssl_verify=self.api.ssl_verify, http_session=self.api.http_session, ).options() try: post_data = req['actions']['POST'] except KeyError: raise ValueError( "Unexpected format in the OPTIONS response at {}".format( self.url ) ) self._choices = {} for prop in post_data: if 'choices' in post_data[prop]: self._choices[prop] = post_data[prop]['choices'] return self._choices