Skip to content

Commit 03a90b6

Browse files
committedMay 28, 2020
added prime module
1 parent 7c88cbf commit 03a90b6

9 files changed

+261
-39
lines changed
 

‎README.md

+39-10
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
# netbox-scanner
2-
A scanner util for [NetBox](https://netbox.readthedocs.io/en/stable/), because certain networks can be updated automagically. netbox-scanner aims to create, update, and delete hosts (`/32`) in NetBox, either discovered after network scans and synchronized from other sources.
2+
A scanner util for [NetBox](https://netbox.readthedocs.io/en/stable/), because certain networks can be updated *automagically*. netbox-scanner aims to create, update, and delete hosts (`/32`) in NetBox, either discovered by network scans and synchronized from other sources.
3+
4+
> I know it's no more a scanner since version 2, because the focus now is to synchronize different databases to NetBox, so a better name would be `netbox-sync`.
35
46

57
## Installation
68
netbox-scanner is compatible with **Python 3.7+**, and can be installed like this:
79

8-
$ wget https://github.com/forkd/netbox-scanner/archive/master.zip
9-
$ unzip netbox-scanner-master.zip -d netbox-scanner
10-
$ cd netbox-scanner
11-
$ pip install -r requirements.txt
10+
```bash
11+
$ wget https://github.com/lopes/netbox-scanner/archive/master.zip
12+
$ unzip netbox-scanner-master.zip -d netbox-scanner
13+
$ cd netbox-scanner
14+
$ pip install -r requirements.txt
15+
```
1216

1317
After installation, use the `netbox-scanner.conf` file as an example to create your own and put this file in `/opt/netbox` or prepend its name with a dot and put it in your home directory --`~/.netbox-scanner.conf`. Keep reading to learn more about configuration.
1418

@@ -18,6 +22,8 @@ netbox-scanner reads a user-defined source to discover IP addresses and descript
1822

1923
It is important to note that if netbox-scanner cannot define the description for a given host, then it will insert the string defined in the `unknown` parameter. Users can change those names at their own will.
2024

25+
For NetBox access, this script uses [pynetbox](https://github.com/digitalocean/pynetbox) --worth saying that was tested under NetBox v2.6.7.
26+
2127
### Garbage Collection
2228
If the user marked the `cleanup` option to `yes`, then netbox-scanner will run a garbage collector after the synchronization finishes. Basically, it will get all IP addresses recorded in NetBox under the same tag. Then, both lists will be compared: the one just retrieved from NetBox and the list that was synced. Hosts in the first list that don't appear in the second list will be deleted.
2329

@@ -27,7 +33,7 @@ Users can interact with netbox-scanner by command line and configuration file.
2733

2834
The configuration file (`netbox-scanner.conf`) is where netbox-scanner looks for details such as authentication data and path to files. This file can be stored on the user's home directory or on `/opt/netbox`, but if you choose the first option, it must be a hidden file --`.netbox-scanner.conf`.
2935

30-
Remember that netbox-scanner will always look for this file at home directory, then at `/opt/netbox`, in this order. The first occurrence will be considered.
36+
> Remember that netbox-scanner will always look for this file at home directory, then at `/opt/netbox`, in this order. The first occurrence will be considered.
3137
3238

3339
## Modules
@@ -39,14 +45,37 @@ Since version 2.0, netbox-scanner is based on modules. This way, this program i
3945

4046

4147
## Nmap Module
42-
Performing the scans is beyond netbox-scanner features, so you must run Nmap and save the output as an XML file using the `-oX` parameter. Since this file can grow really fast, you can scan each network and save it as a single XML file. You just have to assure that all files are under the same directory before running the script --see `samples/nmap.sh` for an example.
48+
Performing the scans is beyond netbox-scanner features, so you must run [Nmap](https://nmap.org/) and save the output as an XML file using the `-oX` parameter. Since this file can grow really fast, you can scan each network and save it as a single XML file. You just have to assure that all files are under the same directory before running the script --see `samples/nmap.sh` for an example.
4349

44-
To properly setup this module, you must inform the path to the directory where the XML files reside, define a tag to insert to discovered hosts, and decide if clean up will take place.
50+
To properly setup this module, you must inform the path to the directory where the XML files reside, define a tag to insert to discovered hosts, and decide if clean up will take place. Tested on Nmap v7.80.
4551

4652

4753
## Prime Module
48-
To be written.
54+
This script accesses [Prime](https://www.cisco.com/c/en/us/products/cloud-systems-management/prime-infrastructure/index.html) through RESTful API and all routines are implemented here. Users only have to point to Prime's API, which looks like `https://prime.domain/webacs/api/v4/`, inform valid credentials allowed to use the API, and fill the other variables, just like in Nmap.
55+
56+
It is important to note everything was tested on Cisco Prime v3.4.0, using API v4. It was noticed that when trying to retrieve access points data, Prime requires more privileges, so you must explicitly inform that you wish this by using `Prime().run(access_points=True)`.
57+
58+
59+
## Tests
60+
Some basic tests are implemented under `tests`. This directory comes with a shell script to run all the tests at once, but before running it, remember to setup some environment variables required by them, such as `NETBOX_ADDRESS` and `NMAP_PATH`.
61+
62+
Here's the list of all variables:
63+
64+
```bash
65+
NETBOX_ADDRESS
66+
NETBOX_TOKEN
67+
68+
NMAP_PATH
69+
70+
PRIME_ADDRESS
71+
PRIME_USER
72+
PRIME_PASS
73+
```
74+
75+
76+
## New Modules
77+
New modules should be implemented as a new file with the name of the module, for instance `nbs/netxms.py`. In this case, a class `NetXMS` should be created in this file with a method `run`. Finally, in `netbox-scanner.py`, a function `cmd_netxms` should be created to execute the just created class, and another option should be created both in the argument parsing section and in the if structure inside the main block.
4978

5079

5180
## License
52-
`netbox-scanner` is licensed under an MIT license --read `LICENSE` file for more information.
81+
netbox-scanner is licensed under an MIT license --read `LICENSE` file for more information.

‎nbs/__init__.py

+10-11
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55

66
class NetBoxScanner(object):
77

8-
def __init__(self, address, token, tls_verify, hosts, tag, cleanup):
8+
def __init__(self, address, token, tls_verify, tag, cleanup):
99
self.netbox = api(address, token, ssl_verify=tls_verify)
10-
self.hosts = hosts
1110
self.tag = tag
1211
self.cleanup = cleanup
1312
self.stats = {
@@ -56,29 +55,29 @@ def sync_host(self, host):
5655

5756
return True
5857

59-
def garbage_collector(self):
58+
def garbage_collector(self, hosts):
6059
'''Removes records from NetBox not found in last sync'''
6160
nbhosts = self.netbox.ipam.ip_addresses.filter(tag=self.tag)
6261
for nbhost in nbhosts:
6362
nbh = str(nbhost).split('/')[0]
64-
if not any(nbh == addr[0] for addr in self.hosts):
63+
if not any(nbh == addr[0] for addr in hosts):
6564
nbhost.delete()
66-
logging.info(f'deleted: {nbhost[0]}')
65+
logging.info(f'deleted: {nbhost}')
6766
self.stats['deleted'] += 1
6867

69-
def sync(self):
70-
'''Synchronizes self.hosts to NetBox
71-
Returns synching statistics
68+
def sync(self, hosts):
69+
'''Syncs hosts to NetBox
70+
hosts: list of tuples like [(addr,description),...]
7271
'''
7372
for s in self.stats:
7473
self.stats[s] = 0
7574

76-
logging.info('started: {} hosts'.format(len(self.hosts)))
77-
for host in self.hosts:
75+
logging.info('started: {} hosts'.format(len(hosts)))
76+
for host in hosts:
7877
self.sync_host(host)
7978

8079
if self.cleanup:
81-
self.garbage_collector()
80+
self.garbage_collector(hosts)
8281

8382
logging.info('finished: .{} +{} ~{} -{} !{}'.format(
8483
self.stats['unchanged'],

‎nbs/prime.md

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Prime
2+
This is the [Cisco Prime](https://www.cisco.com/c/en/us/products/cloud-systems-management/prime-infrastructure/index.html) API wrapper. Prime is self-explanatory and it's highly recommended to read it before start using any endpoints. You can access this documentation page at API's root address (see below).
3+
4+
Prerequisites:
5+
6+
* Tested under Prime 3.4 and API v4
7+
* URL for API (usually `https://prime.corp/webacs/api/v4`)
8+
* Credentials for API access
9+
10+
To start using this wrapper, you must create a user in Prime with NBI permissions, according to the resources you need to access. Read `?id=authentication-doc` under Prime's own documentation to learn more about such privileges.
11+
12+
Remember that Prime's resources are case sensitive, so programmers must be aware when requesting something. Then, reading the documentation is pretty important. At this point, this wrapper only accepts reading requests, but it should be improved in the near future.
13+
14+
15+
## Basic Usage
16+
17+
```python
18+
>>> from corsair.cisco.prime import Api
19+
>>> prime = Api('https://prime.corp/webacs/api/v4', 'cors', 'Strong_P4$$w0rd!')
20+
>>> prime.op.read('aaa/tacacsPlusServer')
21+
>>> prime.data.read('Devices')
22+
>>> prime.data.read('Devices', full='true')
23+
>>> prime.data.read('AccessPoints', firstResult=350, full='true')
24+
```

‎nbs/prime.py

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import urllib.request
2+
3+
from urllib.parse import urlencode, urlsplit
4+
from json import loads
5+
from socket import timeout
6+
from ssl import _create_unverified_context
7+
from base64 import b64encode
8+
9+
10+
TIMEOUT = 20
11+
12+
13+
def gen_auth(username, password):
14+
'Generate basic authorization using username and password'
15+
return b64encode(f'{username}:{password}'.encode('utf-8')).decode()
16+
17+
def make_url(base_url, endpoint, resource):
18+
'Corsair creates URLs using this method'
19+
base_url = urlsplit(base_url)
20+
path = base_url.path + f'/{endpoint}/{resource}'
21+
path = path.replace('//', '/')
22+
path = path[:-1] if path.endswith('/') else path
23+
return base_url._replace(path=path).geturl()
24+
25+
26+
class Api(object):
27+
def __init__(self, base_url, username, password, tls_verify=True):
28+
self.base_url = base_url if base_url[-1] != '/' else base_url[:-1]
29+
self.auth = gen_auth(username, password)
30+
self.tls_verify = tls_verify
31+
self.credentials = (self.base_url, self.auth, self.tls_verify)
32+
33+
self.data = Endpoint(self.credentials, 'data')
34+
self.op = Endpoint(self.credentials, 'op')
35+
36+
37+
class Endpoint(object):
38+
def __init__(self, credentials, endpoint):
39+
self.base_url = credentials[0]
40+
self.endpoint = endpoint
41+
self.resource = ''
42+
self.auth = credentials[1]
43+
self.tls_verify = credentials[2]
44+
45+
def read(self, _resource, **filters):
46+
self.resource = f'{_resource}.json' # will only deal with JSON outputs
47+
first_result = 0 if 'firstResult' not in filters else filters['firstResult']
48+
max_results = 1000 if 'maxResults' not in filters else filters['maxResults']
49+
filters.update({'firstResult':first_result, 'maxResults':max_results})
50+
req = Request(make_url(self.base_url, self.endpoint, self.resource),
51+
self.auth, self.tls_verify)
52+
try:
53+
res = req.get(**filters)
54+
except timeout:
55+
raise Exception('Operation timedout')
56+
return loads(res.read()) # test for possible Prime errors
57+
58+
59+
class Request(object):
60+
def __init__(self, url, auth, tls_verify):
61+
self.url = url
62+
self.auth = auth
63+
self.timeout = TIMEOUT
64+
self.context = None if tls_verify else _create_unverified_context()
65+
self.headers = {
66+
'Content-Type': 'application/json',
67+
'Authorization': f'Basic {self.auth}'
68+
}
69+
70+
def get(self, **filters):
71+
url = f'{self.url}?{self.dotted_filters(**filters)}' if filters else self.url
72+
req = urllib.request.Request(url, headers=self.headers, method='GET')
73+
return urllib.request.urlopen(req, timeout=self.timeout, context=self.context)
74+
75+
def dotted_filters(self, **filters):
76+
'Prime filters start with a dot'
77+
if not filters:
78+
return ''
79+
else:
80+
return f'.{urlencode(filters).replace("&", "&.")}'
81+
82+
83+
class Prime(object):
84+
85+
def __init__(self, address, username, password, tls_verify, unknown):
86+
self.prime = Api(address, username, password, tls_verify)
87+
self.unknown = unknown
88+
self.hosts = list()
89+
90+
def run(self, access_points=False):
91+
'''Extracts devices from Cisco Prime
92+
access_points: if set to True, will try to get APs data
93+
Returns False for no errors or True if errors occurred
94+
'''
95+
errors = False
96+
devices = self.get_devices('Devices')
97+
for device in devices:
98+
try:
99+
self.hosts.append((
100+
device['devicesDTO']['ipAddress'],
101+
device['devicesDTO']['deviceName']
102+
))
103+
except KeyError:
104+
errors = True
105+
106+
if access_points:
107+
aps = self.get_devices('AccessPoints')
108+
for ap in aps:
109+
try:
110+
self.hosts.append((
111+
ap['accessPointsDTO']['ipAddress']['address'],
112+
ap['accessPointsDTO']['model']
113+
))
114+
except KeyError:
115+
errors = True
116+
return errors
117+
118+
def get_devices(self, resource):
119+
'This function is used to support run()'
120+
raw = list()
121+
res = self.prime.data.read(resource, full='true')
122+
count = res['queryResponse']['@count']
123+
last = res['queryResponse']['@last']
124+
raw.extend(res['queryResponse']['entity'])
125+
while last < count - 1:
126+
first_result = last + 1
127+
last += 1000
128+
res = self.prime.data.read(
129+
resource,
130+
full='true',
131+
firstResult=first_result
132+
)
133+
raw.extend(res['queryResponse']['entity'])
134+
return raw
135+

‎netbox-scanner.conf

+7-6
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ tag = nmap
1111
cleanup = yes
1212

1313
[PRIME]
14-
address = https://prime.domain
15-
username =
16-
password =
17-
unknown = autodiscovered:netbox-scanner
18-
tag = prime
19-
cleanup = yes
14+
address = https://prime.domain/webacs/api/v4
15+
username =
16+
password =
17+
tls_verify = no
18+
unknown = autodiscovered:netbox-scanner
19+
tag = prime
20+
cleanup = yes

‎netbox-scanner.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from nbs import NetBoxScanner
1313
from nbs.nmap import Nmap
14+
from nbs.prime import Prime
1415

1516

1617
local_config = expanduser('~/.netbox-scanner.conf')
@@ -49,24 +50,37 @@
4950
disable_warnings(InsecureRequestWarning)
5051

5152

52-
def cmd_nmap(): # nmap handler
53+
def cmd_nmap(s): # nmap handler
5354
h = Nmap(nmap['path'], nmap['unknown'])
5455
h.run()
55-
scan = NetBoxScanner(
56+
s.sync(h.hosts)
57+
58+
def cmd_prime(s): # prime handler
59+
h = Prime(
60+
prime['address'],
61+
prime['username'],
62+
prime['password'],
63+
prime.getboolean('tls_verify'),
64+
prime['unknown']
65+
)
66+
h.run() # set access_point=True to process APs
67+
s.sync(h.hosts)
68+
69+
70+
if __name__ == '__main__':
71+
scanner = NetBoxScanner(
5672
netbox['address'],
5773
netbox['token'],
5874
netbox.getboolean('tls_verify'),
59-
h.hosts,
6075
nmap['tag'],
6176
nmap.getboolean('cleanup')
6277
)
63-
scan.sync()
64-
65-
def cmd_prime(): # prime handler
66-
pass
6778

79+
if args.command == 'nmap':
80+
cmd_nmap(scanner)
81+
elif args.command == 'prime':
82+
scanner.tag = prime['tag']
83+
scanner.cleanup = prime.getboolean('cleanup')
84+
cmd_prime(scanner)
6885

69-
if __name__ == '__main__':
70-
if args.command == 'nmap': cmd_nmap()
71-
elif args.command == 'prime': cmd_prime()
7286
exit(0)

‎tests/run_tests.sh

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010

1111
python -m unittest tests.test_netbox
1212
python -m unittest tests.test_nmap
13+
python -m unittest tests.test_prime

‎tests/test_netbox.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ def test_api(self):
88
address = environ.get('NETBOX_ADDRESS')
99
token = environ.get('NETBOX_TOKEN')
1010

11-
netbox = NetBoxScanner(address, token, False, [], 'test', False)
11+
netbox = NetBoxScanner(address, token, False, 'test', False)
1212
self.assertIsInstance(netbox, NetBoxScanner)
13-
self.assertEqual(netbox.sync(), True)
13+
self.assertEqual(netbox.sync([]), True)
1414

1515

1616
if __name__ == '__main__':

0 commit comments

Comments
 (0)