umnetdb-utils 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- umnetdb_utils-0.1.0/PKG-INFO +22 -0
- umnetdb_utils-0.1.0/README.md +2 -0
- umnetdb_utils-0.1.0/pyproject.toml +24 -0
- umnetdb_utils-0.1.0/umnetdb_utils/__init__.py +4 -0
- umnetdb_utils-0.1.0/umnetdb_utils/base.py +140 -0
- umnetdb_utils-0.1.0/umnetdb_utils/umnetdb.py +58 -0
- umnetdb_utils-0.1.0/umnetdb_utils/umnetdisco.py +233 -0
- umnetdb_utils-0.1.0/umnetdb_utils/umnetequip.py +157 -0
- umnetdb_utils-0.1.0/umnetdb_utils/umnetinfo.py +489 -0
- umnetdb_utils-0.1.0/umnetdb_utils/utils.py +39 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: umnetdb-utils
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Helper classes for querying UMnet databases
|
5
|
+
License: MIT
|
6
|
+
Author: Amy Liebowitz
|
7
|
+
Author-email: amylieb@umich.edu
|
8
|
+
Requires-Python: >=3.10
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
15
|
+
Requires-Dist: oracledb (>=3.1.0,<4.0.0)
|
16
|
+
Requires-Dist: psycopg[binary] (>=3.2.9,<4.0.0)
|
17
|
+
Requires-Dist: sqlalchemy (>=2.0.41,<3.0.0)
|
18
|
+
Description-Content-Type: text/markdown
|
19
|
+
|
20
|
+
# umnetdb-utils
|
21
|
+
Helper classes for gathering data from umnet databases
|
22
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
[project]
|
2
|
+
name = "umnetdb-utils"
|
3
|
+
version = "0.1.0"
|
4
|
+
description = "Helper classes for querying UMnet databases"
|
5
|
+
authors = [
|
6
|
+
{name = "Amy Liebowitz",email = "amylieb@umich.edu"}
|
7
|
+
]
|
8
|
+
license = {text = "MIT"}
|
9
|
+
readme = "README.md"
|
10
|
+
requires-python = ">=3.10"
|
11
|
+
dependencies = [
|
12
|
+
"sqlalchemy (>=2.0.41,<3.0.0)",
|
13
|
+
"oracledb (>=3.1.0,<4.0.0)",
|
14
|
+
"psycopg[binary] (>=3.2.9,<4.0.0)"
|
15
|
+
]
|
16
|
+
|
17
|
+
[tool.poetry]
|
18
|
+
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
20
|
+
ruff = "^0.11.10"
|
21
|
+
|
22
|
+
[build-system]
|
23
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
24
|
+
build-backend = "poetry.core.masonry.api"
|
@@ -0,0 +1,140 @@
|
|
1
|
+
|
2
|
+
from os import getenv
|
3
|
+
import re
|
4
|
+
import logging
|
5
|
+
|
6
|
+
from sqlalchemy import create_engine, text
|
7
|
+
from sqlalchemy.orm import Session
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
class UMnetdbBase:
|
12
|
+
"""
|
13
|
+
Base helper class
|
14
|
+
"""
|
15
|
+
# set in child classes - you can use environment variables within curly braces here
|
16
|
+
URL = None
|
17
|
+
|
18
|
+
def __init__(self):
|
19
|
+
"""
|
20
|
+
Initiate a umnetdb object. Note that you must provide either a url or a path
|
21
|
+
to an env file that looks like the 'sample_env' provided in this repo.
|
22
|
+
If both are provided, the url takes precedence.
|
23
|
+
"""
|
24
|
+
self.url = self._resolve_url()
|
25
|
+
self.engine = create_engine(self.url)
|
26
|
+
self.session = None
|
27
|
+
|
28
|
+
def _resolve_url(self) -> str:
|
29
|
+
"""
|
30
|
+
Resolves any reference to environment variables in the url attribute
|
31
|
+
and returns the string. The string should use curly braces to indicate an environment
|
32
|
+
variable
|
33
|
+
"""
|
34
|
+
url = self.URL
|
35
|
+
for m in re.finditer(r"{(\w+)}", url):
|
36
|
+
var = m.group(1)
|
37
|
+
|
38
|
+
if not getenv(var):
|
39
|
+
raise ValueError(f"Undefined environment variable {var} in {url}")
|
40
|
+
url = re.sub(r"{" + var + "}", getenv(var), url)
|
41
|
+
|
42
|
+
return url
|
43
|
+
|
44
|
+
def open(self):
|
45
|
+
"""
|
46
|
+
Create a new session to the database if there isn't one already
|
47
|
+
"""
|
48
|
+
if not self.session:
|
49
|
+
self.session = Session(self.engine)
|
50
|
+
|
51
|
+
def close(self):
|
52
|
+
"""
|
53
|
+
Closes db session if there is one
|
54
|
+
"""
|
55
|
+
if self.session:
|
56
|
+
self.session.close()
|
57
|
+
self.session = None
|
58
|
+
|
59
|
+
def __enter__(self):
|
60
|
+
self.open()
|
61
|
+
return self
|
62
|
+
|
63
|
+
def __exit__(self, fexc_type, exc_val, exc_tb):
|
64
|
+
self.close()
|
65
|
+
|
66
|
+
def __getattr__(self, val:str):
|
67
|
+
if self.session:
|
68
|
+
return getattr(self.session, val)
|
69
|
+
|
70
|
+
raise AttributeError(self)
|
71
|
+
|
72
|
+
def _build_select(self, select, table, joins=None, where=None, order_by=None, limit=None, group_by=None, distinct=False):
|
73
|
+
'''
|
74
|
+
Generic 'select' query string builder built from standard query components as input.
|
75
|
+
The user is required to generate substrings for the more complex inputs
|
76
|
+
(eg joins, where statements), this function just puts all the components
|
77
|
+
together in the right order with appropriately-added newlines (for debugging purposes)
|
78
|
+
and returns the result.
|
79
|
+
|
80
|
+
:select: a list of columns to select
|
81
|
+
ex: ["nip.mac", "nip.ip", "n.switch", "n.port"]
|
82
|
+
:table: a string representing a table (or comma-separated tables, with our without aliases)
|
83
|
+
ex: "node_ip nip"
|
84
|
+
:joins: a list of strings representing join statements. Include the actual 'join' part!
|
85
|
+
ex: ["join node n on nip.mac = n.mac", "join device d on d.ip = n.switch"]
|
86
|
+
:where: A list of 'where' statements without the 'where'. The list of statements are
|
87
|
+
"anded". If you need "or", embed it in one of your list items
|
88
|
+
ex: ["node_ip.ip = '1.2.3.4'", "node.switch = '10.233.0.5'"]
|
89
|
+
:order_by: A string representing a column name (or names) to order by
|
90
|
+
:group_by: A string representing a column name (or names) to group by
|
91
|
+
:limit: An integer
|
92
|
+
|
93
|
+
'''
|
94
|
+
|
95
|
+
# First part of the sql statement is the 'select'
|
96
|
+
distinct = 'distinct ' if distinct else ''
|
97
|
+
sql = f"select {distinct}" + ", ".join(select) + "\n"
|
98
|
+
|
99
|
+
# Next is the table
|
100
|
+
sql += f"from {table}\n"
|
101
|
+
|
102
|
+
# Now are the joins. The 'join' keyword(s) need to be provided
|
103
|
+
# as part of the input, allowing for different join types
|
104
|
+
if joins:
|
105
|
+
for j in joins:
|
106
|
+
sql += f"{j}\n"
|
107
|
+
|
108
|
+
# Next are the filters. They are 'anded'
|
109
|
+
if where:
|
110
|
+
sql += "where\n"
|
111
|
+
sql += " and\n".join(where) + "\n"
|
112
|
+
|
113
|
+
# Finally the other options
|
114
|
+
if order_by:
|
115
|
+
sql += f"order by {order_by}\n"
|
116
|
+
|
117
|
+
if group_by:
|
118
|
+
sql += f"group by {group_by}\n"
|
119
|
+
|
120
|
+
if limit:
|
121
|
+
sql += f"limit {limit}\n"
|
122
|
+
|
123
|
+
logger.debug(f"Generated SQL command:\n****\n{sql}\n****\n")
|
124
|
+
|
125
|
+
return sql
|
126
|
+
|
127
|
+
def _execute(self, sql, rows_as_dict=True):
|
128
|
+
'''
|
129
|
+
Generic sqlalchemy "execute this sql command and give me all the results"
|
130
|
+
'''
|
131
|
+
with self as session:
|
132
|
+
r = session.execute(text(sql))
|
133
|
+
rows = r.fetchall()
|
134
|
+
|
135
|
+
if rows and rows_as_dict:
|
136
|
+
return [r._mapping for r in rows]
|
137
|
+
elif rows:
|
138
|
+
return rows
|
139
|
+
else:
|
140
|
+
return []
|
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
from sqlalchemy import text
|
5
|
+
from .base import UMnetdbBase
|
6
|
+
|
7
|
+
|
8
|
+
class UMnetdb(UMnetdbBase):
|
9
|
+
|
10
|
+
URL="postgresql+psycopg://{UMNETDB_USER}:{UMNETDB_PASSWORD}@wintermute.umnet.umich.edu/umnetdb"
|
11
|
+
|
12
|
+
def get_device_neighbors(
|
13
|
+
self, device: str, known_devices_only: bool = True
|
14
|
+
) -> List[dict]:
|
15
|
+
"""
|
16
|
+
Gets a list of the neighbors of a particular device. If the port
|
17
|
+
has a parent in the LAG table that is included as well.
|
18
|
+
Neighbor hostname is also looked up in the device table and
|
19
|
+
the "source of truth" hostname is returned instead of what shows
|
20
|
+
up in lldp neighbor.
|
21
|
+
|
22
|
+
Setting 'known_devices_only' to true only returns neighbors that are found
|
23
|
+
in umnet_db's device table. Setting it to false will return all lldp neighbors.
|
24
|
+
|
25
|
+
Returns results as a list of dictionary entries keyed on column names.
|
26
|
+
"""
|
27
|
+
|
28
|
+
if known_devices_only:
|
29
|
+
select = [
|
30
|
+
"n.port",
|
31
|
+
"n_d.name as remote_device",
|
32
|
+
"n.remote_port",
|
33
|
+
"l.parent",
|
34
|
+
"n_l.parent as remote_parent"
|
35
|
+
]
|
36
|
+
else:
|
37
|
+
select = [
|
38
|
+
"n.port",
|
39
|
+
"coalesce(n_d.name, n.remote_device) as remote_device",
|
40
|
+
"n.remote_port",
|
41
|
+
"l.parent",
|
42
|
+
"n_l.parent as remote_parent"
|
43
|
+
]
|
44
|
+
|
45
|
+
table = "neighbor n"
|
46
|
+
|
47
|
+
joins = [
|
48
|
+
"left outer join device n_d on n_d.hostname=n.remote_device",
|
49
|
+
"left outer join lag l on l.device=n.device and l.member=n.port",
|
50
|
+
"left outer join lag n_l on n_l.device=n_d.name and n_l.member=n.remote_port",
|
51
|
+
]
|
52
|
+
|
53
|
+
where = [f"n.device='{device}'"]
|
54
|
+
|
55
|
+
query = self._build_select(select, table, joins, where)
|
56
|
+
result = self.session.execute(text(query))
|
57
|
+
|
58
|
+
return [dict(zip(result.keys(), r)) for r in result]
|
@@ -0,0 +1,233 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
from .base import UMnetdbBase
|
4
|
+
from .utils import is_ip_address, is_mac_address
|
5
|
+
|
6
|
+
|
7
|
+
class Umnetdisco(UMnetdbBase):
|
8
|
+
URL="postgresql+psycopg://{NETDISCO_DB_USER}:{NETDISCO_DB_PASSWORD}@netdisco.umnet.umich.edu:5432/netdisco"
|
9
|
+
|
10
|
+
def host_arp(self, host, start_time=None, end_time=None, limit=1, active_only=False):
|
11
|
+
'''
|
12
|
+
Does an ARP query against the netdisco db for a single host.
|
13
|
+
:host: A string representing a MAC address or an IPv4 address
|
14
|
+
'''
|
15
|
+
|
16
|
+
# what table are we querying?
|
17
|
+
table = 'node_ip nip'
|
18
|
+
joins = ['join node n on nip.mac = n.mac']
|
19
|
+
|
20
|
+
# define select statements
|
21
|
+
select = ['nip.mac', 'nip.ip', 'n.switch', 'n.port', 'nip.time_first', 'nip.time_last']
|
22
|
+
|
23
|
+
# First determine if this host is a MAC address or an IP
|
24
|
+
where = []
|
25
|
+
if is_mac_address(host):
|
26
|
+
where.append(f"nip.mac ='{host}'")
|
27
|
+
elif is_ip_address(host, version=4):
|
28
|
+
where.append(f"nip.ip ='{host}'")
|
29
|
+
else:
|
30
|
+
raise ValueError(f"{host} is not a valid IP or mac address")
|
31
|
+
|
32
|
+
# filter for specific start/end times if specified
|
33
|
+
if start_time and end_time:
|
34
|
+
where.append(f"n.time_last between timestamp '{start_time}' and timestamp '{end_time}'")
|
35
|
+
elif start_time:
|
36
|
+
where.append(f"n.time_last > timestamp '{start_time}'")
|
37
|
+
|
38
|
+
# filter for active if defined
|
39
|
+
if active_only:
|
40
|
+
where.append("nip.active = true")
|
41
|
+
|
42
|
+
# order by last seen
|
43
|
+
sql = self._build_select(select, table, joins=joins, where=where, order_by="time_last", limit=limit)
|
44
|
+
result = self._execute(sql)
|
45
|
+
|
46
|
+
return result
|
47
|
+
|
48
|
+
def arp_count(self, prefix=None, start_time=None, end_time=None, active_only=False):
|
49
|
+
'''
|
50
|
+
Queries the host data in netdisco based on prefix and start/end time.
|
51
|
+
First any prefix greater than or equal to the inputted prefix is searched for
|
52
|
+
(in the 'device_ip' table).
|
53
|
+
|
54
|
+
Then the host table is queried.
|
55
|
+
'''
|
56
|
+
|
57
|
+
# We're counting all the host IPs by subnet
|
58
|
+
select = ['count(distinct nip.ip)', 'dip.subnet']
|
59
|
+
table = 'node_ip nip'
|
60
|
+
|
61
|
+
# postgres allows us to do a join based on an IPs (type inet) membership
|
62
|
+
# of a subnet (type cidr)
|
63
|
+
joins = ['join device_ip dip on nip.ip <<= dip.subnet']
|
64
|
+
|
65
|
+
# grouping by subnet is what gives us per-subnet host counts
|
66
|
+
group_by = 'dip.subnet'
|
67
|
+
|
68
|
+
# append all cli filtering options
|
69
|
+
where = [f"dip.subnet <<= inet '{prefix}'"]
|
70
|
+
if start_time and end_time:
|
71
|
+
where.append(f"nip.time_last between timestamp '{start_time}' and timestamp '{end_time}'")
|
72
|
+
elif start_time:
|
73
|
+
where.append(f"nip.time_last > timestamp '{start_time}'")
|
74
|
+
|
75
|
+
if active_only:
|
76
|
+
where.append("nip.active = true")
|
77
|
+
|
78
|
+
sql = self._build_select(select, table, joins=joins, where=where, group_by=group_by)
|
79
|
+
return self._execute(sql)
|
80
|
+
|
81
|
+
def neighbors(self, device=None):
|
82
|
+
'''
|
83
|
+
Queries the device_ip table in netdisco to get neighbors of a device.
|
84
|
+
If no device is specified, all neighbors are pulled.
|
85
|
+
The device input can be an IPv4 address or an FQDN.
|
86
|
+
'''
|
87
|
+
|
88
|
+
select = [ 'dp.ip as local_ip',
|
89
|
+
'ld.dns as local_dns',
|
90
|
+
'dp.port as local_port',
|
91
|
+
'dp.remote_ip',
|
92
|
+
'rd.dns as remote_dns',
|
93
|
+
'dp.remote_port']
|
94
|
+
table = 'device d'
|
95
|
+
joins = ['join device_port dp on d.ip = dp.ip',
|
96
|
+
'join device ld on dp.ip = ld.ip',
|
97
|
+
'join device rd on dp.remote_ip = rd.ip']
|
98
|
+
|
99
|
+
where = ['dp.remote_ip is not null']
|
100
|
+
if is_ip_address(device):
|
101
|
+
where.append(f"d.ip = '{device}'")
|
102
|
+
elif device:
|
103
|
+
where.append(f"d.dns = '{device}'")
|
104
|
+
|
105
|
+
sql = self._build_select(select, table, joins, where)
|
106
|
+
return self._execute(sql)
|
107
|
+
|
108
|
+
def get_devices(self, match_subnets=None, exclude_subnets=None):
|
109
|
+
'''
|
110
|
+
Queries netdisco for a list of devices.
|
111
|
+
Optionally, limit it by a list of prefixes
|
112
|
+
'''
|
113
|
+
|
114
|
+
select = [ 'ip', 'dns', 'serial', 'model', 'vendor', 'os' ]
|
115
|
+
table = 'device'
|
116
|
+
|
117
|
+
where = []
|
118
|
+
if match_subnets:
|
119
|
+
where.append(" or\n".join([f"ip << inet '{s}'" for s in match_subnets]))
|
120
|
+
|
121
|
+
if exclude_subnets:
|
122
|
+
where.append("not\n" + " or\n".join([f"ip << inet '{s}'" for s in exclude_subnets]))
|
123
|
+
|
124
|
+
sql = self._build_select(select, table, where=where)
|
125
|
+
return self._execute(sql)
|
126
|
+
|
127
|
+
def get_device(self, name_or_ip):
|
128
|
+
'''
|
129
|
+
Pulls the device name, ip, model, and description (where version info often is)
|
130
|
+
from the netinfo device table
|
131
|
+
'''
|
132
|
+
select = [
|
133
|
+
"replace(lower(name), '.umnet.umich.edu', '') as name",
|
134
|
+
"device.ip as ip",
|
135
|
+
"model",
|
136
|
+
"description",
|
137
|
+
]
|
138
|
+
table = "device"
|
139
|
+
|
140
|
+
if is_ip_address(name_or_ip):
|
141
|
+
joins = ["join device_ip on device.ip=device_ip.alias"]
|
142
|
+
where = [f"device_ip.alias='{name_or_ip}'"]
|
143
|
+
else:
|
144
|
+
joins = []
|
145
|
+
where = [f"lower(name) like '{name_or_ip.lower()}%'"]
|
146
|
+
|
147
|
+
sql = self._build_select(select, table, where=where, joins=joins, limit=1)
|
148
|
+
result = self._execute(sql)
|
149
|
+
|
150
|
+
if result:
|
151
|
+
return result[0]
|
152
|
+
else:
|
153
|
+
return None
|
154
|
+
|
155
|
+
def get_port_and_host_data(self, name_or_ip):
|
156
|
+
'''
|
157
|
+
Pulls a list of all ports on the switch and all hosts seen on those ports.
|
158
|
+
You will see multiple rows for an interface where multiple hosts have been seen.
|
159
|
+
|
160
|
+
Returns a list of ports along with:
|
161
|
+
- port name
|
162
|
+
- port description
|
163
|
+
- admin status
|
164
|
+
- oper status
|
165
|
+
- description
|
166
|
+
- host mac
|
167
|
+
- host IP
|
168
|
+
- vlan the host was seen on
|
169
|
+
- date last mac was seen
|
170
|
+
|
171
|
+
'''
|
172
|
+
|
173
|
+
# getting device by name or IP
|
174
|
+
if is_ip_address(name_or_ip):
|
175
|
+
device_ip = name_or_ip
|
176
|
+
else:
|
177
|
+
nd_device = self.get_device(name_or_ip)
|
178
|
+
if not(nd_device):
|
179
|
+
raise LookupError(f'Could not find {nd_device} in Netdisco')
|
180
|
+
device_ip = nd_device['ip']
|
181
|
+
|
182
|
+
# The port description is in different fields depending on the platform, so we need to do
|
183
|
+
# some if-else stuff that's not supported in our basic sql builer in the base class.
|
184
|
+
# That's why we're just doing a long sting here
|
185
|
+
sql = f'''
|
186
|
+
select
|
187
|
+
dp.port,
|
188
|
+
case
|
189
|
+
when dp.name=dp.port then dp.descr
|
190
|
+
else dp.name
|
191
|
+
end
|
192
|
+
as description,
|
193
|
+
REPLACE(dp.up,'lowerLayerDown','down') as oper_status,
|
194
|
+
dp.up_admin as admin_status,
|
195
|
+
dp.speed,
|
196
|
+
node.vlan,
|
197
|
+
dp.remote_ip,
|
198
|
+
node.mac,
|
199
|
+
node.time_last,
|
200
|
+
node_ip.ip
|
201
|
+
from device_port dp
|
202
|
+
left outer join node on dp.port = node.port and dp.ip = node.switch
|
203
|
+
left outer join node_ip on node.mac = node_ip.mac
|
204
|
+
where dp.ip='{device_ip}'
|
205
|
+
'''
|
206
|
+
|
207
|
+
result = self._execute(sql)
|
208
|
+
|
209
|
+
return result
|
210
|
+
|
211
|
+
def get_poe_data(self, name_or_ip):
|
212
|
+
"""
|
213
|
+
Pulls PoE admin, status, class, and power columns from device_port_pwer
|
214
|
+
"""
|
215
|
+
|
216
|
+
# getting device by name or IP
|
217
|
+
if is_ip_address(name_or_ip):
|
218
|
+
device_ip = name_or_ip
|
219
|
+
else:
|
220
|
+
nd_device = self.get_device(name_or_ip)
|
221
|
+
if not(nd_device):
|
222
|
+
raise LookupError(f'Could not find {nd_device} in Netdisco')
|
223
|
+
device_ip = nd_device['ip']
|
224
|
+
|
225
|
+
select = ["port", "admin", "status", "class", "power"]
|
226
|
+
table = "device_port_power"
|
227
|
+
where = [f"ip='{device_ip}'"]
|
228
|
+
|
229
|
+
|
230
|
+
sql = self._build_select(select, table, where=where)
|
231
|
+
result = self._execute(sql)
|
232
|
+
|
233
|
+
return result
|
@@ -0,0 +1,157 @@
|
|
1
|
+
from typing import Union, Optional
|
2
|
+
|
3
|
+
from .base import UMnetdbBase
|
4
|
+
from .utils import is_ip_address
|
5
|
+
|
6
|
+
class UMnetequip(UMnetdbBase):
|
7
|
+
'''
|
8
|
+
This class wraps helpful equip db queries in python.
|
9
|
+
'''
|
10
|
+
URL = "oracle+oracledb://{EQUIP_DB_USERNAME}:{EQUIP_DB_PASSWORD}@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=equipdb-prod.umnet.umich.edu)(PORT=1521))(CONNECT_DATA=(SID=KANNADA)))"
|
11
|
+
|
12
|
+
def get_devices_by_category(self, category, active_only=False):
|
13
|
+
'''
|
14
|
+
Queries equip db for devices by category. You can also
|
15
|
+
specify if you only want active devices.
|
16
|
+
'''
|
17
|
+
|
18
|
+
select = [ 'eq.monitored_device',
|
19
|
+
'eq.rancid',
|
20
|
+
'eq.off_line',
|
21
|
+
'eq.dns_name',
|
22
|
+
'eq.ip_address',
|
23
|
+
]
|
24
|
+
table = 'ARADMIN1.UMNET_EQUIPMENT eq'
|
25
|
+
|
26
|
+
where = [f"eq.category = '{category}'"]
|
27
|
+
|
28
|
+
# Equipshim status numbers (see ARADMIN1.UMNET_EQUIPMENT_STATUS)
|
29
|
+
# 1: RESERVE, 2:ACTIVE, 3:RETIRED
|
30
|
+
if active_only:
|
31
|
+
where.append("eq.status = 2")
|
32
|
+
|
33
|
+
sql = self._build_select(select, table, where=where, distinct=True)
|
34
|
+
return self._execute(sql)
|
35
|
+
|
36
|
+
|
37
|
+
def get_device_type(self, ip):
|
38
|
+
'''
|
39
|
+
Queries equip db for a device by ip, returns the 'type' of the device.
|
40
|
+
By type we mean the UMNET_EQUIPMENT_TYPE: ACCESS LAYER, DISTRIBUTION LAYER, UMD, etc
|
41
|
+
'''
|
42
|
+
select = [ 'types as type' ]
|
43
|
+
table = 'ARADMIN1.UMNET_EQUIPMENT'
|
44
|
+
where = [f"ip_address='{ip}'"]
|
45
|
+
|
46
|
+
sql = self._build_select(select, table, where=where)
|
47
|
+
return self._execute(sql)
|
48
|
+
|
49
|
+
def get_devices_by_building_no(self,
|
50
|
+
building_no:Union[int, list],
|
51
|
+
|
52
|
+
|
53
|
+
active_only:bool=False,
|
54
|
+
types:Optional[list]=None,
|
55
|
+
location_info:bool=False,
|
56
|
+
):
|
57
|
+
"""
|
58
|
+
Queries equipdb for devices by building no. You can provide a single
|
59
|
+
building number, or a list of numbers. You can specify if you want 'active only' devices
|
60
|
+
(based on the 'status' field, defalut false) or you can limit to a certain device type (default all).
|
61
|
+
You can also get the location info for each device via 'location_info' (default false)
|
62
|
+
"""
|
63
|
+
select = [ 'dns_name',
|
64
|
+
'ip_address',
|
65
|
+
'model_no_ as model',
|
66
|
+
'types as device_type',
|
67
|
+
'rancid',
|
68
|
+
'off_line',
|
69
|
+
]
|
70
|
+
if location_info:
|
71
|
+
select.extend([
|
72
|
+
'bldg as building_name',
|
73
|
+
'address as building_address',
|
74
|
+
'room as room_no',
|
75
|
+
'floor as floor',
|
76
|
+
'bldg_code__ as building_no',
|
77
|
+
])
|
78
|
+
|
79
|
+
table = 'ARADMIN1.UMNET_EQUIPMENT eq'
|
80
|
+
|
81
|
+
where = []
|
82
|
+
if isinstance(building_no, int):
|
83
|
+
where.append(f"eq.bldg_code__ = {building_no}")
|
84
|
+
elif isinstance(building_no, list):
|
85
|
+
bldgs_str = ",".join([str(bldg) for bldg in building_no])
|
86
|
+
where.append(f"eq.bldg_code__ in ({bldgs_str})")
|
87
|
+
else:
|
88
|
+
raise ValueError(f"{building_no} must be int or list!")
|
89
|
+
|
90
|
+
# Equipshim status numbers (see ARADMIN1.UMNET_EQUIPMENT_STATUS)
|
91
|
+
# 1: RESERVE, 2:ACTIVE, 3:RETIRED
|
92
|
+
if active_only:
|
93
|
+
where.append("eq.status = 2")
|
94
|
+
|
95
|
+
if types:
|
96
|
+
types_str = ",".join([f"'{t}'" for t in types])
|
97
|
+
where.append(f"eq.types in ({types_str})")
|
98
|
+
|
99
|
+
sql = self._build_select(select, table, where=where)
|
100
|
+
return self._execute(sql)
|
101
|
+
|
102
|
+
def get_device(self, name_or_ip:str, location_info:bool=False):
|
103
|
+
|
104
|
+
if is_ip_address(name_or_ip):
|
105
|
+
where = [f"ip_address='{name_or_ip}'"]
|
106
|
+
else:
|
107
|
+
name_or_ip = name_or_ip.replace(".umnet.umich.edu","")
|
108
|
+
where = [f"dns_name='{name_or_ip}'"]
|
109
|
+
|
110
|
+
select = [ 'dns_name',
|
111
|
+
'ip_address',
|
112
|
+
'model_no_ as model',
|
113
|
+
'types as device_type',
|
114
|
+
'rancid',
|
115
|
+
'off_line',
|
116
|
+
]
|
117
|
+
if location_info:
|
118
|
+
select.extend([
|
119
|
+
'bldg as building_name',
|
120
|
+
'address as building_address',
|
121
|
+
'room as room_no',
|
122
|
+
'floor as floor',
|
123
|
+
'bldg_code__ as building_no',
|
124
|
+
])
|
125
|
+
table = 'ARADMIN1.UMNET_EQUIPMENT eq'
|
126
|
+
|
127
|
+
sql = self._build_select(select, table, where=where)
|
128
|
+
return self._execute(sql)
|
129
|
+
|
130
|
+
def get_all_devices(self):
|
131
|
+
select = [ "bldg_code__ as bldg_code",
|
132
|
+
"bldg",
|
133
|
+
"room",
|
134
|
+
"ip_address",
|
135
|
+
"dns_name",
|
136
|
+
"category",
|
137
|
+
"types",
|
138
|
+
"manufacturer",
|
139
|
+
"serial_number",
|
140
|
+
"billing_code_ as billing_code",
|
141
|
+
"warehouse_item__ as warehouse_item",
|
142
|
+
"st.descr as status",
|
143
|
+
"sla_network",
|
144
|
+
"address",
|
145
|
+
"floor",
|
146
|
+
"model_no_ as model_no",
|
147
|
+
"customer_name",
|
148
|
+
"mat_l_item_description",
|
149
|
+
"rancid",
|
150
|
+
"off_line",
|
151
|
+
]
|
152
|
+
table = 'ARADMIN1.UMNET_EQUIPMENT eq'
|
153
|
+
joins = ["join ARADMIN1.UMNET_EQUIPMENT_STATUS st on st.idnum=eq.status"]
|
154
|
+
where = ["eq.status != 3"]
|
155
|
+
|
156
|
+
sql = self._build_select(select, table,where=where, joins=joins)
|
157
|
+
return self._execute(sql)
|
@@ -0,0 +1,489 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
import ipaddress
|
4
|
+
import re
|
5
|
+
from .base import UMnetdbBase
|
6
|
+
from .utils import is_ip_address, is_ip_network, is_mac_address
|
7
|
+
|
8
|
+
class UMnetinfo(UMnetdbBase):
|
9
|
+
"""
|
10
|
+
Wraps helpful netinfo db queries into python
|
11
|
+
"""
|
12
|
+
URL = "oracle+oracledb://{NETINFO_USERNAME}:{NETINFO_PASSWORD}@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=kannada.web.itd.umich.edu)(PORT=1521))(CONNECT_DATA=(SID=KANNADA)))"
|
13
|
+
|
14
|
+
def get_network_by_name(self, netname, active_only=False):
|
15
|
+
"""
|
16
|
+
Looks up a network by netname.
|
17
|
+
"""
|
18
|
+
select = [
|
19
|
+
"p.itemname as netname",
|
20
|
+
"i.itemname as ipv4_subnet",
|
21
|
+
"six_i.itemname as ipv6_subnet",
|
22
|
+
"NVL(n.vlanid,n.local_vlanid) as vlan_id",
|
23
|
+
"s.statusdes as status",
|
24
|
+
]
|
25
|
+
table = "UMNET_ONLINE.ITEM p"
|
26
|
+
joins = [
|
27
|
+
"join UMNET_ONLINE.NETWORK n on p.itemidnum = n.itemidnum",
|
28
|
+
"join UMNET_ONLINE.STATUS_CODE s on p.statuscd = s.statuscd",
|
29
|
+
"left outer join UMNET_ONLINE.ITEM i on i.parentitemidnum = p.itemidnum",
|
30
|
+
"left outer join UMNET_ONLINE.IP_SUBNET ip on i.itemidnum = ip.itemidnum",
|
31
|
+
"left outer join UMNET_ONLINE.RELATED_ITEM r on p.itemidnum = r.relitemidnum",
|
32
|
+
"left outer join UMNET_ONLINE.IP6NET six on r.itemidnum = six.itemidnum",
|
33
|
+
"left outer join UMNET_ONLINE.ITEM six_i on six.itemidnum = six_i.itemidnum",
|
34
|
+
]
|
35
|
+
where = [
|
36
|
+
f"p.itemname = '{netname}'",
|
37
|
+
]
|
38
|
+
|
39
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
40
|
+
results = self._execute(sql)
|
41
|
+
|
42
|
+
# there are also IPv4 subnets that are "pending removal" or "pending activation"
|
43
|
+
# tied to the network in the RELATED_ITEM table, we need to find those too.
|
44
|
+
# doing it as a separate query to maintain existing results row structure
|
45
|
+
select = [
|
46
|
+
"p.itemname as netname",
|
47
|
+
"r_i.itemname as ipv4_subnet",
|
48
|
+
"'' as ipv6_subnet",
|
49
|
+
"NVL(n.vlanid,n.local_vlanid) as vlan_id",
|
50
|
+
"r.itemreltypcd as status",
|
51
|
+
]
|
52
|
+
joins = [
|
53
|
+
"join UMNET_ONLINE.NETWORK n on p.itemidnum = n.itemidnum",
|
54
|
+
"left outer join UMNET_ONLINE.ITEM i on i.parentitemidnum = p.itemidnum",
|
55
|
+
"left outer join UMNET_ONLINE.RELATED_ITEM r on p.itemidnum = r.relitemidnum",
|
56
|
+
"left outer join UMNET_ONLINE.ITEM r_i on r.itemidnum = r_i.itemidnum",
|
57
|
+
]
|
58
|
+
where = [
|
59
|
+
f"p.itemname = '{netname}'",
|
60
|
+
"r_i.itemcatcd = 'IP'",
|
61
|
+
]
|
62
|
+
# if we're doing active only, we only want "pending removal"
|
63
|
+
if active_only:
|
64
|
+
where.append("r.itemreltypcd = 'IP-PRNET'")
|
65
|
+
|
66
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
67
|
+
more_results = self._execute(sql)
|
68
|
+
|
69
|
+
if more_results:
|
70
|
+
results.extend(more_results)
|
71
|
+
|
72
|
+
return results
|
73
|
+
|
74
|
+
def get_network_by_ip(self, ip, active_only=False):
|
75
|
+
"""
|
76
|
+
Looks up a network based on an IPv4 or IPv6 address. Returns the netname,
|
77
|
+
vlan id, as well as *all active subnets* (IPv4 and IPv6) tied to the netname.
|
78
|
+
"""
|
79
|
+
|
80
|
+
ip = ipaddress.ip_address(ip)
|
81
|
+
|
82
|
+
# to make our lives simpler we're breaking this up into two steps.
|
83
|
+
# first let's find the network entry in the 'item' table
|
84
|
+
if ip.version == 4:
|
85
|
+
select = ["NVL(p.itemidnum, r.relitemidnum) as id"]
|
86
|
+
table = "UMNET_ONLINE.IP_SUBNET ip"
|
87
|
+
joins = [
|
88
|
+
"join UMNET_ONLINE.ITEM i on ip.itemidnum = i.itemidnum",
|
89
|
+
"left outer join UMNET_ONLINE.RELATED_ITEM r on r.itemidnum = ip.itemidnum",
|
90
|
+
"left outer join UMNET_ONLINE.ITEM p on i.parentitemidnum = p.itemidnum",
|
91
|
+
]
|
92
|
+
where = [
|
93
|
+
f"{int(ip)} >= ip.ADDRESS32BIT",
|
94
|
+
f"{int(ip)} <= ip.ENDADDRESS32BIT",
|
95
|
+
"NVL(p.itemidnum, r.relitemidnum) is not null",
|
96
|
+
]
|
97
|
+
|
98
|
+
# IPv6 table is related to the 'item' table via the RELATED_ITEM table
|
99
|
+
elif ip.version == 6:
|
100
|
+
select = ["p.itemidnum as id"]
|
101
|
+
table = "UMNET_ONLINE.IP6NET ip"
|
102
|
+
joins = [
|
103
|
+
"join UMNET_ONLINE.RELATED_iTEM r on r.itemidnum = ip.itemidnum",
|
104
|
+
"join UMNET_ONLINE.ITEM p on p.itemidnum = r.relitemidnum",
|
105
|
+
]
|
106
|
+
# and the start/end addresses are stored as hex strings
|
107
|
+
addr_str = ip.exploded.replace(":", "")
|
108
|
+
where = [
|
109
|
+
f"'{addr_str}' >= ip.ADDRESS128BIT",
|
110
|
+
f"'{addr_str}' <= ip.ENDADDRESS128BIT",
|
111
|
+
]
|
112
|
+
|
113
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
114
|
+
network = self._execute(sql)
|
115
|
+
|
116
|
+
if not (network):
|
117
|
+
return False
|
118
|
+
net_id = network[0]["id"]
|
119
|
+
|
120
|
+
# Now let's use the network itemidnum to find all associated subnets
|
121
|
+
# with that network, as well as the netname and VLAN ID
|
122
|
+
select = [
|
123
|
+
"p.itemname as netname",
|
124
|
+
"i.itemname as ipv4_subnet",
|
125
|
+
"six_i.itemname as ipv6_subnet",
|
126
|
+
"NVL(n.vlanid,n.local_vlanid) AS vlan_id",
|
127
|
+
"i.itemcatcd as ITEMCAT",
|
128
|
+
"p.itemcatcd as PCAT",
|
129
|
+
"r.itemreltypcd as RELCAT",
|
130
|
+
]
|
131
|
+
table = "UMNET_ONLINE.ITEM p"
|
132
|
+
joins = [
|
133
|
+
"join UMNET_ONLINE.NETWORK n on p.itemidnum = n.itemidnum",
|
134
|
+
"join UMNET_ONLINE.ITEM i on i.parentitemidnum = p.itemidnum",
|
135
|
+
"left outer join UMNET_ONLINE.RELATED_ITEM r on p.itemidnum = r.relitemidnum",
|
136
|
+
"left outer join UMNET_ONLINE.IP6NET six on r.itemidnum = six.itemidnum",
|
137
|
+
"left outer join UMNET_ONLINE.ITEM six_i on six.itemidnum = six_i.itemidnum",
|
138
|
+
]
|
139
|
+
where = [
|
140
|
+
f"p.itemidnum = {net_id}",
|
141
|
+
]
|
142
|
+
|
143
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
144
|
+
results = self._execute(sql)
|
145
|
+
|
146
|
+
# there are also IPv4 subnets that are "pending removal" or "pending activation"
|
147
|
+
# tied to the network in the RELATED_ITEM table, we need to find those too.
|
148
|
+
# doing it as a separate query to maintain existing results row structure
|
149
|
+
select = [
|
150
|
+
"p.itemname as netname",
|
151
|
+
"r_i.itemname as ipv4_subnet",
|
152
|
+
"'' as ipv6_subnet",
|
153
|
+
"NVL(n.vlanid,n.local_vlanid) AS vlan_id",
|
154
|
+
"r.itemreltypcd as RELTYPCD",
|
155
|
+
]
|
156
|
+
joins = [
|
157
|
+
"join UMNET_ONLINE.NETWORK n on p.itemidnum = n.itemidnum",
|
158
|
+
"left outer join UMNET_ONLINE.ITEM i on i.parentitemidnum = p.itemidnum",
|
159
|
+
"left outer join UMNET_ONLINE.RELATED_ITEM r on p.itemidnum = r.relitemidnum",
|
160
|
+
"left outer join UMNET_ONLINE.ITEM r_i on r.itemidnum = r_i.itemidnum",
|
161
|
+
]
|
162
|
+
where = [
|
163
|
+
f"p.itemidnum = {net_id}",
|
164
|
+
"r_i.itemcatcd = 'IP'",
|
165
|
+
]
|
166
|
+
# if we're doing active only, we only want "pending removal"
|
167
|
+
if active_only:
|
168
|
+
where.append("r.itemreltypcd = 'IP-PRNET'")
|
169
|
+
|
170
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
171
|
+
more_results = self._execute(sql)
|
172
|
+
|
173
|
+
if more_results:
|
174
|
+
results.extend(more_results)
|
175
|
+
|
176
|
+
return results
|
177
|
+
|
178
|
+
def get_vrfs(self, vrf_name=None, rd=None):
|
179
|
+
"""
|
180
|
+
Pulls data from the vrf table on netinfo. If you supply a name and/or rd, it filters only
|
181
|
+
for that vrf. Otherwise it returns all VRFs
|
182
|
+
"""
|
183
|
+
|
184
|
+
select = [
|
185
|
+
"shortname",
|
186
|
+
"route_distinguisher",
|
187
|
+
"default_vrf",
|
188
|
+
"inside_vrf",
|
189
|
+
]
|
190
|
+
table = "UMNET_ONLINE.VRF"
|
191
|
+
|
192
|
+
where = []
|
193
|
+
if vrf_name:
|
194
|
+
where.append(f"shortname = '{vrf_name}'")
|
195
|
+
if rd:
|
196
|
+
where.append(f"route_distinguisher = '{rd}'")
|
197
|
+
|
198
|
+
sql = self._build_select(select, table, where=where)
|
199
|
+
results = self._execute(sql)
|
200
|
+
|
201
|
+
return results
|
202
|
+
|
203
|
+
def get_special_acl(self, netname: str):
|
204
|
+
"""
|
205
|
+
Looks for a special ACL assignment by netname
|
206
|
+
"""
|
207
|
+
select = ["acl.itemname"]
|
208
|
+
table = "UMNET_ONLINE.ITEM net"
|
209
|
+
joins = [
|
210
|
+
"join UMNET_ONLINE.FILTER_NETWORK fn on fn.net_itemidnum = net.itemidnum",
|
211
|
+
"join UMNET_ONLINE.ITEM acl on fn.filter_itemidnum = acl.itemidnum",
|
212
|
+
]
|
213
|
+
where = [f"net.itemname='{netname}'"]
|
214
|
+
|
215
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
216
|
+
results = self._execute(sql)
|
217
|
+
|
218
|
+
return results
|
219
|
+
|
220
|
+
def get_asns(self, name_filter: str = ""):
|
221
|
+
"""
|
222
|
+
Pulls all the ASNs from the AUTONOMOUS_SYSTEM
|
223
|
+
table, optionally filtering by asname
|
224
|
+
"""
|
225
|
+
select = ["ASNAME", "ASN"]
|
226
|
+
table = "UMNET_ONLINE.AUTONOMOUS_SYSTEM"
|
227
|
+
|
228
|
+
where = []
|
229
|
+
if name_filter:
|
230
|
+
where = [f"ASNAME like '%{name_filter}%'"]
|
231
|
+
|
232
|
+
sql = self._build_select(select, table, where=where)
|
233
|
+
results = self._execute(sql)
|
234
|
+
|
235
|
+
return results
|
236
|
+
|
237
|
+
def get_dlzone_buildings(self, zone: str):
|
238
|
+
"""
|
239
|
+
given a dlzone shortname, returns a list of building numbers tied
|
240
|
+
to that zone.
|
241
|
+
"""
|
242
|
+
|
243
|
+
select = ["BUILDINGNUM as building_no"]
|
244
|
+
table = "UMNET_ONLINE.AUTONOMOUS_SYSTEM asn"
|
245
|
+
joins = [
|
246
|
+
"join UMNET_ONLINE.BUILDING_AS b_asn on b_asn.ASID = asn.ASID",
|
247
|
+
]
|
248
|
+
where = [f"asn.ASNAME = '{zone}'"]
|
249
|
+
|
250
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
251
|
+
results = self._execute(sql)
|
252
|
+
|
253
|
+
return results
|
254
|
+
|
255
|
+
def get_dlzone_by_building(self, bldg_num: int):
|
256
|
+
"""
|
257
|
+
given a seven-digit building number, return the name of the
|
258
|
+
distribution-zone to which it is assigned
|
259
|
+
"""
|
260
|
+
|
261
|
+
select = ["ASNAME"]
|
262
|
+
table = "UMNET_ONLINE.AUTONOMOUS_SYSTEM asn"
|
263
|
+
joins = [
|
264
|
+
"join UMNET_ONLINE.BUILDING_AS b_asn on b_asn.ASID = asn.ASID",
|
265
|
+
]
|
266
|
+
where = [f"b_asn.BUILDINGNUM = '{bldg_num}'"]
|
267
|
+
|
268
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
269
|
+
results = self._execute(sql)
|
270
|
+
|
271
|
+
return results
|
272
|
+
|
273
|
+
def get_user_groups(self, username: str):
|
274
|
+
"""
|
275
|
+
Looks up a user in netinfo and returns the groups they're in
|
276
|
+
"""
|
277
|
+
|
278
|
+
select = ["e_grp.name"]
|
279
|
+
table = "UMNET_ONLINE.PERSON p"
|
280
|
+
joins = [
|
281
|
+
"join UMNET_ONLINE.PERSON_GROUP p_grp on p_grp.PERSON_ENTITYIDNUM = p.ENTITYIDNUM",
|
282
|
+
"join UMNET_ONLINE.ENTITY_GROUP e_grp on e_grp.ENTITYIDNUM = p_grp.GROUP_ENTITYIDNUM",
|
283
|
+
]
|
284
|
+
where = [f"UNIQNAME='{username}'"]
|
285
|
+
|
286
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
287
|
+
results = self._execute(sql)
|
288
|
+
|
289
|
+
return results
|
290
|
+
|
291
|
+
def get_network_admins(
|
292
|
+
self, netname: str, entity_types: list = [], rel_types: list = []
|
293
|
+
):
|
294
|
+
"""
|
295
|
+
Looks up the users and user groups that are tied to this network.
|
296
|
+
You can fiter by entity types (see UMNET_ONLINE.ENTITY_TYPE_CODE), these are:
|
297
|
+
group, organization, person
|
298
|
+
You can also filter by relationship type (UMNET_ONLINE.ENTITEM_RELATIONSHIP_TYPE_CODE):
|
299
|
+
Owner, Worker, Business, Admin, Security
|
300
|
+
"""
|
301
|
+
|
302
|
+
select = [
|
303
|
+
"NVL(NVL(e_g.name, p.uniqname), o.NAME) as name",
|
304
|
+
"e_t_c.ENTITYTYPDES as entity_type",
|
305
|
+
"i_a.ENTITYITEMRELTYPCD as rel_type",
|
306
|
+
]
|
307
|
+
|
308
|
+
table = "UMNET_ONLINE.ITEM i"
|
309
|
+
joins = [
|
310
|
+
"join UMNET_ONLINE.ITEM_ADMIN i_a on i_a.ITEMIDNUM=i.ITEMIDNUM",
|
311
|
+
"join UMNET_ONLINE.ENTITY e on i_a.ENTITYIDNUM=e.ENTITYIDNUM",
|
312
|
+
"join UMNET_ONLINE.ENTITY_TYPE_CODE e_t_c on e_t_c.ENTITYTYPCD=e.ENTITYTYPCD",
|
313
|
+
"left outer join UMNET_ONLINE.ENTITY_GROUP e_g on e_g.ENTITYIDNUM=e.ENTITYIDNUM",
|
314
|
+
"left outer join UMNET_ONLINE.PERSON p on p.ENTITYIDNUM=e.ENTITYIDNUM",
|
315
|
+
"left outer join UMNET_ONLINE.ORGANIZATION o on o.ENTITYIDNUM=e.ENTITYIDNUM",
|
316
|
+
]
|
317
|
+
|
318
|
+
where = [f"i.ITEMNAME='{netname}'"]
|
319
|
+
|
320
|
+
if entity_types:
|
321
|
+
e_where = " or ".join([f"e_t_c.ENTITYTYPDES='{e}'" for e in entity_types])
|
322
|
+
where.append(f"({e_where})")
|
323
|
+
if rel_types:
|
324
|
+
rel_where = " or ".join(
|
325
|
+
[f"i_a.ENTITYITEMRELTYPCD='{rel}'" for rel in rel_types]
|
326
|
+
)
|
327
|
+
where.append(f"({rel_where})")
|
328
|
+
|
329
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
330
|
+
results = self._execute(sql)
|
331
|
+
|
332
|
+
return results
|
333
|
+
|
334
|
+
def get_device_admins(
|
335
|
+
self, name_or_ip: str, entity_types: list = [], rel_types: list = []
|
336
|
+
):
|
337
|
+
"""
|
338
|
+
Looks up the users and user groups that are tied to this device.
|
339
|
+
You can fiter by entity types (see UMNET_ONLINE.ENTITY_TYPE_CODE), these are:
|
340
|
+
group, organization, person
|
341
|
+
You can also filter by relationship type (UMNET_ONLINE.ENTITEM_RELATIONSHIP_TYPE_CODE):
|
342
|
+
Owner, Worker, Business, Admin, Security
|
343
|
+
"""
|
344
|
+
|
345
|
+
select = [
|
346
|
+
"NVL(NVL(e_g.name, p.uniqname), o.NAME) as name",
|
347
|
+
"e_t_c.ENTITYTYPDES as entity_type",
|
348
|
+
"i_a.ENTITYITEMRELTYPCD as rel_type",
|
349
|
+
]
|
350
|
+
|
351
|
+
table = "UMNET_ONLINE.DEVICE d"
|
352
|
+
joins = [
|
353
|
+
"join UMNET_ONLINE.ITEM i on i.itemidnum=d.ITEMIDNUM",
|
354
|
+
"join UMNET_ONLINE.ITEM_ADMIN i_a on i_a.ITEMIDNUM=i.ITEMIDNUM",
|
355
|
+
"join UMNET_ONLINE.ENTITY e on i_a.ENTITYIDNUM=e.ENTITYIDNUM",
|
356
|
+
"join UMNET_ONLINE.ENTITY_TYPE_CODE e_t_c on e_t_c.ENTITYTYPCD=e.ENTITYTYPCD",
|
357
|
+
"left outer join UMNET_ONLINE.ENTITY_GROUP e_g on e_g.ENTITYIDNUM=e.ENTITYIDNUM",
|
358
|
+
"left outer join UMNET_ONLINE.PERSON p on p.ENTITYIDNUM=e.ENTITYIDNUM",
|
359
|
+
"left outer join UMNET_ONLINE.ORGANIZATION o on o.ENTITYIDNUM=e.ENTITYIDNUM",
|
360
|
+
]
|
361
|
+
|
362
|
+
# netinfo IPv4 addresses are always stored as integers
|
363
|
+
if is_ip_address(name_or_ip):
|
364
|
+
where = [f"d.ADDRESS32={int(ipaddress.ip_address(name_or_ip))}"]
|
365
|
+
|
366
|
+
# dns names in the device table are fqdn and end in a dot (eg 'dl-arbl-1.umnet.umich.edu.')
|
367
|
+
else:
|
368
|
+
|
369
|
+
name_or_ip = name_or_ip.lower()
|
370
|
+
if "." not in name_or_ip:
|
371
|
+
name_or_ip += ".umnet.umich.edu"
|
372
|
+
if not (name_or_ip.endswith(".")):
|
373
|
+
name_or_ip += "."
|
374
|
+
|
375
|
+
where = [f"d.DNS_NAME='{name_or_ip}'"]
|
376
|
+
|
377
|
+
if entity_types:
|
378
|
+
e_where = " or ".join([f"e_t_c.ENTITYTYPDES='{e}'" for e in entity_types])
|
379
|
+
where.append(f"({e_where})")
|
380
|
+
if rel_types:
|
381
|
+
rel_where = " or ".join(
|
382
|
+
[f"i_a.ENTITYITEMRELTYPCD='{rel}'" for rel in rel_types]
|
383
|
+
)
|
384
|
+
where.append(f"({rel_where})")
|
385
|
+
|
386
|
+
sql = self._build_select(select, table, joins=joins, where=where)
|
387
|
+
results = self._execute(sql)
|
388
|
+
|
389
|
+
return results
|
390
|
+
|
391
|
+
def get_arphist(
|
392
|
+
self, query: str, device=None, interface=None, no_hist=False, no_device=False
|
393
|
+
):
|
394
|
+
"""
|
395
|
+
Given either a MAC address, IP address, or subnet, query UMNET.ARPHIST
|
396
|
+
and return the results.
|
397
|
+
|
398
|
+
If you don't care about history, set "no_hist"
|
399
|
+
If you don't care about which device the entries are routed on,
|
400
|
+
set "no_device"
|
401
|
+
|
402
|
+
Optionally limit the query by device and/or by interface
|
403
|
+
with the "device" and "interface" fields
|
404
|
+
"""
|
405
|
+
|
406
|
+
# for 'no_hist' 'no_device' we are just straight up pulling ip->mac
|
407
|
+
# mappings
|
408
|
+
select = ["address32bit, mac_addr"]
|
409
|
+
if not no_hist:
|
410
|
+
select.extend(["first_seen", "last_seen"])
|
411
|
+
if not no_device:
|
412
|
+
select.extend(["device_name as device", "ifdescr as interface"])
|
413
|
+
|
414
|
+
table = "UMNET.ARPHIST arp"
|
415
|
+
where = []
|
416
|
+
|
417
|
+
# UMNET.ARPHIST stores MACs as strings without separators .:-, ie
|
418
|
+
# 0010.abcd.1010 => '0010abcd1010'
|
419
|
+
if is_mac_address(query):
|
420
|
+
|
421
|
+
arphist_mac = re.sub(r"[\.\:\-]", "", query)
|
422
|
+
where.append(f"arp.mac_addr = '{arphist_mac}'")
|
423
|
+
|
424
|
+
# UMNET.ARPHIST stores IPs as integers, ie
|
425
|
+
# 10.233.0.10 => 183042058. Also - only supports IPv4
|
426
|
+
# IPv6 is stored in IP6NEIGHBOR
|
427
|
+
elif is_ip_address(query, version=4):
|
428
|
+
ip = ipaddress.ip_address(query)
|
429
|
+
where.append(f"arp.address32bit = {int(ip)}")
|
430
|
+
|
431
|
+
elif is_ip_network(query, version=4):
|
432
|
+
net = ipaddress.ip_network(query)
|
433
|
+
where.extend(
|
434
|
+
[
|
435
|
+
f"arp.address32bit >= {int(net[0])}",
|
436
|
+
f"arp.address32bit <= {int(net[-1])}",
|
437
|
+
]
|
438
|
+
)
|
439
|
+
|
440
|
+
else:
|
441
|
+
raise ValueError(
|
442
|
+
f"Unsupported input {query}, must be a MAC, a subnet, or an IP"
|
443
|
+
)
|
444
|
+
|
445
|
+
if device:
|
446
|
+
where.append(f"arp.device = '{device}'")
|
447
|
+
if interface:
|
448
|
+
where.append(f"arp.interface = '{interface}'")
|
449
|
+
|
450
|
+
sql = self._build_select(select, table, where=where, distinct=True)
|
451
|
+
results = self._execute(sql)
|
452
|
+
|
453
|
+
# normally we like to return the results unprocessed. But the IPs and
|
454
|
+
# MACs really need to get converted to make the ouptut useful.
|
455
|
+
# hopefully the type different (Sqlalchemy result object vs list[dict])
|
456
|
+
# isn't throwing anyone :-/
|
457
|
+
processed_results = []
|
458
|
+
for r in results:
|
459
|
+
|
460
|
+
processed = {
|
461
|
+
"ip": ipaddress.ip_address(r["address32bit"]),
|
462
|
+
"mac": f"{r['mac_addr'][0:4]}.{r['mac_addr'][4:8]}.{r['mac_addr'][8:12]}",
|
463
|
+
}
|
464
|
+
for col in ["device", "interface", "first_seen", "last_seen"]:
|
465
|
+
if col in r:
|
466
|
+
processed[col] = r[col]
|
467
|
+
|
468
|
+
processed_results.append(processed)
|
469
|
+
|
470
|
+
return processed_results
|
471
|
+
|
472
|
+
def get_prefix_lists(self, prefix_lists: list = None):
|
473
|
+
"""
|
474
|
+
Queries UMNET_ONLINE.PREFIX_LISTS for a list of prefix list names (as defined in the database).
|
475
|
+
If you don't provide a list of names it will return all of them
|
476
|
+
"""
|
477
|
+
|
478
|
+
select = ["name", "prefix"]
|
479
|
+
table = "UMNET_ONLINE.PREFIX_LIST"
|
480
|
+
|
481
|
+
where = []
|
482
|
+
if prefix_lists:
|
483
|
+
in_query = "(" + ",".join([f"'{p}'" for p in prefix_lists]) + ")"
|
484
|
+
where.append(f"name in {in_query}")
|
485
|
+
|
486
|
+
sql = self._build_select(select, table, where=where, order_by="1,2")
|
487
|
+
results = self._execute(sql)
|
488
|
+
|
489
|
+
return results
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import ipaddress
|
2
|
+
import re
|
3
|
+
def is_ip_address(input_str, version=None):
|
4
|
+
try:
|
5
|
+
ip = ipaddress.ip_address(input_str)
|
6
|
+
except ValueError:
|
7
|
+
return False
|
8
|
+
|
9
|
+
if version and version != ip.version:
|
10
|
+
return False
|
11
|
+
|
12
|
+
return True
|
13
|
+
|
14
|
+
def is_ip_network(input_str, version=None):
|
15
|
+
|
16
|
+
# First check that this is a valid IP or network
|
17
|
+
try:
|
18
|
+
net = ipaddress.ip_network(input_str)
|
19
|
+
except ValueError:
|
20
|
+
return False
|
21
|
+
|
22
|
+
if version and version != net.version:
|
23
|
+
return False
|
24
|
+
|
25
|
+
return True
|
26
|
+
|
27
|
+
def is_mac_address(input_str):
|
28
|
+
'''
|
29
|
+
Validates the input string as a mac address. Valid formats are
|
30
|
+
XX:XX:XX:XX:XX:XX, XX-XX-XX-XX-XX-XX, XXXX.XXXX.XXXX
|
31
|
+
where 'X' is a hexadecimal digit (upper or lowercase).
|
32
|
+
'''
|
33
|
+
mac = input_str.lower()
|
34
|
+
if re.match(r'[0-9a-f]{2}([-:])[0-9a-f]{2}(\1[0-9a-f]{2}){4}$', mac):
|
35
|
+
return True
|
36
|
+
if re.match(r'[0-9a-f]{4}\.[0-9a-f]{4}\.[0-9a-f]{4}$', mac):
|
37
|
+
return True
|
38
|
+
|
39
|
+
return False
|