├── LICENSE ├── LibrenmsAPI ├── LibreNMSAPI.py └── __init__.py └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 RobertH1993 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LibrenmsAPI/LibreNMSAPI.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | import requests 3 | import json 4 | 5 | 6 | class LibreNMSInvalidEndpointException(Exception): 7 | def __init__(self, message): 8 | super(LibreNMSInvalidEndpointException, self).__init__(message) 9 | 10 | 11 | class LibreNMSStatusNotOKException(Exception): 12 | def __init__(self, message): 13 | super(LibreNMSStatusNotOKException, self).__init__(message) 14 | 15 | 16 | class LibreNMSDataExtractionFailedException(Exception): 17 | def __init__(self, message): 18 | super(LibreNMSDataExtractionFailedException, self).__init__(message) 19 | 20 | 21 | class LibreNMSJsonNormalizer(): 22 | def __init__(self): 23 | pass 24 | 25 | def _extract_root_endpoint_objects(self, json, endpoint): 26 | objects_key = endpoint 27 | if endpoint == "devicegroups": 28 | objects_key = "groups" 29 | elif endpoint == "bgp": 30 | objects_key = "bgp_sessions" 31 | 32 | if objects_key not in json: 33 | objects_key = None 34 | 35 | singular = endpoint[:-1] 36 | if singular in json: 37 | return json[singular] 38 | 39 | return json[objects_key] 40 | 41 | def _extract_endpoint_objects(self, json, endpoint): 42 | root_endpoint = endpoint.split("/")[0] 43 | singular_root_endpoint = root_endpoint[:-1] 44 | 45 | if root_endpoint in json: 46 | return json[root_endpoint] 47 | elif singular_root_endpoint in json: 48 | return json[singular_root_endpoint] 49 | elif 'graphs' in json: 50 | return json['graphs'] 51 | elif 'addresses' in json: 52 | return json['addresses'] 53 | elif 'devices' in json: 54 | return json['devices'] 55 | 56 | return None 57 | 58 | def _is_root_endpoint(self, endpoint): 59 | if len(endpoint.split('/')) == 1: 60 | return True 61 | 62 | return False 63 | 64 | def get_json_data(self, json, endpoint): 65 | if self._is_root_endpoint(endpoint): 66 | objects = self._extract_root_endpoint_objects(json, endpoint) 67 | else: 68 | objects = self._extract_endpoint_objects(json, endpoint) 69 | 70 | if not objects: 71 | objects = [] 72 | 73 | return objects 74 | 75 | def get_json_id_keyname(self, json_objects, endpoint): 76 | if len(json_objects) == 0: 77 | return None 78 | 79 | first_object = json_objects[0] 80 | keyname = None 81 | if 'id' in first_object: 82 | return 'id' 83 | 84 | singular = endpoint[:-1] 85 | if "{}_id".format(singular) in first_object: 86 | keyname = "{}_id".format(singular) 87 | 88 | if "event_id" in first_object: 89 | keyname = "event_id" 90 | 91 | return keyname 92 | 93 | 94 | class LibreNMSEndpoint: 95 | valid_endpoints = { 96 | "bgp": { 97 | "bgp": ['GET'], 98 | }, 99 | 100 | "ospf": { 101 | "ospf": ['GET'], 102 | }, 103 | 104 | "oxidized": { 105 | "oxidized": ['GET'], 106 | }, 107 | 108 | "devicegroups": { 109 | "devicegroups": ['GET'], 110 | }, 111 | 112 | "portgroups": { 113 | "portgroups": ['GET'], 114 | "portgtoups/multiports/bit": ['GET'], 115 | }, 116 | 117 | "alerts": { 118 | "alerts": ['GET'], 119 | }, 120 | 121 | "rules": { 122 | "rules": ['GET'], 123 | }, 124 | 125 | "services": { 126 | "services": ['GET'], 127 | }, 128 | 129 | "resources": { 130 | "resources/links": ['GET'], 131 | "resources/locations": ['GET'], 132 | "resources/ip/addresses": ['GET'], 133 | "resources/ip/networks": ['GET'], 134 | "resources/ip/networks/{int}/ip": ['GET'], 135 | "resources/fdb": ['GET'], 136 | "resources/links/": ['GET'], 137 | "resources/sensors": ['GET'], 138 | "resources/vlans": ['GET'], 139 | }, 140 | 141 | "logs": { 142 | "logs/eventlog": ['GET'], 143 | "logs/eventlog/{int}": ['GET'], 144 | "logs/syslog": ['GET'], 145 | "logs/syslog/{int}": ['GET'], 146 | "logs/alertlog": ['GET'], 147 | 'logs/alertlog/{int}': ['GET'], 148 | "logs/authlog": ['GET'], 149 | "logs/authlog/{int}": ['GET'] 150 | }, 151 | 152 | "devices": { 153 | "devices": ['GET', 'POST', 'DELETE', 'PATCH', 'PUT'], 154 | "devices/{int}": ['GET'], 155 | "devices/{string}": ['GET'], 156 | "devices/{string}/discover": ['GET'], 157 | "devices/{int}/discover": ['GET'], 158 | "devices/{string}/graphs/health/{string}": ['GET'], 159 | "devices/{int}/graphs/health/{string}": ['GET'], 160 | "devices/{string}/graphs/wireless/{string}": ['GET'], 161 | "devices/{int}/graphs/wireless/{string}": ['GET'], 162 | "devices/{string}/vlans": ['GET'], 163 | "devices/{int}/vlans": ['GET'], 164 | "devices/{string}/links": ['GET'], 165 | "devices/{int}/links": ['GET'], 166 | "devices/{string}/graphs": ['GET'], 167 | "devices/{int}/graphs": ['GET'], 168 | "devices/{string}/fdb": ['GET'], 169 | "devices/{int}/fdb": ['GET'], 170 | "devices/{string}/health/{string}": ['GET'], 171 | "devices/{int}/health/{string}": ['GET'], 172 | "devices/{string}/wireless/{string}": ['GET'], 173 | "devices/{int}/wireless/{string}": ['GET'], 174 | "devices/{string}/ports": ['GET'], 175 | "devices/{int}/ports": ['GET'], 176 | "devices/{string}/ip": ['GET'], 177 | "devices/{int}/ip": ['GET'], 178 | "devices/{string}/port_stack": ['GET'], 179 | "devices/{int}/port_stack": ['GET'], 180 | "devices/{string}/components": ['GET'], 181 | "devices/{int}/components": ['GET'], 182 | "devices/{string}/groups": ['GET'], 183 | "devices/{int}/groups": ['GET'], 184 | "devices/{string}/ports/{string}": ['GET'], 185 | "devices/{int}/ports/{string}": ['GET'], 186 | }, 187 | 188 | "ports": { 189 | "ports": ['GET'], 190 | "ports/{int}": ['GET'], 191 | "ports/{int}/ip": ['GET'], 192 | }, 193 | 194 | "bills": { 195 | "bills": ['GET'], 196 | "bills/{int}": ['GET'], 197 | "bills/{int}/graphs": ['GET'], 198 | "bills/{int}/graphdata": ['GET'], 199 | "bills/{int}/history": ['GET'], 200 | "bills/{int}/history/{int}/graphs": ['GET'], 201 | "bills/{int}/history/{int}/graphdata": ['GET'], 202 | }, 203 | 204 | "routing": { 205 | "routing/vrf": ['GET'], 206 | "routing/ipsec/data": ['GET'], 207 | "routing/bgp/cbgp": ['GET'], 208 | }, 209 | 210 | "inventory": { 211 | "inventory": ['GET'], 212 | "inventory/{string}/all": ['GET'] 213 | }, 214 | } 215 | 216 | def __init__(self, endpoint_name, access_token, base_url, data=None): 217 | self._normalizer = LibreNMSJsonNormalizer() 218 | self._endpoint_data = {} if data is None else data 219 | self._endpoint_name = endpoint_name 220 | self._access_token = access_token 221 | self._base_url = base_url 222 | self._default_headers = { 223 | "Content-Type": "application/json", 224 | "Accept": "application/json", 225 | "X-Auth-Token": self._access_token 226 | } 227 | self._valid_methods = self._get_endpoint_request_methods() 228 | #TODO check if base url ends with trailing slash 229 | 230 | def _valid_response_status(self, json): 231 | if 'status' not in json: 232 | return False 233 | 234 | if json['status'] == "ok": 235 | return True 236 | 237 | def _valid_status_code(self, status_code): 238 | if status_code >= 200 and status_code <= 299: 239 | return True 240 | 241 | return False 242 | 243 | def _valid_response(self, response): 244 | if not self._valid_status_code(response.status_code): 245 | return False 246 | if not self._valid_response_status(json.loads(response.text)): 247 | return False 248 | 249 | return True 250 | 251 | def _create_url(self, parts): 252 | return "/".join(parts) 253 | 254 | def _get_endpoint_request_methods(self): 255 | splitted_endpoint = self._endpoint_name.split("/") 256 | if not splitted_endpoint[0] in LibreNMSEndpoint.valid_endpoints: 257 | raise LibreNMSInvalidEndpointException("Endpoint does not exists") 258 | 259 | possible_endpoints = LibreNMSEndpoint.valid_endpoints[splitted_endpoint[0]] 260 | for possible_endpoint in possible_endpoints.keys(): 261 | splitted_possible_endpoint = possible_endpoint.split("/") 262 | 263 | # Start by filtering on length 264 | if len(splitted_possible_endpoint) != len(splitted_endpoint): 265 | continue 266 | 267 | found_endpoint = True 268 | for i in range(len(splitted_endpoint)): 269 | if splitted_endpoint[i] == splitted_possible_endpoint[i]: 270 | continue 271 | if splitted_possible_endpoint[i] == "{int}" and splitted_endpoint[i].isdigit(): 272 | continue 273 | if splitted_possible_endpoint[i] == "{string}" and not splitted_endpoint[i].isdigit(): 274 | continue 275 | 276 | found_endpoint = False 277 | break 278 | 279 | if not found_endpoint: 280 | raise LibreNMSInvalidEndpointException("Endpoint {} does not exists".format(self._endpoint_name)) 281 | 282 | return possible_endpoints[possible_endpoint] 283 | 284 | def all(self): 285 | if 'GET' not in self._valid_methods: 286 | raise LibreNMSInvalidEndpointException("{} Endpoint doesnt accept GET command".format(self._endpoint_name)) 287 | 288 | url = self._create_url([self._base_url, self._endpoint_name]) 289 | response = requests.get( 290 | url, 291 | headers=self._default_headers 292 | ) 293 | 294 | if not self._valid_response(response): 295 | raise LibreNMSStatusNotOKException("An error occurred while fetching endpoint") 296 | 297 | json_objects = self._normalizer.get_json_data(json.loads(response.text), self._endpoint_name) 298 | if len(json_objects) == 0: 299 | return [] 300 | 301 | id_keyname = self._normalizer.get_json_id_keyname(json_objects, self._endpoint_name) 302 | if not id_keyname: 303 | raise LibreNMSDataExtractionFailedException("Cant find key to access data, did we retrieve a empty set?") 304 | 305 | endpoint_objects = [] 306 | for json_object in json_objects: 307 | endpoint_objects.append( 308 | LibreNMSEndpoint( 309 | "{}/{}".format(self._endpoint_name, json_object[id_keyname]), 310 | self._access_token, 311 | self._base_url, 312 | json_object 313 | ) 314 | ) 315 | 316 | return endpoint_objects 317 | 318 | def get(self, object_name): 319 | if 'GET' not in self._valid_methods: 320 | raise LibreNMSInvalidEndpointException("{} Endpoint doesnt accept GET command".format(self._endpoint_name)) 321 | 322 | url = self._create_url([self._base_url, self._endpoint_name, object_name]) 323 | response = requests.get( 324 | url, 325 | headers=self._default_headers 326 | ) 327 | 328 | if not self._valid_response(response): 329 | raise LibreNMSStatusNotOKException("An error occurred while fetching endpoint") 330 | 331 | json_objects = self._normalizer.get_json_data(json.loads(response.text), self._endpoint_name) 332 | if not json_objects: 333 | return [] 334 | 335 | id_keyname = self._normalizer.get_json_id_keyname(json_objects, self._endpoint_name) 336 | if not id_keyname: 337 | print(json_objects) 338 | raise LibreNMSDataExtractionFailedException("Failed to extract data, does endpoint or id exists?") 339 | 340 | json_object = json_objects[0] 341 | return LibreNMSEndpoint( 342 | "{}/{}".format(self._endpoint_name, object_name), 343 | self._access_token, 344 | self._base_url, 345 | data=json_object 346 | ) 347 | 348 | def delete(self, object_name): 349 | if 'DELETE' not in self._valid_methods: 350 | raise LibreNMSInvalidEndpointException("{} Endpoint doesnt accept DELETE command".format(self._endpoint_name)) 351 | 352 | url = self._create_url([self._base_url, self._endpoint_name, object_name]) 353 | response = requests.delete( 354 | url, 355 | headers=self._default_headers 356 | ) 357 | 358 | if not self._valid_response(response): 359 | raise LibreNMSStatusNotOKException("An error occurred while fetching endpoints") 360 | 361 | return json.loads(response.text) 362 | 363 | def create(self, data): 364 | if 'POST' not in self._valid_methods: 365 | raise LibreNMSInvalidEndpointException("{} Endpoint doesnt accept POST command".format(self._endpoint_name)) 366 | 367 | url = self._create_url([self._base_url, self._endpoint_name]) 368 | response = requests.post( 369 | url, 370 | headers=self._default_headers, 371 | json=data 372 | ) 373 | 374 | if not self._valid_response(response): 375 | raise LibreNMSStatusNotOKException("An error occured while fetching endpoints") 376 | 377 | return json.loads(response.text) 378 | 379 | def test(self): 380 | return self._get_endpoint_request_methods() 381 | 382 | def __setattr__(self, key, value): 383 | # Dirty hack to prevent recursion.. 384 | if key[0] == "_": 385 | super(LibreNMSEndpoint, self).__setattr__(key, value) 386 | else: 387 | self._endpoint_data[key] = value 388 | 389 | def __getattr__(self, attribute_name): 390 | if attribute_name in self._endpoint_data.keys(): 391 | return self._endpoint_data[attribute_name] 392 | else: 393 | return LibreNMSEndpoint( 394 | "{}/{}".format(self._endpoint_name, attribute_name), 395 | self._access_token, 396 | self._base_url 397 | ) 398 | 399 | def __repr__(self): 400 | return json.dumps(self._endpoint_data) 401 | 402 | 403 | class LibreNMSAPI: 404 | def __init__(self, access_token, base_url): 405 | self._access_token = access_token 406 | self._base_url = base_url 407 | 408 | def __getattr__(self, attribute_name): 409 | return LibreNMSEndpoint(attribute_name, self._access_token, self._base_url) 410 | 411 | -------------------------------------------------------------------------------- /LibrenmsAPI/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobertH1993/LibreNMSAPI/1ed697004a9fa121a5d8ce5118efac99919b7f84/LibrenmsAPI/__init__.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibreNMSAPI 2 | A Python API client library for (https://www.librenms.org/ "LibreNMS"). 3 | LibreNMS is a fully featured network monitoring system that provides a wealth of features and device support. 4 | 5 | ## Installation 6 | Add the folder LibreNMSAPI to your project this way it can be used as module. 7 | 8 | ## Quick start 9 | To begin import the API and create an instance of the LibreNMSAPI class. 10 | 11 | ``` python 12 | from LibrenmsAPI.LibreNMSAPI import LibreNMSAPI 13 | 14 | # Do not use a trailing slasg for the URL 15 | api = librenmsAPI( 16 | access_token = "token" 17 | base_url= "https://librenms.example.com/api/v0" 18 | ) 19 | ``` 20 | 21 | ## Queries 22 | You can reach all endpoints inside the LibreNMS API by calling there routes as attribute of the LibreNMSAPI instance. 23 | All queries return an Endpoint that can again be queried and in some cases contain the data from the queries endpoint (if any). 24 | For example: 25 | 26 | ``` python 27 | all_devices = api.devices.all() 28 | for device in all_devices: 29 | print(device.device_id) 30 | 31 | specific_device = api.devices.get("s01.example.com") 32 | if specific_device: 33 | print(device.device_id) 34 | 35 | specific_device.delete() 36 | ``` 37 | ## Contributing 38 | If you want to contribute please fork this project, push your changes and send a pull request. 39 | --------------------------------------------------------------------------------