umnetdb-utils 0.1.0__py3-none-any.whl → 0.1.2__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/base.py CHANGED
@@ -2,6 +2,7 @@
2
2
  from os import getenv
3
3
  import re
4
4
  import logging
5
+ from decouple import Config, RepositoryEnv
5
6
 
6
7
  from sqlalchemy import create_engine, text
7
8
  from sqlalchemy.orm import Session
@@ -15,13 +16,20 @@ class UMnetdbBase:
15
16
  # set in child classes - you can use environment variables within curly braces here
16
17
  URL = None
17
18
 
18
- def __init__(self):
19
+ def __init__(self, env_file:str=".env"):
19
20
  """
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.
21
+ Initiate a umnetdb object. Optionally provide a path to a file with environment variables
22
+ containing the credentials for the database. If no file is provided and there's no ".env",
23
+ the code will look in the user's environment (os.getenv) for these values.
23
24
  """
25
+
26
+ try:
27
+ self._env = Config(RepositoryEnv(env_file))
28
+ except FileNotFoundError:
29
+ self._env = {}
30
+
24
31
  self.url = self._resolve_url()
32
+
25
33
  self.engine = create_engine(self.url)
26
34
  self.session = None
27
35
 
@@ -34,10 +42,11 @@ class UMnetdbBase:
34
42
  url = self.URL
35
43
  for m in re.finditer(r"{(\w+)}", url):
36
44
  var = m.group(1)
45
+ val = self._env.get(var, getenv(var))
37
46
 
38
- if not getenv(var):
47
+ if not val:
39
48
  raise ValueError(f"Undefined environment variable {var} in {url}")
40
- url = re.sub(r"{" + var + "}", getenv(var), url)
49
+ url = re.sub(r"{" + var + "}", val, url)
41
50
 
42
51
  return url
43
52
 
@@ -83,8 +92,9 @@ class UMnetdbBase:
83
92
  ex: "node_ip nip"
84
93
  :joins: a list of strings representing join statements. Include the actual 'join' part!
85
94
  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
95
+ :where: For a single where statement, provide a string. For multiple provide a list.
96
+ The list of statements are "anded". If you need "or", embed it in one of your list items
97
+ DO NOT provide the keyword 'where' - it is auto-added.
88
98
  ex: ["node_ip.ip = '1.2.3.4'", "node.switch = '10.233.0.5'"]
89
99
  :order_by: A string representing a column name (or names) to order by
90
100
  :group_by: A string representing a column name (or names) to group by
@@ -106,9 +116,12 @@ class UMnetdbBase:
106
116
  sql += f"{j}\n"
107
117
 
108
118
  # Next are the filters. They are 'anded'
109
- if where:
119
+ if where and isinstance(where, list):
120
+
110
121
  sql += "where\n"
111
122
  sql += " and\n".join(where) + "\n"
123
+ elif where:
124
+ sql += f"where {where}\n"
112
125
 
113
126
  # Finally the other options
114
127
  if order_by:
@@ -126,7 +139,12 @@ class UMnetdbBase:
126
139
 
127
140
  def _execute(self, sql, rows_as_dict=True):
128
141
  '''
129
- Generic sqlalchemy "execute this sql command and give me all the results"
142
+ Generic sqlalchemy "open a session, execute this sql command and give me all the results"
143
+
144
+ NB This function is defined for legacy database classes that came from umnet-scripts.
145
+ It's encouraged to use "self.session.execute" in other child methods, allowing
146
+ scripts that import the child class to use the context manager and execute multiple
147
+ mehtods within the same session.
130
148
  '''
131
149
  with self as session:
132
150
  r = session.execute(text(sql))
@@ -137,4 +155,19 @@ class UMnetdbBase:
137
155
  elif rows:
138
156
  return rows
139
157
  else:
140
- return []
158
+ return []
159
+
160
+ def execute(self, sql:str, rows_as_dict:bool=True, fetch_one:bool=False):
161
+ """
162
+ Executes a sqlalchemy command and gives all the results. Does not open a session - you must
163
+ open one yourself
164
+ """
165
+ result = self.session.execute(text(sql))
166
+
167
+ if rows_as_dict:
168
+ result = result.mappings()
169
+
170
+ if fetch_one:
171
+ return result.fetchone()
172
+
173
+ return result.fetchall()
umnetdb_utils/umnetdb.py CHANGED
@@ -1,5 +1,7 @@
1
1
 
2
2
  from typing import List
3
+ import logging
4
+
3
5
 
4
6
  from sqlalchemy import text
5
7
  from .base import UMnetdbBase
@@ -9,7 +11,7 @@ class UMnetdb(UMnetdbBase):
9
11
 
10
12
  URL="postgresql+psycopg://{UMNETDB_USER}:{UMNETDB_PASSWORD}@wintermute.umnet.umich.edu/umnetdb"
11
13
 
12
- def get_device_neighbors(
14
+ def get_neighbors(
13
15
  self, device: str, known_devices_only: bool = True
14
16
  ) -> List[dict]:
15
17
  """
@@ -33,6 +35,11 @@ class UMnetdb(UMnetdbBase):
33
35
  "l.parent",
34
36
  "n_l.parent as remote_parent"
35
37
  ]
38
+ joins = [
39
+ "join device n_d on n_d.hostname=n.remote_device",
40
+ "left outer join lag l on l.device=n.device and l.member=n.port",
41
+ "left outer join lag n_l on n_l.device=n_d.name and n_l.member=n.remote_port",
42
+ ]
36
43
  else:
37
44
  select = [
38
45
  "n.port",
@@ -41,18 +48,65 @@ class UMnetdb(UMnetdbBase):
41
48
  "l.parent",
42
49
  "n_l.parent as remote_parent"
43
50
  ]
44
-
51
+ joins = [
52
+ "left outer join device n_d on n_d.hostname=n.remote_device",
53
+ "left outer join lag l on l.device=n.device and l.member=n.port",
54
+ "left outer join lag n_l on n_l.device=n_d.name and n_l.member=n.remote_port",
55
+ ]
56
+
45
57
  table = "neighbor n"
46
58
 
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
- ]
59
+
52
60
 
53
61
  where = [f"n.device='{device}'"]
54
62
 
55
63
  query = self._build_select(select, table, joins, where)
56
64
  result = self.session.execute(text(query))
57
65
 
58
- return [dict(zip(result.keys(), r)) for r in result]
66
+ return [dict(zip(result.keys(), r)) for r in result]
67
+
68
+
69
+ def get_dlzone(self, zone_name:str) -> List[dict]:
70
+ """
71
+ Gets all devices within a DL zone based on walking the 'neighbors'
72
+ table.
73
+
74
+ For each device, the following attributes are returned:
75
+ "name", "ip", "version", "vendor", "model", "serial"
76
+ """
77
+ device_cols = ["name", "ip", "version", "vendor", "model", "serial"]
78
+
79
+ # step 1 is to find DLs in the database - we'll seed our zone with them
80
+ query = self._build_select(select=device_cols, table="device",where=f"name similar to '(d-|dl-){zone_name}-(1|2)'")
81
+ dls = self.execute(query)
82
+
83
+ if not dls:
84
+ raise ValueError(f"No DLs found in umnetdb for zone {zone_name}")
85
+
86
+ devices_by_name = {d['name']:d for d in dls}
87
+
88
+ # now we'll look for neighbors on each device within the zone.
89
+ # Note that outside of the DLs we only expect to find devices that start with
90
+ # "s-" anything else is considered 'outside the zone'
91
+ todo = list(devices_by_name.keys())
92
+ while(len(todo) != 0):
93
+
94
+ device = todo.pop()
95
+
96
+ # note that by default this method only returns neighbors in the 'device' table,
97
+ # any others are ignored
98
+ neighs = self.get_device_neighbors(device)
99
+
100
+ for neigh in neighs:
101
+ if neigh["remote_device"].startswith("s-"):
102
+
103
+ query = self._build_select(select=device_cols, table="device", where=f"name = '{neigh['remote_device']}'")
104
+ neigh_device = self.execute(query, fetch_one=True)
105
+
106
+ if neigh_device["name"] not in devices_by_name:
107
+ todo.append(neigh_device["name"])
108
+
109
+ devices_by_name[neigh_device["name"]] = neigh_device
110
+
111
+
112
+ return list(devices_by_name.values())
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.3
2
+ Name: umnetdb-utils
3
+ Version: 0.1.2
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: python-decouple (>=3.8,<4.0)
18
+ Requires-Dist: sqlalchemy (>=2.0.41,<3.0.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # umnetdb-utils
22
+ Helper classes for gathering data from umnet databases
23
+
24
+ This package is hosted on pypi - you can install it with `pip install umnetdb-utils` and use it in your own code.
25
+
26
+ ## Database Helper Classes
27
+ As of May 2025 this repo defines db wrapper classes for Equipdb, Netinfo, Netdisco and UMnetdb (populated by agador, hosted on wintermute).
28
+ To use these classes you need to provide credentials, either in a config file that you pass into the initializer, in `.env`, or in your environment:
29
+ * Netinfo: `NETINFO_USERNAME`, `NETINFO_PASSSWORD`
30
+ * Netdisco: `NETDISCO_DB_USER`, `NETDISCO_DB_PASSWORD`
31
+ * Equipdb: `EQUIP_DB_USER`, `EQUIP_DB_PASSWORD`
32
+ * UMnetdb: `UMNETDB_USER`, `UMNETDB_PASSWORD`
33
+
34
+ Netinfo, Netdisco, and Equipdb classes are copied over from `umnet-scripts` which is reaching the end of its life as a package.
35
+
@@ -1,10 +1,10 @@
1
1
  umnetdb_utils/__init__.py,sha256=QJaytbr4ccKESiwaKjpf1b4b8s2cHNfCDdnCOs1tmoI,131
2
- umnetdb_utils/base.py,sha256=u0ZEKb3Vx8aSTrN8r4dbVCSpKHaBVw0Aw4VwTZfMRjc,4609
3
- umnetdb_utils/umnetdb.py,sha256=2jPsZsJWFtwWKIRqww86B8tRWBQ0cbw3iOk9PwAXaxY,1948
2
+ umnetdb_utils/base.py,sha256=aIcCdeQ2FTrWV4Az_gdPc-aXuEwRjhef1Xj9SVak438,5878
3
+ umnetdb_utils/umnetdb.py,sha256=O-JLqyECPC5sGcUf2ICEKWVR1oAaOaH8H6GY2eYd3BQ,4065
4
4
  umnetdb_utils/umnetdisco.py,sha256=Z2XwT79jKO_avd3w_z99DDEdAikrfKxYm1JYHRWqvG4,7841
5
5
  umnetdb_utils/umnetequip.py,sha256=jOW5kvk0FXtdHv8PA4rYT_PWfLdMiq83Mjwmy-c1DN8,5701
6
6
  umnetdb_utils/umnetinfo.py,sha256=MH1YDW4OWtHD46qfYb5Pv40vPSbL0GrMNW5gAhdlihE,18445
7
7
  umnetdb_utils/utils.py,sha256=wU6QMYfofj7trX3QeqXty0btbGdhP_RUaSqA7QTflFM,991
8
- umnetdb_utils-0.1.0.dist-info/METADATA,sha256=JTcsp2paNp2hNZowNqNbfGyZIx23TKym5x3pVtz5GBc,750
9
- umnetdb_utils-0.1.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
- umnetdb_utils-0.1.0.dist-info/RECORD,,
8
+ umnetdb_utils-0.1.2.dist-info/METADATA,sha256=hHifN_wH6Qe-222YqMLtXmC3l2Iq5f83zsNxgzbdjtY,1555
9
+ umnetdb_utils-0.1.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
10
+ umnetdb_utils-0.1.2.dist-info/RECORD,,
@@ -1,22 +0,0 @@
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
-