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/umnetdisco.py
CHANGED
@@ -1,24 +1,31 @@
|
|
1
|
-
|
2
|
-
|
3
1
|
from .base import UMnetdbBase
|
4
2
|
from .utils import is_ip_address, is_mac_address
|
5
3
|
|
6
4
|
|
7
5
|
class Umnetdisco(UMnetdbBase):
|
8
|
-
URL="postgresql+psycopg://{NETDISCO_DB_USER}:{NETDISCO_DB_PASSWORD}@netdisco.umnet.umich.edu:5432/netdisco"
|
6
|
+
URL = "postgresql+psycopg://{NETDISCO_DB_USER}:{NETDISCO_DB_PASSWORD}@netdisco.umnet.umich.edu:5432/netdisco"
|
9
7
|
|
10
|
-
def host_arp(
|
11
|
-
|
12
|
-
|
8
|
+
def host_arp(
|
9
|
+
self, host, start_time=None, end_time=None, limit=1, active_only=False
|
10
|
+
):
|
11
|
+
"""
|
12
|
+
Does an ARP query against the netdisco db for a single host.
|
13
13
|
:host: A string representing a MAC address or an IPv4 address
|
14
|
-
|
14
|
+
"""
|
15
15
|
|
16
16
|
# what table are we querying?
|
17
|
-
table =
|
18
|
-
joins = [
|
17
|
+
table = "node_ip nip"
|
18
|
+
joins = ["join node n on nip.mac = n.mac"]
|
19
19
|
|
20
20
|
# define select statements
|
21
|
-
select = [
|
21
|
+
select = [
|
22
|
+
"nip.mac",
|
23
|
+
"nip.ip",
|
24
|
+
"n.switch",
|
25
|
+
"n.port",
|
26
|
+
"nip.time_first",
|
27
|
+
"nip.time_last",
|
28
|
+
]
|
22
29
|
|
23
30
|
# First determine if this host is a MAC address or an IP
|
24
31
|
where = []
|
@@ -31,7 +38,9 @@ class Umnetdisco(UMnetdbBase):
|
|
31
38
|
|
32
39
|
# filter for specific start/end times if specified
|
33
40
|
if start_time and end_time:
|
34
|
-
where.append(
|
41
|
+
where.append(
|
42
|
+
f"n.time_last between timestamp '{start_time}' and timestamp '{end_time}'"
|
43
|
+
)
|
35
44
|
elif start_time:
|
36
45
|
where.append(f"n.time_last > timestamp '{start_time}'")
|
37
46
|
|
@@ -40,63 +49,73 @@ class Umnetdisco(UMnetdbBase):
|
|
40
49
|
where.append("nip.active = true")
|
41
50
|
|
42
51
|
# order by last seen
|
43
|
-
sql = self._build_select(
|
52
|
+
sql = self._build_select(
|
53
|
+
select, table, joins=joins, where=where, order_by="time_last", limit=limit
|
54
|
+
)
|
44
55
|
result = self._execute(sql)
|
45
56
|
|
46
57
|
return result
|
47
58
|
|
48
59
|
def arp_count(self, prefix=None, start_time=None, end_time=None, active_only=False):
|
49
|
-
|
60
|
+
"""
|
50
61
|
Queries the host data in netdisco based on prefix and start/end time.
|
51
62
|
First any prefix greater than or equal to the inputted prefix is searched for
|
52
63
|
(in the 'device_ip' table).
|
53
64
|
|
54
65
|
Then the host table is queried.
|
55
|
-
|
56
|
-
|
66
|
+
"""
|
67
|
+
|
57
68
|
# We're counting all the host IPs by subnet
|
58
|
-
select = [
|
59
|
-
table =
|
69
|
+
select = ["count(distinct nip.ip)", "dip.subnet"]
|
70
|
+
table = "node_ip nip"
|
60
71
|
|
61
72
|
# postgres allows us to do a join based on an IPs (type inet) membership
|
62
73
|
# of a subnet (type cidr)
|
63
|
-
joins = [
|
74
|
+
joins = ["join device_ip dip on nip.ip <<= dip.subnet"]
|
64
75
|
|
65
76
|
# grouping by subnet is what gives us per-subnet host counts
|
66
|
-
group_by =
|
77
|
+
group_by = "dip.subnet"
|
67
78
|
|
68
79
|
# append all cli filtering options
|
69
80
|
where = [f"dip.subnet <<= inet '{prefix}'"]
|
70
81
|
if start_time and end_time:
|
71
|
-
where.append(
|
82
|
+
where.append(
|
83
|
+
f"nip.time_last between timestamp '{start_time}' and timestamp '{end_time}'"
|
84
|
+
)
|
72
85
|
elif start_time:
|
73
86
|
where.append(f"nip.time_last > timestamp '{start_time}'")
|
74
87
|
|
75
88
|
if active_only:
|
76
89
|
where.append("nip.active = true")
|
77
90
|
|
78
|
-
sql = self._build_select(
|
91
|
+
sql = self._build_select(
|
92
|
+
select, table, joins=joins, where=where, group_by=group_by
|
93
|
+
)
|
79
94
|
return self._execute(sql)
|
80
95
|
|
81
96
|
def neighbors(self, device=None):
|
82
|
-
|
97
|
+
"""
|
83
98
|
Queries the device_ip table in netdisco to get neighbors of a device.
|
84
99
|
If no device is specified, all neighbors are pulled.
|
85
100
|
The device input can be an IPv4 address or an FQDN.
|
86
|
-
|
87
|
-
|
88
|
-
select = [
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
101
|
+
"""
|
102
|
+
|
103
|
+
select = [
|
104
|
+
"dp.ip as local_ip",
|
105
|
+
"ld.dns as local_dns",
|
106
|
+
"dp.port as local_port",
|
107
|
+
"dp.remote_ip",
|
108
|
+
"rd.dns as remote_dns",
|
109
|
+
"dp.remote_port",
|
110
|
+
]
|
111
|
+
table = "device d"
|
112
|
+
joins = [
|
113
|
+
"join device_port dp on d.ip = dp.ip",
|
114
|
+
"join device ld on dp.ip = ld.ip",
|
115
|
+
"join device rd on dp.remote_ip = rd.ip",
|
116
|
+
]
|
117
|
+
|
118
|
+
where = ["dp.remote_ip is not null"]
|
100
119
|
if is_ip_address(device):
|
101
120
|
where.append(f"d.ip = '{device}'")
|
102
121
|
elif device:
|
@@ -106,37 +125,39 @@ class Umnetdisco(UMnetdbBase):
|
|
106
125
|
return self._execute(sql)
|
107
126
|
|
108
127
|
def get_devices(self, match_subnets=None, exclude_subnets=None):
|
109
|
-
|
128
|
+
"""
|
110
129
|
Queries netdisco for a list of devices.
|
111
130
|
Optionally, limit it by a list of prefixes
|
112
|
-
|
131
|
+
"""
|
113
132
|
|
114
|
-
select = [
|
115
|
-
table =
|
133
|
+
select = ["ip", "dns", "serial", "model", "vendor", "os"]
|
134
|
+
table = "device"
|
116
135
|
|
117
136
|
where = []
|
118
137
|
if match_subnets:
|
119
138
|
where.append(" or\n".join([f"ip << inet '{s}'" for s in match_subnets]))
|
120
|
-
|
139
|
+
|
121
140
|
if exclude_subnets:
|
122
|
-
where.append(
|
141
|
+
where.append(
|
142
|
+
"not\n" + " or\n".join([f"ip << inet '{s}'" for s in exclude_subnets])
|
143
|
+
)
|
123
144
|
|
124
145
|
sql = self._build_select(select, table, where=where)
|
125
146
|
return self._execute(sql)
|
126
147
|
|
127
148
|
def get_device(self, name_or_ip):
|
128
|
-
|
149
|
+
"""
|
129
150
|
Pulls the device name, ip, model, and description (where version info often is)
|
130
151
|
from the netinfo device table
|
131
|
-
|
152
|
+
"""
|
132
153
|
select = [
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
154
|
+
"replace(lower(name), '.umnet.umich.edu', '') as name",
|
155
|
+
"device.ip as ip",
|
156
|
+
"model",
|
157
|
+
"description",
|
158
|
+
]
|
138
159
|
table = "device"
|
139
|
-
|
160
|
+
|
140
161
|
if is_ip_address(name_or_ip):
|
141
162
|
joins = ["join device_ip on device.ip=device_ip.alias"]
|
142
163
|
where = [f"device_ip.alias='{name_or_ip}'"]
|
@@ -153,7 +174,7 @@ class Umnetdisco(UMnetdbBase):
|
|
153
174
|
return None
|
154
175
|
|
155
176
|
def get_port_and_host_data(self, name_or_ip):
|
156
|
-
|
177
|
+
"""
|
157
178
|
Pulls a list of all ports on the switch and all hosts seen on those ports.
|
158
179
|
You will see multiple rows for an interface where multiple hosts have been seen.
|
159
180
|
|
@@ -168,21 +189,21 @@ class Umnetdisco(UMnetdbBase):
|
|
168
189
|
- vlan the host was seen on
|
169
190
|
- date last mac was seen
|
170
191
|
|
171
|
-
|
192
|
+
"""
|
172
193
|
|
173
194
|
# getting device by name or IP
|
174
195
|
if is_ip_address(name_or_ip):
|
175
196
|
device_ip = name_or_ip
|
176
197
|
else:
|
177
198
|
nd_device = self.get_device(name_or_ip)
|
178
|
-
if not(nd_device):
|
179
|
-
raise LookupError(f
|
180
|
-
device_ip = nd_device[
|
199
|
+
if not (nd_device):
|
200
|
+
raise LookupError(f"Could not find {nd_device} in Netdisco")
|
201
|
+
device_ip = nd_device["ip"]
|
181
202
|
|
182
203
|
# The port description is in different fields depending on the platform, so we need to do
|
183
204
|
# some if-else stuff that's not supported in our basic sql builer in the base class.
|
184
205
|
# That's why we're just doing a long sting here
|
185
|
-
sql = f
|
206
|
+
sql = f"""
|
186
207
|
select
|
187
208
|
dp.port,
|
188
209
|
case
|
@@ -202,7 +223,7 @@ from device_port dp
|
|
202
223
|
left outer join node on dp.port = node.port and dp.ip = node.switch
|
203
224
|
left outer join node_ip on node.mac = node_ip.mac
|
204
225
|
where dp.ip='{device_ip}'
|
205
|
-
|
226
|
+
"""
|
206
227
|
|
207
228
|
result = self._execute(sql)
|
208
229
|
|
@@ -210,7 +231,7 @@ where dp.ip='{device_ip}'
|
|
210
231
|
|
211
232
|
def get_poe_data(self, name_or_ip):
|
212
233
|
"""
|
213
|
-
Pulls PoE admin, status, class, and power columns from device_port_pwer
|
234
|
+
Pulls PoE admin, status, class, and power columns from device_port_pwer
|
214
235
|
"""
|
215
236
|
|
216
237
|
# getting device by name or IP
|
@@ -218,16 +239,15 @@ where dp.ip='{device_ip}'
|
|
218
239
|
device_ip = name_or_ip
|
219
240
|
else:
|
220
241
|
nd_device = self.get_device(name_or_ip)
|
221
|
-
if not(nd_device):
|
222
|
-
raise LookupError(f
|
223
|
-
device_ip = nd_device[
|
224
|
-
|
242
|
+
if not (nd_device):
|
243
|
+
raise LookupError(f"Could not find {nd_device} in Netdisco")
|
244
|
+
device_ip = nd_device["ip"]
|
245
|
+
|
225
246
|
select = ["port", "admin", "status", "class", "power"]
|
226
247
|
table = "device_port_power"
|
227
248
|
where = [f"ip='{device_ip}'"]
|
228
249
|
|
229
|
-
|
230
250
|
sql = self._build_select(select, table, where=where)
|
231
251
|
result = self._execute(sql)
|
232
252
|
|
233
|
-
return result
|
253
|
+
return result
|
umnetdb_utils/umnetequip.py
CHANGED
@@ -3,25 +3,28 @@ from typing import Union, Optional
|
|
3
3
|
from .base import UMnetdbBase
|
4
4
|
from .utils import is_ip_address
|
5
5
|
|
6
|
+
|
6
7
|
class UMnetequip(UMnetdbBase):
|
7
|
-
|
8
|
+
"""
|
8
9
|
This class wraps helpful equip db queries in python.
|
9
|
-
|
10
|
+
"""
|
11
|
+
|
10
12
|
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
13
|
|
12
14
|
def get_devices_by_category(self, category, active_only=False):
|
13
|
-
|
15
|
+
"""
|
14
16
|
Queries equip db for devices by category. You can also
|
15
17
|
specify if you only want active devices.
|
16
|
-
|
18
|
+
"""
|
17
19
|
|
18
|
-
select = [
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
20
|
+
select = [
|
21
|
+
"eq.monitored_device",
|
22
|
+
"eq.rancid",
|
23
|
+
"eq.off_line",
|
24
|
+
"eq.dns_name",
|
25
|
+
"eq.ip_address",
|
26
|
+
]
|
27
|
+
table = "ARADMIN1.UMNET_EQUIPMENT eq"
|
25
28
|
|
26
29
|
where = [f"eq.category = '{category}'"]
|
27
30
|
|
@@ -29,54 +32,55 @@ class UMnetequip(UMnetdbBase):
|
|
29
32
|
# 1: RESERVE, 2:ACTIVE, 3:RETIRED
|
30
33
|
if active_only:
|
31
34
|
where.append("eq.status = 2")
|
32
|
-
|
35
|
+
|
33
36
|
sql = self._build_select(select, table, where=where, distinct=True)
|
34
37
|
return self._execute(sql)
|
35
38
|
|
36
|
-
|
37
39
|
def get_device_type(self, ip):
|
38
|
-
|
40
|
+
"""
|
39
41
|
Queries equip db for a device by ip, returns the 'type' of the device.
|
40
42
|
By type we mean the UMNET_EQUIPMENT_TYPE: ACCESS LAYER, DISTRIBUTION LAYER, UMD, etc
|
41
|
-
|
42
|
-
select = [
|
43
|
-
table =
|
43
|
+
"""
|
44
|
+
select = ["types as type"]
|
45
|
+
table = "ARADMIN1.UMNET_EQUIPMENT"
|
44
46
|
where = [f"ip_address='{ip}'"]
|
45
47
|
|
46
48
|
sql = self._build_select(select, table, where=where)
|
47
49
|
return self._execute(sql)
|
48
50
|
|
49
|
-
def get_devices_by_building_no(
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
):
|
51
|
+
def get_devices_by_building_no(
|
52
|
+
self,
|
53
|
+
building_no: Union[int, list],
|
54
|
+
active_only: bool = False,
|
55
|
+
types: Optional[list] = None,
|
56
|
+
location_info: bool = False,
|
57
|
+
):
|
57
58
|
"""
|
58
59
|
Queries equipdb for devices by building no. You can provide a single
|
59
60
|
building number, or a list of numbers. You can specify if you want 'active only' devices
|
60
61
|
(based on the 'status' field, defalut false) or you can limit to a certain device type (default all).
|
61
62
|
You can also get the location info for each device via 'location_info' (default false)
|
62
63
|
"""
|
63
|
-
select = [
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
64
|
+
select = [
|
65
|
+
"dns_name",
|
66
|
+
"ip_address",
|
67
|
+
"model_no_ as model",
|
68
|
+
"types as device_type",
|
69
|
+
"rancid",
|
70
|
+
"off_line",
|
71
|
+
]
|
70
72
|
if location_info:
|
71
|
-
select.extend(
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
73
|
+
select.extend(
|
74
|
+
[
|
75
|
+
"bldg as building_name",
|
76
|
+
"address as building_address",
|
77
|
+
"room as room_no",
|
78
|
+
"floor as floor",
|
79
|
+
"bldg_code__ as building_no",
|
80
|
+
]
|
81
|
+
)
|
82
|
+
|
83
|
+
table = "ARADMIN1.UMNET_EQUIPMENT eq"
|
80
84
|
|
81
85
|
where = []
|
82
86
|
if isinstance(building_no, int):
|
@@ -99,59 +103,62 @@ class UMnetequip(UMnetdbBase):
|
|
99
103
|
sql = self._build_select(select, table, where=where)
|
100
104
|
return self._execute(sql)
|
101
105
|
|
102
|
-
def get_device(self, name_or_ip:str, location_info:bool=False):
|
103
|
-
|
106
|
+
def get_device(self, name_or_ip: str, location_info: bool = False):
|
104
107
|
if is_ip_address(name_or_ip):
|
105
108
|
where = [f"ip_address='{name_or_ip}'"]
|
106
109
|
else:
|
107
|
-
name_or_ip = name_or_ip.replace(".umnet.umich.edu","")
|
110
|
+
name_or_ip = name_or_ip.replace(".umnet.umich.edu", "")
|
108
111
|
where = [f"dns_name='{name_or_ip}'"]
|
109
112
|
|
110
|
-
select = [
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
113
|
+
select = [
|
114
|
+
"dns_name",
|
115
|
+
"ip_address",
|
116
|
+
"model_no_ as model",
|
117
|
+
"types as device_type",
|
118
|
+
"rancid",
|
119
|
+
"off_line",
|
120
|
+
]
|
117
121
|
if location_info:
|
118
|
-
select.extend(
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
122
|
+
select.extend(
|
123
|
+
[
|
124
|
+
"bldg as building_name",
|
125
|
+
"address as building_address",
|
126
|
+
"room as room_no",
|
127
|
+
"floor as floor",
|
128
|
+
"bldg_code__ as building_no",
|
129
|
+
]
|
130
|
+
)
|
131
|
+
table = "ARADMIN1.UMNET_EQUIPMENT eq"
|
126
132
|
|
127
133
|
sql = self._build_select(select, table, where=where)
|
128
134
|
return self._execute(sql)
|
129
135
|
|
130
136
|
def get_all_devices(self):
|
131
|
-
select = [
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
137
|
+
select = [
|
138
|
+
"bldg_code__ as bldg_code",
|
139
|
+
"bldg",
|
140
|
+
"room",
|
141
|
+
"ip_address",
|
142
|
+
"dns_name",
|
143
|
+
"category",
|
144
|
+
"types",
|
145
|
+
"manufacturer",
|
146
|
+
"serial_number",
|
147
|
+
"billing_code_ as billing_code",
|
148
|
+
"warehouse_item__ as warehouse_item",
|
149
|
+
"st.descr as status",
|
150
|
+
"sla_network",
|
151
|
+
"address",
|
152
|
+
"floor",
|
153
|
+
"model_no_ as model_no",
|
154
|
+
"customer_name",
|
155
|
+
"mat_l_item_description",
|
156
|
+
"rancid",
|
157
|
+
"off_line",
|
158
|
+
]
|
159
|
+
table = "ARADMIN1.UMNET_EQUIPMENT eq"
|
153
160
|
joins = ["join ARADMIN1.UMNET_EQUIPMENT_STATUS st on st.idnum=eq.status"]
|
154
161
|
where = ["eq.status != 3"]
|
155
162
|
|
156
|
-
sql = self._build_select(select, table,where=where, joins=joins)
|
157
|
-
return self._execute(sql)
|
163
|
+
sql = self._build_select(select, table, where=where, joins=joins)
|
164
|
+
return self._execute(sql)
|
umnetdb_utils/umnetinfo.py
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
1
|
import ipaddress
|
4
2
|
import re
|
5
3
|
from .base import UMnetdbBase
|
6
4
|
from .utils import is_ip_address, is_ip_network, is_mac_address
|
7
5
|
|
6
|
+
|
8
7
|
class UMnetinfo(UMnetdbBase):
|
9
8
|
"""
|
10
9
|
Wraps helpful netinfo db queries into python
|
11
10
|
"""
|
11
|
+
|
12
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
13
|
|
14
14
|
def get_network_by_name(self, netname, active_only=False):
|
@@ -365,7 +365,6 @@ class UMnetinfo(UMnetdbBase):
|
|
365
365
|
|
366
366
|
# dns names in the device table are fqdn and end in a dot (eg 'dl-arbl-1.umnet.umich.edu.')
|
367
367
|
else:
|
368
|
-
|
369
368
|
name_or_ip = name_or_ip.lower()
|
370
369
|
if "." not in name_or_ip:
|
371
370
|
name_or_ip += ".umnet.umich.edu"
|
@@ -417,7 +416,6 @@ class UMnetinfo(UMnetdbBase):
|
|
417
416
|
# UMNET.ARPHIST stores MACs as strings without separators .:-, ie
|
418
417
|
# 0010.abcd.1010 => '0010abcd1010'
|
419
418
|
if is_mac_address(query):
|
420
|
-
|
421
419
|
arphist_mac = re.sub(r"[\.\:\-]", "", query)
|
422
420
|
where.append(f"arp.mac_addr = '{arphist_mac}'")
|
423
421
|
|
@@ -456,7 +454,6 @@ class UMnetinfo(UMnetdbBase):
|
|
456
454
|
# isn't throwing anyone :-/
|
457
455
|
processed_results = []
|
458
456
|
for r in results:
|
459
|
-
|
460
457
|
processed = {
|
461
458
|
"ip": ipaddress.ip_address(r["address32bit"]),
|
462
459
|
"mac": f"{r['mac_addr'][0:4]}.{r['mac_addr'][4:8]}.{r['mac_addr'][8:12]}",
|
@@ -486,4 +483,4 @@ class UMnetinfo(UMnetdbBase):
|
|
486
483
|
sql = self._build_select(select, table, where=where, order_by="1,2")
|
487
484
|
results = self._execute(sql)
|
488
485
|
|
489
|
-
return results
|
486
|
+
return results
|