/[marcuscom]/switchports-reports/sw-reports.py
ViewVC logotype

Contents of /switchports-reports/sw-reports.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 20466 - (show annotations) (download) (as text)
Thu Aug 17 18:22:22 2017 UTC (2 years, 3 months ago) by jclarke
File MIME type: text/x-python
File size: 13569 byte(s)
Make friendlier for Python 3.

1 #!/usr/bin/env python
2 #-
3 # Copyright (c) 2015 Joe Clarke <jclarke@cisco.com>
4 # All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 # 1. Redistributions of source code must retain the above copyright
10 # notice, this list of conditions and the following disclaimer.
11 # 2. Redistributions in binary form must reproduce the above copyright
12 # notice, this list of conditions and the following disclaimer in the
13 # documentation and/or other materials provided with the distribution.
14 #
15 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24 #
25 # $MCom$
26
27 import sys
28 import re
29 import requests
30 from requests import Request, Session
31 import json
32 try:
33 import urllib.parse as ul
34 except ImportError:
35 import urllib as ul
36 import time
37 import paramiko
38 import sqlite3
39 import calendar
40 import time
41 import argparse
42
43 DEBUG = 0
44 REST_TIMEOUT = 300
45 REST_RETRY_INTERVAL = 1
46 REST_RETRIES = 3
47 REST_PAGE_LIMIT_MI = 100
48 REST_PAGE_LIMIT = 500
49 SSH_TIMEOUT = 60
50
51 class SwReports:
52 pass
53
54 class RestError:
55 code = -1
56 msg = ""
57
58 def set_code(self, code):
59 self.code = code
60
61 def set_msg(self, msg):
62 self.msg = msg
63
64 def get_code(self):
65 return self.code
66
67 def get_msg(self):
68 return self.msg
69
70 def fetch_url(url, error, p=False, timeout=REST_TIMEOUT):
71 global DEBUG
72
73 s = Session()
74 requests.packages.urllib3.disable_warnings()
75 req = 'GET'
76 data = {}
77 headers = {}
78
79 if 'get' in p:
80 url += '?' + ul.urlencode(p['get'])
81 if 'post' in p:
82 req = 'POST'
83 data = p['post']
84 if 'put' in p:
85 req = 'PUT'
86 if 'header' not in p:
87 p['header'] = {}
88 p['header']['Content-Length'] = len(p['put'])
89 data = p['put']
90 if 'header' in p:
91 headers = p['header']
92
93 req = Request(req, url, data=data, headers=headers)
94 ph = req.prepare()
95
96 if DEBUG > 0:
97 print("DEBUG: Requested URL " + url)
98 print("DEBUG: Headers = ")
99 for h in ph.headers:
100 print(" " + h + " : " + ph.headers[h])
101 print("DEBUG: Body = " + str(ph.body))
102
103 res = s.send(ph, verify=False, timeout=timeout)
104
105 if res.status_code > 299:
106 if DEBUG > 0:
107 print("DEBUG: The server returned code: " + str(res.status_code) + " response: " + res.text)
108 print("DEBUG: Response Headers: ")
109 for h in res.headers:
110 print(" " + h + " : " + res.headers[h])
111 error.set_code(res.status_code)
112 j = res.json()
113 error.set_msg(j['response']['message'])
114 return None
115 else:
116 return res.text
117
118 def get_ticket(host, user, password, error):
119 global REST_RETRIES, REST_RETRY_INTERVAL
120
121 url = "https://" + host + "/api/v1/ticket"
122 p = {}
123
124 p['post'] = json.dumps({"username":user,"password":password})
125 p['header'] = {"Content-Type":"application/json"}
126
127 i = 0
128 res = None
129 while i < REST_RETRIES:
130 res = fetch_url(url, error, p)
131 if res is not None:
132 break
133 time.sleep(REST_RETRY_INTERVAL)
134 i = i + 1
135 if res is not None:
136 j = json.loads(res)
137 return j['response']['serviceTicket']
138
139 return None
140
141
142 def get_devices(host, ticket, error):
143 global REST_RETRIES, REST_RETRY_INTERVAL
144
145 url = "https://" + host + "/api/v1/network-device"
146 p = {}
147
148 p['header'] = {"X-Auth-Token":ticket}
149
150 i = 0
151 res = None
152 while i < REST_RETRIES:
153 res = fetch_url(url, error, p)
154 if res is not None:
155 break
156 time.sleep(REST_RETRY_INTERVAL)
157 i = i + 1
158 if res is not None:
159 j = json.loads(res)
160 return j['response']
161
162 return None
163
164 def get_interface_count(host, ticket, devid, error):
165 global REST_RETRIES, REST_RETRY_INTERVAL
166
167 url = "https://" + host + "/api/v1/interface/network-device/" + devid + "/count"
168 p = {}
169 p['header'] = {"X-Auth-Token":ticket}
170
171 i = 0
172 res = None
173 while i < REST_RETRIES:
174 res = fetch_url(url, error, p)
175 if res is not None:
176 break
177 time.sleep(REST_RETRY_INTERVAL)
178 i = i + 1
179 if res is not None:
180 j = json.loads(res)
181 return j['response']
182
183 return None
184
185 def get_interfaces(host, ticket, devid, error):
186 global REST_RETRIES, REST_RETRY_INTERVAL, REST_PAGE_LIMIT
187
188 url = "https://" + host + "/api/v1/interface/network-device/" + devid
189 p = {}
190 p['header'] = {"X-Auth-Token":ticket}
191
192 count = get_interface_count(host, ticket, devid, error)
193 if count is None:
194 return None
195
196 offset = 1
197 limit = REST_PAGE_LIMIT
198 intfs = []
199 while True:
200 iurl = url
201 iurl += "/" + str(offset) + "/" + str(limit)
202
203 i = 0
204 res = None
205 while i < REST_RETRIES:
206 res = fetch_url(iurl, error, p)
207 if res is not None:
208 break
209 time.sleep(REST_RETRY_INTERVAL)
210 i = i + 1
211 if res is not None:
212 j = json.loads(res)
213 intfs.extend(j['response'])
214
215 offset = offset + limit
216 if offset + limit >= count:
217 break
218
219 return intfs
220
221 def get_device_count(host, ticket, error):
222 global REST_RETRIES, REST_RETRY_INTERVAL
223
224 url = "https://" + host + "/api/v1/network-device/count"
225 p = {}
226 p['header'] = {"X-Auth-Token":ticket}
227
228 i = 0
229 res = None
230 while i < REST_RETRIES:
231 res = fetch_url(url, error, p)
232 if res is not None:
233 break
234 time.sleep(REST_RETRY_INTERVAL)
235 i = i + 1
236 if res is not None:
237 j = json.loads(res)
238 return j['response']
239
240 return None
241
242 def get_device_creds(host, ticket, devid, error):
243 global REST_RETRIES, REST_RETRY_INTERVAL, REST_PAGE_LIMIT_MI
244
245 url = "https://" + host + "/api/v1/network-device/management-info"
246 p = {}
247 p['header'] = {"X-Auth-Token":ticket}
248
249 count = get_device_count(host, ticket, error)
250 if count is None:
251 return None
252
253 offset = 1
254 limit = REST_PAGE_LIMIT_MI
255 p['get'] = {}
256 while True:
257 p['get']['offset'] = offset
258 p['get']['limit'] = limit
259
260 i = 0
261 res = None
262 while i < REST_RETRIES:
263 res = fetch_url(url, error, p)
264 if res is not None:
265 break
266 time.sleep(REST_RETRY_INTERVAL)
267 i = i + 1
268 if res is not None:
269 j = json.loads(res)
270 minfo = j['response']['networkDeviceManagementInfo']
271 marr = []
272 marr.extend(minfo)
273 for dev in marr:
274 if dev['id'] == devid:
275 if 'cli_transport' in dev['credentials']:
276 if dev['credentials']['cli_transport'] != 'ssh2':
277 error.set_msg("Transport is not 'ssh2'")
278 return None
279 if 'cli_enable_password' in dev:
280 return (dev['credentials']['cli_login_username'], dev['credentials']['cli_login_password'], dev['credentials']['cli_enable_password'])
281 else:
282 return (dev['credentials']['cli_login_username'], dev['credentials']['cli_login_password'], "")
283 else:
284 error.set_msg("No cli credentials found for device")
285 return None
286 offset = offset + limit
287 if offset + limit >= count:
288 break
289
290 return None
291
292 if __name__ == '__main__':
293 error = RestError()
294 swr = SwReports()
295 parser = argparse.ArgumentParser(prog='sw-reports.py', description='Track those ports that are administratively up, but operationally down in order to find unsued capacity.')
296 parser.add_argument('--apic-em-hostname', '-H', dest='apic_em', metavar='<HOSTNAME>', help='Hostname of the APIC-EM controller', required=True)
297 parser.add_argument('--apic-em-username', '-U', dest='username', metavar='<USERNAME>', help='APIC-EM API username', required=True)
298 parser.add_argument('--apic-em-password', '-P', dest='password', metavar='<PASSWORD>', help='APIC-EM API password', required=True)
299 parser.add_argument('--threshold', '-t', dest='threshold', metavar='<threshold>', type=int, default=30, help='Number of days before a port is considered reclaimable (default: 30)')
300 parser.add_argument('--database-file', '-b', dest='db_file', metavar='<db file>', default='switch_port.db', help='Path to the SQLite database file used for tracking port states (default: switch_port.db)')
301 parser.add_argument('--output-file', '-o', dest='output_file', metavar='<output file>', default='reclaimable_ports.csv', help='Path to the output file into which reclaimable ports will be written (default: reclaimable_ports.csv')
302 parser.parse_args(namespace=swr)
303 ticket = get_ticket(swr.apic_em, swr.username, swr.password, error)
304 if ticket is None:
305 print("Error obtaining RBAC ticket: " + error.get_msg())
306 sys.exit(1)
307 devs = get_devices(swr.apic_em, ticket, error)
308 if devs is None:
309 print("Error retrieving devices: " + error.get_msg())
310 sys.exit(1)
311 con = sqlite3.connect(swr.db_file)
312 cur = con.cursor()
313 cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='ports'")
314 row = cur.fetchone()
315 if row is None:
316 cur.execute("CREATE TABLE ports (name TEXT, ts INT)")
317 con.commit()
318 now = calendar.timegm(time.gmtime())
319 tt = swr.threshold * 60 * 60 * 24
320 reclaimable = []
321 for dev in devs:
322 if dev['interfaceCount'] == 0:
323 continue
324 check_intfs = []
325 intfs = get_interfaces(swr.apic_em, ticket, dev['instanceUuid'], error)
326 if intfs is None:
327 print("Failed to get interfaces for " + dev['hostname'] + ": " + error.get_msg())
328 continue
329 for intf in intfs:
330 if intf['portType'] != 'Ethernet Port':
331 continue
332 if intf['portMode'] != 'access' or intf['status'] == 'up':
333 cur.execute("DELETE FROM ports WHERE name = ?", (intf['portName'],))
334 con.commit
335 continue
336 check_intfs.append(intf['portName'])
337 if len(check_intfs) > 0:
338 creds = get_device_creds(swr.apic_em, ticket, dev['instanceUuid'], error)
339 if creds is None:
340 print("Failed to get credentials for " + dev['hostname'] + ": " + error.get_msg())
341 continue
342 output = ""
343 try:
344 ssh_client = paramiko.SSHClient()
345 ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
346 ssh_client.connect(dev['managementIpAddress'], username=creds[0], password=creds[1], timeout=SSH_TIMEOUT, allow_agent=False, look_for_keys=False)
347 chan = ssh_client.invoke_shell()
348 chan.sendall("term length 0\n")
349 chan.sendall("show ip int brief\n")
350 i = 0
351 while i < 10:
352 if chan.recv_ready():
353 break
354 time.sleep(.5)
355 i = i + 1
356 while chan.recv_ready():
357 output = output + chan.recv(65535).decode('ascii')
358 ssh_client.close()
359 except Exception as e:
360 ssh_client.close()
361 print("Failed to get port status from " + dev['hostname'] + ": ", e)
362 continue
363 for line in output.splitlines():
364 m = re.match(r"^([^\s]+)\s", line)
365 if m is None:
366 continue
367 sintf = m.group(1)
368 if sintf in check_intfs:
369 m = re.search("administratively down", line, re.I)
370 if m is not None:
371 cur.execute("DELETE FROM ports WHERE name = ?", (intf['portName'],))
372 con.commit()
373 continue
374 m = re.search(r"\bdown\b", line)
375 if m is not None:
376 cur.execute("SELECT ts FROM ports WHERE name = ?", (sintf,))
377 row = cur.fetchone()
378 if row is not None:
379 if int(now) - int(row[0]) >= tt:
380 reclaimable.append((dev['hostname'], sintf, row[0]))
381 else:
382 cur.execute("INSERT INTO ports VALUES (?, ?)", (sintf, int(now)))
383 con.commit()
384
385 with open (swr.output_file, 'w') as fd:
386 fd.write("Hostname,Port Name,Down Since\n")
387 for entry in reclaimable:
388 hn, pn, ts = entry
389 fd.write(hn + "," + pn + "," + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts)) + "\n")
390
391 if con:
392 con.close()
393

Properties

Name Value
mcom:keywords yes
svn:executable

  ViewVC Help
Powered by ViewVC 1.1.27