umnetdb-utils 0.1.4__py3-none-any.whl → 0.2.0__py3-none-any.whl
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/__init__.py +1 -1
- umnetdb_utils/base.py +28 -17
- umnetdb_utils/cli.py +114 -0
- umnetdb_utils/umnetdb.py +331 -31
- umnetdb_utils/umnetdisco.py +84 -64
- umnetdb_utils/umnetequip.py +90 -83
- umnetdb_utils/umnetinfo.py +3 -6
- umnetdb_utils/utils.py +293 -9
- {umnetdb_utils-0.1.4.dist-info → umnetdb_utils-0.2.0.dist-info}/METADATA +2 -1
- umnetdb_utils-0.2.0.dist-info/RECORD +12 -0
- umnetdb_utils-0.2.0.dist-info/entry_points.txt +3 -0
- umnetdb_utils-0.1.4.dist-info/RECORD +0 -10
- {umnetdb_utils-0.1.4.dist-info → umnetdb_utils-0.2.0.dist-info}/WHEEL +0 -0
umnetdb_utils/utils.py
CHANGED
@@ -1,6 +1,26 @@
|
|
1
1
|
import ipaddress
|
2
2
|
import re
|
3
|
-
|
3
|
+
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from copy import copy, deepcopy
|
6
|
+
|
7
|
+
from typing import Optional, Union, List, Dict
|
8
|
+
|
9
|
+
type IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
|
10
|
+
|
11
|
+
LOCAL_PROTOCOLS = [
|
12
|
+
"Access-internal",
|
13
|
+
"am",
|
14
|
+
"connected",
|
15
|
+
"direct",
|
16
|
+
"Direct",
|
17
|
+
"hmm",
|
18
|
+
"local",
|
19
|
+
"VPN",
|
20
|
+
"vrrpv3",
|
21
|
+
]
|
22
|
+
|
23
|
+
def is_ip_address(input_str:str, version:Optional[int]=None):
|
4
24
|
try:
|
5
25
|
ip = ipaddress.ip_address(input_str)
|
6
26
|
except ValueError:
|
@@ -11,8 +31,8 @@ def is_ip_address(input_str, version=None):
|
|
11
31
|
|
12
32
|
return True
|
13
33
|
|
14
|
-
|
15
|
-
|
34
|
+
|
35
|
+
def is_ip_network(input_str:str, version:Optional[int]=None):
|
16
36
|
# First check that this is a valid IP or network
|
17
37
|
try:
|
18
38
|
net = ipaddress.ip_network(input_str)
|
@@ -21,19 +41,283 @@ def is_ip_network(input_str, version=None):
|
|
21
41
|
|
22
42
|
if version and version != net.version:
|
23
43
|
return False
|
24
|
-
|
44
|
+
|
25
45
|
return True
|
26
46
|
|
27
|
-
|
28
|
-
|
47
|
+
|
48
|
+
def is_mac_address(input_str:str):
|
49
|
+
"""
|
29
50
|
Validates the input string as a mac address. Valid formats are
|
30
51
|
XX:XX:XX:XX:XX:XX, XX-XX-XX-XX-XX-XX, XXXX.XXXX.XXXX
|
31
52
|
where 'X' is a hexadecimal digit (upper or lowercase).
|
32
|
-
|
53
|
+
"""
|
33
54
|
mac = input_str.lower()
|
34
|
-
if re.match(r
|
55
|
+
if re.match(r"[0-9a-f]{2}([-:])[0-9a-f]{2}(\1[0-9a-f]{2}){4}$", mac):
|
35
56
|
return True
|
36
|
-
if re.match(r
|
57
|
+
if re.match(r"[0-9a-f]{4}\.[0-9a-f]{4}\.[0-9a-f]{4}$", mac):
|
37
58
|
return True
|
38
59
|
|
39
60
|
return False
|
61
|
+
|
62
|
+
|
63
|
+
@dataclass(order=True)
|
64
|
+
class Packet:
|
65
|
+
"""
|
66
|
+
Modeling a packet as it traverses the network
|
67
|
+
"""
|
68
|
+
|
69
|
+
dst_ip: IPAddress
|
70
|
+
#ttl: int = 255
|
71
|
+
inner_dst_ip: Optional[IPAddress] = None
|
72
|
+
label_stack: Optional[list[int]] = None
|
73
|
+
vni: Optional[int] = None
|
74
|
+
|
75
|
+
def __str__(self):
|
76
|
+
if self.label_stack:
|
77
|
+
return f"MPLS: {self.label_stack}"
|
78
|
+
if self.vni:
|
79
|
+
return f"VXLAN: {self.dst_ip}:{self.vni}"
|
80
|
+
return f"{self.dst_ip}"
|
81
|
+
|
82
|
+
# def decrement_ttl(self):
|
83
|
+
# new_packet = copy(self)
|
84
|
+
# new_packet.ttl -= 1
|
85
|
+
# return new_packet
|
86
|
+
|
87
|
+
def vxlan_encap(self, vni:int, tunnel_destination: ipaddress.IPv4Address) -> "Packet":
|
88
|
+
"""
|
89
|
+
Return a copy of the existing packet, but with a vxlan encap
|
90
|
+
"""
|
91
|
+
if self.vni or self.inner_dst_ip or self.label_stack:
|
92
|
+
raise ValueError(f"Can't encapsulate an already-encapsulated packet: {self}")
|
93
|
+
|
94
|
+
# new_packet = deepcopy(self)
|
95
|
+
# new_packet.inner_dst_ip = self.dst_ip
|
96
|
+
# new_packet.vni = vni
|
97
|
+
# new_packet.dst_ip = tunnel_destination
|
98
|
+
|
99
|
+
# return new_packet
|
100
|
+
self.inner_dst_ip = self.dst_ip
|
101
|
+
self.vni = vni
|
102
|
+
self.dst_ip = tunnel_destination
|
103
|
+
|
104
|
+
def vxlan_decap(self) -> "Packet":
|
105
|
+
"""
|
106
|
+
Return a copy of the existing packet, but with a vxlan decap
|
107
|
+
"""
|
108
|
+
if not self.vni or not self.inner_dst_ip or self.label_stack:
|
109
|
+
raise ValueError(f"Can't decap a non-VXLAN packet: {self}")
|
110
|
+
|
111
|
+
# new_packet = deepcopy(self)
|
112
|
+
# new_packet.dst_ip = self.inner_dst_ip
|
113
|
+
# new_packet.vni = None
|
114
|
+
# new_packet.inner_dst_ip = None
|
115
|
+
|
116
|
+
# return new_packet
|
117
|
+
self.dst_ip = self.inner_dst_ip
|
118
|
+
self.vni = None
|
119
|
+
self.inner_dst_ip = None
|
120
|
+
|
121
|
+
def mpls_push(self, label:Union[int,list[int]]) -> "Packet":
|
122
|
+
"""
|
123
|
+
Retrun a copy of the existing packet but with MPLS labels pushed on the stack
|
124
|
+
"""
|
125
|
+
if self.vni or self.inner_dst_ip:
|
126
|
+
raise ValueError(f"Can't do mpls on a VXLAN packet: {self}")
|
127
|
+
|
128
|
+
# new_packet = deepcopy(self)
|
129
|
+
# if not new_packet.label_stack:
|
130
|
+
# new_packet.label_stack = []
|
131
|
+
|
132
|
+
# if isinstance(label, int):
|
133
|
+
# new_packet.label_stack.append(label)
|
134
|
+
# else:
|
135
|
+
# new_packet.label_stack.extend(label)
|
136
|
+
|
137
|
+
# return new_packet
|
138
|
+
if not self.label_stack:
|
139
|
+
self.label_stack = []
|
140
|
+
|
141
|
+
if isinstance(label, list):
|
142
|
+
self.label_stack.extend(label)
|
143
|
+
else:
|
144
|
+
self.label_stack.append(label)
|
145
|
+
|
146
|
+
def mpls_pop(self, num_pops:int=1) -> "Packet":
|
147
|
+
"""
|
148
|
+
Return a copy of the existing packet but with MPLS label(s) popped
|
149
|
+
"""
|
150
|
+
if not self.label_stack:
|
151
|
+
raise ValueError(f"Can't pop from an empty label stack!: {self}")
|
152
|
+
if len(self.label_stack) < num_pops:
|
153
|
+
raise ValueError(f"Can't pop {num_pops} labels from packet: {self}")
|
154
|
+
|
155
|
+
# new_packet = copy(self)
|
156
|
+
# for _ in range(num_pops):
|
157
|
+
# new_packet.label_stack.pop()
|
158
|
+
|
159
|
+
# return new_packet
|
160
|
+
|
161
|
+
for _ in range(num_pops):
|
162
|
+
self.label_stack.pop()
|
163
|
+
|
164
|
+
|
165
|
+
def mpls_swap(self, label:Union[int,list[int]]) -> "Packet":
|
166
|
+
"""
|
167
|
+
Rerturn a copy of the existing packet but with MPLS label swap
|
168
|
+
"""
|
169
|
+
if not self.label_stack:
|
170
|
+
raise ValueError(f"Can't pop from an empty label stack!: {self}")
|
171
|
+
|
172
|
+
# new_packet = copy(self)
|
173
|
+
# new_packet.label_stack.pop()
|
174
|
+
# if isinstance(label, int):
|
175
|
+
# new_packet.label_stack.append(label)
|
176
|
+
|
177
|
+
# # inconsistency in mpls table where this really should be a 'pop' but
|
178
|
+
# # it shows up as a single blank entry in a list.
|
179
|
+
# elif label == [""]:
|
180
|
+
# new_packet.label_stack = []
|
181
|
+
# else:
|
182
|
+
# new_packet.label_stack.extend(label)
|
183
|
+
|
184
|
+
# return new_packet
|
185
|
+
self.label_stack.pop()
|
186
|
+
if label == [""]:
|
187
|
+
self.lable_stack = []
|
188
|
+
elif isinstance(label, list):
|
189
|
+
self.label_stack.extend(label)
|
190
|
+
else:
|
191
|
+
self.label_stack.append(label)
|
192
|
+
|
193
|
+
def is_encapped(self):
|
194
|
+
return bool(self.vni or self.label_stack)
|
195
|
+
|
196
|
+
|
197
|
+
@dataclass(order=True)
|
198
|
+
class Hop:
|
199
|
+
"""
|
200
|
+
A hop along the path and the corresponding packet at that location
|
201
|
+
"""
|
202
|
+
router: str
|
203
|
+
vrf: str
|
204
|
+
interface: str
|
205
|
+
packet: Packet
|
206
|
+
|
207
|
+
_parent: "Hop" = field(init=False, compare=False, default=None)
|
208
|
+
_children: List["Hop"] = field(init=False, compare=False, default_factory=list)
|
209
|
+
|
210
|
+
def __str__(self):
|
211
|
+
vrf = "" if self.vrf == "default" else f" VRF {self.vrf}"
|
212
|
+
return f"{self.router}{vrf} {self.interface}, Packet: {self.packet}"
|
213
|
+
|
214
|
+
def copy(self):
|
215
|
+
return deepcopy(self)
|
216
|
+
|
217
|
+
@classmethod
|
218
|
+
def unknown(self, packet=Packet):
|
219
|
+
"""
|
220
|
+
Returns an "unknown" hop for a specific packet
|
221
|
+
"""
|
222
|
+
return Hop("","","", packet)
|
223
|
+
|
224
|
+
def is_unknown(self):
|
225
|
+
return self.router == ""
|
226
|
+
|
227
|
+
def __hash__(self):
|
228
|
+
return(hash(str(self)))
|
229
|
+
|
230
|
+
def add_next_hop(self, other:"Hop"):
|
231
|
+
"""
|
232
|
+
creates parent/child relationship between this hop and another one
|
233
|
+
"""
|
234
|
+
self._children.append(other)
|
235
|
+
other._parent = self
|
236
|
+
|
237
|
+
|
238
|
+
|
239
|
+
class Path:
|
240
|
+
"""
|
241
|
+
Keeps track of all our paths
|
242
|
+
"""
|
243
|
+
def __init__(self, first_hop:Hop):
|
244
|
+
|
245
|
+
self.first_hop = first_hop
|
246
|
+
self.hops = {str(first_hop): first_hop}
|
247
|
+
|
248
|
+
def add_hop(self, curr_hop:Hop, next_hop:Hop) -> bool:
|
249
|
+
"""
|
250
|
+
Adds a hop to our current hop. If this is a new hop,
|
251
|
+
which means we're on a new path, returns true. Otherwise
|
252
|
+
returns false to indicate we don't have to walk this path
|
253
|
+
since it's a duplicate of another.
|
254
|
+
"""
|
255
|
+
|
256
|
+
# if we've seen this hop already we will check for loops.
|
257
|
+
if str(next_hop) in self.hops:
|
258
|
+
new_path = False
|
259
|
+
|
260
|
+
# parent = self.curr_hop._parent
|
261
|
+
# while parent:
|
262
|
+
# if parent == next_hop:
|
263
|
+
# raise ValueError(f"Loop detected for {self.curr_hop} -> {next_hop}")
|
264
|
+
# parent = parent._parent
|
265
|
+
|
266
|
+
self.hops[str(next_hop)]
|
267
|
+
|
268
|
+
else:
|
269
|
+
new_path = True
|
270
|
+
self.hops[str(next_hop)] = next_hop
|
271
|
+
|
272
|
+
curr_hop.add_next_hop(next_hop)
|
273
|
+
return new_path
|
274
|
+
|
275
|
+
|
276
|
+
|
277
|
+
def walk_path(self) -> Dict[int, List[Hop]]:
|
278
|
+
"""
|
279
|
+
'Flattens' path from first hop to last into a dict
|
280
|
+
of list of hops keyed on hop number.
|
281
|
+
"""
|
282
|
+
hop_num = 1
|
283
|
+
result = { hop_num: {self.first_hop}}
|
284
|
+
self._walk_path(self.first_hop, hop_num, result)
|
285
|
+
return result
|
286
|
+
|
287
|
+
|
288
|
+
def _walk_path(self, curr_hop, hop_num, result):
|
289
|
+
hop_num +=1
|
290
|
+
for child in curr_hop._children:
|
291
|
+
if hop_num not in result:
|
292
|
+
result[hop_num] = set()
|
293
|
+
result[hop_num].add(child)
|
294
|
+
self._walk_path(child, hop_num, result)
|
295
|
+
|
296
|
+
|
297
|
+
def print_path(self):
|
298
|
+
"""
|
299
|
+
Prints out the path, hop by hop
|
300
|
+
"""
|
301
|
+
for hop_num, hops in self.walk_path().items():
|
302
|
+
hop_list = list(hops)
|
303
|
+
hop_list.sort()
|
304
|
+
print(f"***** hop {hop_num} *****")
|
305
|
+
for hop in hop_list:
|
306
|
+
print(f"\t{hop}")
|
307
|
+
|
308
|
+
|
309
|
+
def get_path(self) -> list[dict]:
|
310
|
+
"""
|
311
|
+
Returns hops as a list of dicts - for very basic displaying in
|
312
|
+
a table. In the future want to beef up the cli script
|
313
|
+
so that it can do nested tables, which is really
|
314
|
+
what we need for this.
|
315
|
+
"""
|
316
|
+
result = []
|
317
|
+
for hop_num, hops in self.walk_path().items():
|
318
|
+
hop_list = list(hops)
|
319
|
+
hop_list.sort()
|
320
|
+
hop_str = "\n".join([str(h) for h in hop_list]) + "\n"
|
321
|
+
result.append({'hop_num':hop_num, 'hops': hop_str})
|
322
|
+
|
323
|
+
return result
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: umnetdb-utils
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0
|
4
4
|
Summary: Helper classes for querying UMnet databases
|
5
5
|
License: MIT
|
6
6
|
Author: Amy Liebowitz
|
@@ -16,6 +16,7 @@ Requires-Dist: oracledb (>=3.1.0,<4.0.0)
|
|
16
16
|
Requires-Dist: psycopg[binary] (>=3.2.9,<4.0.0)
|
17
17
|
Requires-Dist: python-decouple (>=3.8,<4.0)
|
18
18
|
Requires-Dist: sqlalchemy (>=2.0.41,<3.0.0)
|
19
|
+
Requires-Dist: typer (>=0.16.0,<0.17.0)
|
19
20
|
Description-Content-Type: text/markdown
|
20
21
|
|
21
22
|
# umnetdb-utils
|
@@ -0,0 +1,12 @@
|
|
1
|
+
umnetdb_utils/__init__.py,sha256=YrT7drJ2MlU287z4--8EQI3fKLQZBlVxaCacU1yoDtg,132
|
2
|
+
umnetdb_utils/base.py,sha256=LWVhrbShhuYekXZxwvhhVUkH6DHd_wnJ2QZ_zw3fKqM,6139
|
3
|
+
umnetdb_utils/cli.py,sha256=tq_XxGrCWJacP3tzxTULPHeuvbh1AU_GHXHY1jkI4-U,3349
|
4
|
+
umnetdb_utils/umnetdb.py,sha256=bzMwItOfCgpKhCGwmghqAyp9gKvckGsUIGvSJC1iUho,17122
|
5
|
+
umnetdb_utils/umnetdisco.py,sha256=D9MiF_QrYznYL_ozLUmD1ASzkGDs1jqZ_XksNV7wW3o,8018
|
6
|
+
umnetdb_utils/umnetequip.py,sha256=Nnh_YCcqGpYRp2KsS8qbAGJ9AgTeYB4umeYeP9_B-6A,5440
|
7
|
+
umnetdb_utils/umnetinfo.py,sha256=vqFvLbIu7Wu650RTqzaELpbKhq6y91fgBXM5Q_elRl4,18443
|
8
|
+
umnetdb_utils/utils.py,sha256=kAVLjX4UvN5VdllYPN2t-q386iDjsZZ-Bp8G_ZTchbI,9418
|
9
|
+
umnetdb_utils-0.2.0.dist-info/METADATA,sha256=kkv7rHDme5K2VXBfe24KFg6qBOsezgLZc8XYku5dpwU,1595
|
10
|
+
umnetdb_utils-0.2.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
11
|
+
umnetdb_utils-0.2.0.dist-info/entry_points.txt,sha256=nNmxVRlYLaeepjDKNCyARBF3d8igHtZGTVA_zoglevI,50
|
12
|
+
umnetdb_utils-0.2.0.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
1
|
-
umnetdb_utils/__init__.py,sha256=QJaytbr4ccKESiwaKjpf1b4b8s2cHNfCDdnCOs1tmoI,131
|
2
|
-
umnetdb_utils/base.py,sha256=aC5oKSB5Ox9QtylmHV2uvEUVUx4CRLZTgH6HpQJ6DZo,6064
|
3
|
-
umnetdb_utils/umnetdb.py,sha256=IxqpAcPZn71HVV_oL3ryvbKS7cupUsyM6rJSGk6NLaU,4554
|
4
|
-
umnetdb_utils/umnetdisco.py,sha256=Z2XwT79jKO_avd3w_z99DDEdAikrfKxYm1JYHRWqvG4,7841
|
5
|
-
umnetdb_utils/umnetequip.py,sha256=jOW5kvk0FXtdHv8PA4rYT_PWfLdMiq83Mjwmy-c1DN8,5701
|
6
|
-
umnetdb_utils/umnetinfo.py,sha256=MH1YDW4OWtHD46qfYb5Pv40vPSbL0GrMNW5gAhdlihE,18445
|
7
|
-
umnetdb_utils/utils.py,sha256=wU6QMYfofj7trX3QeqXty0btbGdhP_RUaSqA7QTflFM,991
|
8
|
-
umnetdb_utils-0.1.4.dist-info/METADATA,sha256=JI2hwHkX3B8y-GLwS0yywAiFszcqUlkiWUyga0A2KaY,1555
|
9
|
-
umnetdb_utils-0.1.4.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
10
|
-
umnetdb_utils-0.1.4.dist-info/RECORD,,
|
File without changes
|