diff --git a/bw_check.py b/bw_check.py index e1bd018..9392a40 100644 --- a/bw_check.py +++ b/bw_check.py @@ -1,9 +1,27 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + #!/usr/bin/env python3 # # Script to show bandwidth usage by client over a definable threshold # -# The 5 minutes averages seem to poorly catch the peaks that you might see on the +# The 5 minutes averages seem to poorly catch the peaks that you might see on the # dashboard graph. # @@ -33,10 +51,10 @@ import time from datetime import datetime -tracking = 'rx_bytes' # download = tx_bytes / upload = rx_bytes -threshold = (1*1024*1024)/8 # 1.5 mbps +tracking = 'rx_bytes' # download = tx_bytes / upload = rx_bytes +threshold = (1 * 1024 * 1024) / 8 # 1.5 mbps interval = '5minutes' -interval_sec = 5*60 # 300 seconds in 5 mintues to calculate bandwidth +interval_sec = 5 * 60 # 300 seconds in 5 mintues to calculate bandwidth print("Logging into controller") c = controller() @@ -45,6 +63,7 @@ print("Fetching and processing client lists") clients = s.clients() + def best_name(client): if 'name' in client: return "{name}".format(**client) @@ -52,10 +71,11 @@ def best_name(client): return "{hostname}".format(**client) return "UKN ({mac})".format(**client) -mac_to_name = dict(( (x['mac'], best_name(x)) for x in clients )) -end = time.time()*1000 -start = end-(60*60*24*1000) +mac_to_name = dict(((x['mac'], best_name(x)) for x in clients)) + +end = time.time() * 1000 +start = end - (60 * 60 * 24 * 1000) print("Fetching bandwidth per user report") bandwidth_per_user = s.user_report(interval=interval, end=end, start=start) @@ -68,13 +88,16 @@ def best_name(client): # Let's filter our records for ones above our threshold for record in bandwidth_per_user: - if record[tracking] > ( threshold * interval_sec): + if record[tracking] > (threshold * interval_sec): users_per_time[record['time']].append(record) for timestamp in sorted(timestamps): if users_per_time[timestamp]: - print(datetime.fromtimestamp(timestamp/1000).strftime('%m-%d %I:%M %p')) + print( + datetime.fromtimestamp( + timestamp / + 1000).strftime('%m-%d %I:%M %p')) for user in users_per_time[timestamp]: - speed = int((user[tracking]/interval_sec)/1024)*8 + speed = int((user[tracking] / interval_sec) / 1024) * 8 name = mac_to_name[user['user']] print(name, speed, "kbps") diff --git a/extract_dpi.py b/extract_dpi.py index f597a0b..eed07b8 100644 --- a/extract_dpi.py +++ b/extract_dpi.py @@ -1,3 +1,21 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + #!/usr/bin/env python3 @@ -6,18 +24,26 @@ import re data = open(sys.argv[1]).read() -#print(data) +# print(data) applications = {} -for app in re.findall('\d+:{.*?}',re.search('applications:{(.*?)}}',data).group(1)): - foo = re.search('(.+):{name:\"(.*?)\"',app) - #print(app) +for app in re.findall( + r'\d+:{.*?}', + re.search( + 'applications:{(.*?)}}', + data).group(1)): + foo = re.search('(.+):{name:\"(.*?)\"', app) + # print(app) applications[foo.group(1)] = foo.group(2) - + categories = {} -for cat in re.findall('\d+:{.*?}',re.search('categories:{(.*?})}',data).group(1)): - foo = re.search('(.+):{name:\"(.*?)\"',cat) - #print(cat) +for cat in re.findall( + r'\d+:{.*?}', + re.search( + 'categories:{(.*?})}', + data).group(1)): + foo = re.search('(.+):{name:\"(.*?)\"', cat) + # print(cat) categories[foo.group(1)] = foo.group(2) - -print(json.dumps({"categories":categories,"applications":applications})) + +print(json.dumps({"categories": categories, "applications": applications})) diff --git a/influx.py b/influx.py index b782164..f9638d1 100644 --- a/influx.py +++ b/influx.py @@ -1,3 +1,21 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + from influxdb import InfluxDBClient from unifiapi import controller, UnifiApiError from random import randint @@ -25,7 +43,7 @@ 'site': site_name, 'desc': c.sites[site_name]['desc'] } -except: +except BaseException: print("Could not generate site default tags") client.create_database('unifi') @@ -36,46 +54,71 @@ last_cat_dpi = 0 dups = {} + + def is_dup(point): cts = time() - _id = [ point['measurement'] ] - _id.extend( point['tags'].values() ) + _id = [point['measurement']] + _id.extend(point['tags'].values()) xid = '-'.join(_id) - yid = '-'.join( map(str, point['fields'].values()) ) + yid = '-'.join(map(str, point['fields'].values())) if xid in dups: - ( ts, cyid ) = dups[xid] - if cyid == yid and (cts-ts)<60: + (ts, cyid) = dups[xid] + if cyid == yid and (cts - ts) < 60: return True dups[xid] = (cts, yid) return False + def time_str(dt): return dt.strftime('%Y-%m-%dT%H:%M:%SZ') + def dev_to_measures(dev): - for field in ['rx_bytes', 'rx_packets', 'rx_dropped', 'rx_errors', 'tx_bytes','tx_packets', 'tx_dropped', 'tx_errors']: + for field in [ + 'rx_bytes', + 'rx_packets', + 'rx_dropped', + 'rx_errors', + 'tx_bytes', + 'tx_packets', + 'tx_dropped', + 'tx_errors']: try: yield dev['mac'], field, dev['uplink'][field] - except: + except BaseException: pass yield dev['mac'], 'num_sta', dev['num_sta'] + def client_markup(client, devs): if 'ap_mac' in client: # try to get the name try: - ap = devs.filter_by('mac',client['ap_mac']) + ap = devs.filter_by('mac', client['ap_mac']) client.data['ap_name'] = ap[0]['name'] - except: + except BaseException: pass + def client_to_measures(client): - for field in ['rx_bytes', 'rx_packets', 'tx_bytes','tx_packets', 'ap_name', 'essid', 'rssi', 'rx_rate', 'tx_rate', 'channel']: + for field in [ + 'rx_bytes', + 'rx_packets', + 'tx_bytes', + 'tx_packets', + 'ap_name', + 'essid', + 'rssi', + 'rx_rate', + 'tx_rate', + 'channel']: try: yield client['mac'], field, client[field] - except: + except BaseException: pass + def client_best_name(client): if 'name' in client: return client['name'] @@ -83,6 +126,7 @@ def client_best_name(client): return client['hostname'] return "UNKN-{}".format(client['mac']) + while True: json = [] ts = datetime.utcnow() @@ -98,14 +142,16 @@ def client_best_name(client): 'name': dev['name'], 'mac': dev['mac'], 'type': dev['type'], - }, + }, 'time': time_str(ts), 'fields': {} - } - for mac,field,value in dev_to_measures(dev): - cur_value, cur_ts = current_data.get((mac,field), [0,datetime.fromtimestamp(10000)]) - if value != cur_value or (ts-cur_ts).total_seconds() > 30+randint(0,10): - current_data[(mac,field)] = [value, ts] + } + for mac, field, value in dev_to_measures(dev): + cur_value, cur_ts = current_data.get( + (mac, field), [0, datetime.fromtimestamp(10000)]) + if value != cur_value or ( + ts - cur_ts).total_seconds() > 30 + randint(0, 10): + current_data[(mac, field)] = [value, ts] temp_json['fields'][field] = value if temp_json['fields']: json.append(temp_json) @@ -117,22 +163,22 @@ def client_best_name(client): 'name': dev['name'], 'mac': dev['mac'], 'type': dev['type'], - }, + }, 'time': time_str(ts), 'fields': {} - } + } try: temp_json['fields']['cpu'] = float(dev['system-stats']['cpu']) temp_json['fields']['mem'] = float(dev['system-stats']['mem']) - except: + except BaseException: pass try: temp_json['fields']['temp'] = float(dev['general_temperature']) - except: + except BaseException: pass try: temp_json['fields']['fan_level'] = int(dev['fan_level']) - except: + except BaseException: pass if len(temp_json['fields']) and not is_dup(temp_json): json.append(temp_json) @@ -140,64 +186,65 @@ def client_best_name(client): # client activity clients = s.active_clients() for cli in clients: - client_markup(cli,devs) + client_markup(cli, devs) temp_json = { 'measurement': 'client', 'tags': { 'name': client_best_name(cli), 'mac': cli['mac'], - }, + }, 'time': time_str(ts), 'fields': {} - } - for mac,field,value in client_to_measures(cli): - cur_value, cur_ts = current_data.get((mac,field), [0,datetime.fromtimestamp(10000)]) - if value != cur_value or (ts-cur_ts).total_seconds() > 60+randint(0,30): - current_data[(mac,field)] = [value, ts] + } + for mac, field, value in client_to_measures(cli): + cur_value, cur_ts = current_data.get( + (mac, field), [0, datetime.fromtimestamp(10000)]) + if value != cur_value or ( + ts - cur_ts).total_seconds() > 60 + randint(0, 30): + current_data[(mac, field)] = [value, ts] temp_json['fields'][field] = value if temp_json['fields']: print(temp_json) json.append(temp_json) - # Site DPI dpi = s.dpi(type='by_app') dpi[0].translate() for row in dpi[0]['by_app']: tags = { - 'appid': (row['cat']<<16)+row['app'], - 'application': row['application'], - } - fields = { - 'category': row['category'], - 'rx_bytes': row['rx_bytes'], - 'rx_packets': row['rx_packets'], - 'tx_bytes': row['tx_bytes'], - 'tx_packets': row['tx_packets'], - } + 'appid': (row['cat'] << 16) + row['app'], + 'application': row['application'], + } + fields = { + 'category': row['category'], + 'rx_bytes': row['rx_bytes'], + 'rx_packets': row['rx_packets'], + 'tx_bytes': row['tx_bytes'], + 'tx_packets': row['tx_packets'], + } cur_field = current_data.get(tuple(tags.items()), {}) if tuple(fields.items()) != tuple(cur_field.items()): - #print(tags,fields) + # print(tags,fields) current_data[tuple(tags.items())] = fields json.append({ 'time': time_str(ts), 'measurement': 'dpi_site_by_app', 'tags': tags, 'fields': fields, - }) + }) except UnifiApiError: print("exception in controller, wait 30 and try logging in again") sleep(30) c = controller(profile=profile) s = c.sites[site_name]() - except: + except BaseException: print("exception in gather") pass - + if json: while not client.write_points(json, tags=default_tags): # keep trying every second to post results sleep(1) - #pprint(json) + # pprint(json) print(ts) sleep(10) diff --git a/reset_dpi.py b/reset_dpi.py index 176f2d0..8af42a0 100644 --- a/reset_dpi.py +++ b/reset_dpi.py @@ -1,3 +1,21 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + #!/usr/bin/env python from unifiapi import controller @@ -20,4 +38,3 @@ print("Re-enabling DPI") settings['dpi']['enabled'] = True settings['dpi'].update() - diff --git a/setup.py b/setup.py index 53a8145..4805660 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,24 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + from setuptools import setup + def readme(): with open('README.rst') as f: return f.read() @@ -10,12 +29,12 @@ def readme(): description='Bare-bones json interaction with Ubiquiti controllers', long_description=readme(), classifiers=[ - 'Development Status :: 3 - Alpha', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7.5', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', + 'Development Status :: 3 - Alpha', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7.5', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', ], keywords='UBNT controller unifi', url='http://github.com/brontide/unifiapi', @@ -28,8 +47,8 @@ def readme(): 'PyYAML', 'future', ], -# entry_points={ -# 'console_scripts': ['unificmd=unifiapi.cmd:main'], -# }, + # entry_points={ + # 'console_scripts': ['unificmd=unifiapi.cmd:main'], + # }, include_package_data=True, zip_safe=False) diff --git a/sync_backups.py b/sync_backups.py index 3fb6575..6a7c655 100644 --- a/sync_backups.py +++ b/sync_backups.py @@ -1,3 +1,21 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + #!/usr/bin/env python3 # @@ -33,6 +51,6 @@ print("Copying {} to {}".format(backup['filename'], full_file)) try: full_file.write_bytes(backup.download().read()) - except: + except BaseException: # Oops, delete file that could be only partly complete full_file.unlink() diff --git a/sync_firewalllist.py b/sync_firewalllist.py index f50b9c0..eef0e46 100644 --- a/sync_firewalllist.py +++ b/sync_firewalllist.py @@ -1,16 +1,36 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + #!/usr/bin/env python3 # # Script pulls in IP address/ranges from these URLs and updates the list on the unifi controller to match -# +# import unifiapi import requests +import json sync_list = { - 'Spamhaus EDROP': 'https://www.spamhaus.org/drop/edrop.txt', - 'Emerging Threats': 'http://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt', + 'Spamhaus_EDROP': 'https://www.spamhaus.org/drop/edrop.txt', + 'Emerging_Threats': 'http://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt', 'TOR Exit Nodes': 'https://check.torproject.org/cgi-bin/TorBulkExitList.py?ip=1.1.1.1', + 'Bad_Packets_List': 'https://raw.githubusercontent.com/tg12/bad_packets_blocklist/master/bad_packets_list.txt' } @@ -19,14 +39,19 @@ def download_ips(url): out = requests.get(url, stream=True) out.raise_for_status() for line in out.iter_lines(decode_unicode=True): - if not line or not line[0].isdigit(): + if not line or not str(line[0]).isdigit(): continue candidate = line.split()[0] if candidate: yield candidate + def new_firewall_group(name, list_members, group_type='address-group'): - return {'name': name, 'group_type': group_type, 'group_members': list(list_members) } + return json.dumps({ + 'name': name, + 'group_type': group_type, + 'group_members': list(list_members)}) + print("Logging into controller") c = unifiapi.controller() @@ -38,14 +63,19 @@ def new_firewall_group(name, list_members, group_type='address-group'): print(f'Syncing {list_name}') list_ips = sorted(set((download_ips(url)))) try: - curfw = fwg[list_name] # this will raise KeyError and fall back to adding the firewall + # this will raise KeyError and fall back to adding the firewall + curfw = fwg[list_name] curips = sorted(set(curfw['group_members'])) if curips == list_ips: - print("Found IDENTICAL existing list {} with {} members - download list has {} members".format(list_name, len(curips), len(list_ips))) + print( + "Found IDENTICAL existing list {} with {} members - download list has {} members".format( + list_name, + len(curips), + len(list_ips))) else: print("List has changed, updating") curfw['group_members'] = list_ips - curfw.update() # Update the record. + curfw.update() # Update the record. except KeyError: print("No list {} found, adding".format(list_name)) - r = s.firewallgroups(**new_firewall_group(list_name, list_ips)) + r = s.firewallgroups(new_firewall_group(list_name, list_ips)) diff --git a/unifiapi/__init__.py b/unifiapi/__init__.py index 44c9540..c24491c 100644 --- a/unifiapi/__init__.py +++ b/unifiapi/__init__.py @@ -1,4 +1,21 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + from unifiapi.api import controller, quiet, DEVICES, DPI, cat_app_to_dpi, dpi_to_cat_app, UnifiApiError #__all__ = [ "controller" ] - diff --git a/unifiapi/api.py b/unifiapi/api.py index 6307fcd..5e82fe5 100644 --- a/unifiapi/api.py +++ b/unifiapi/api.py @@ -1,3 +1,21 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + ''' Client tools for Unifi restful api ''' @@ -7,6 +25,18 @@ # print function from __future__ import print_function +import requests +import pkg_resources +import json +import yaml +from json import dumps +from collections import UserList, UserDict +import time +from urllib.parse import urlparse, quote +import sys +import os +from getpass import getpass, getuser +import logging # This is a hack to allow partialmethod on py2 @@ -37,53 +67,54 @@ def __get__(self, instance, owner): # END py2 compatibility cruft # + def quiet(): ''' This function turns off InsecureRequestWarnings ''' try: # old vendored packages - requests.packages.urllib3.disable_warnings() #pylint: disable=E1101 - except: + requests.packages.urllib3.disable_warnings() # pylint: disable=E1101 + except BaseException: # New way import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - -import requests -import logging -from getpass import getpass, getuser -import os -import sys -from urllib.parse import urlparse,quote -import time -from collections import UserList, UserDict -from json import dumps -import yaml -import json -import pkg_resources - logger = logging.getLogger(__name__) + def jsonKeys2int(x): if isinstance(x, dict): - try: - return {int(k):v for k,v in x.items()} - except: - pass + try: + return {int(k): v for k, v in x.items()} + except BaseException: + pass return x + def cat_app_to_dpi(cat, app): ''' convert numeric category and app codes to dpi_id for lookup in the application list ''' - return (int(cat)<<16)+int(app) + return (int(cat) << 16) + int(app) + def dpi_to_cat_app(dpi_id): ''' convert dpi_id to category and app codes ''' - return int(dpi_id)>>16, int(dpi_id)&65536 + return int(dpi_id) >> 16, int(dpi_id) & 65536 + # FIXME: should be in data/ -DEVICES = json.load(open(pkg_resources.resource_filename('unifiapi','unifi_devices.json'))) -DPI = json.load(open(pkg_resources.resource_filename('unifiapi','unifi_dpi.json')), object_hook=jsonKeys2int) +DEVICES = json.load( + open( + pkg_resources.resource_filename( + 'unifiapi', + 'unifi_devices.json'))) +DPI = json.load( + open( + pkg_resources.resource_filename( + 'unifiapi', + 'unifi_dpi.json')), + object_hook=jsonKeys2int) + def get_username_password(endpoint, username=None): # Query for interactive credentials @@ -97,10 +128,9 @@ def get_username_password(endpoint, username=None): # if no username was supplied use the logged in username username = getuser() def_username = username - # Start interactive login - username = input( + username = input( "Please enter credentials for Unifi {}\nUsername (CR={}): ".format( endpoint, username)) @@ -108,12 +138,18 @@ def get_username_password(endpoint, username=None): if username == "": # User hit enter, use default username = def_username - + password = getpass("{} Password : ".format(username), sys.stderr) return username, password -def controller(profile=None, endpoint=None, username=None, password=None, verify=None): + +def controller( + profile=None, + endpoint=None, + username=None, + password=None, + verify=None): ''' Controller factory gived a profile or endpoint, username, password will return a controller object. If profile and endpoint are both None the function will automatically try the default profile config ''' @@ -125,12 +161,14 @@ def controller(profile=None, endpoint=None, username=None, password=None, verify # should go into **kwargs. Username and password # into self profile_config = {} - for filename in ('unifiapi.yaml', os.path.expanduser('~/.unifiapi_yaml')): + for filename in ( + 'unifiapi.yaml', + os.path.expanduser('~/.unifiapi_yaml')): try: profile_config = yaml.safe_load(open(filename))[profile] logger.debug('Found config for profile %s', profile) break - except: + except BaseException: pass endpoint = profile_config['endpoint'] if not username: @@ -142,12 +180,16 @@ def controller(profile=None, endpoint=None, username=None, password=None, verify # Finished loading profile defaults if verify is None: verify = True - + if not username or not password: # If we don't have full credentials, get them username, password = get_username_password(endpoint, username) - logger.debug("Attempting to login to endpoint %s with username %s and verify %s", endpoint, username, repr(verify)) + logger.debug( + "Attempting to login to endpoint %s with username %s and verify %s", + endpoint, + username, + repr(verify)) c = UnifiController(endpoint=endpoint, verify=verify) c.login(username, password) @@ -157,6 +199,7 @@ def controller(profile=None, endpoint=None, username=None, password=None, verify class UnifiError(Exception): pass + class UnifiApiError(UnifiError): ''' Wapper around UniFi errors which attempts to pull the error from the json if possible ''' @@ -164,20 +207,22 @@ def __init__(self, out): self.request_response = out try: data = out.json() - UnifiError.__init__(self, "Error {} when connecting to url {}".format( - data['meta']['msg'], out.url)) + UnifiError.__init__( + self, "Error {} when connecting to url {}".format( + data['meta']['msg'], out.url)) except BaseException: UnifiError.__init__( self, "URL: {} status code {}".format( out.url, out.status_code)) + class UnifiData(UserDict): def __init__(self, session, call, data): self._client = session self.data = data - if not '_id' in data: + if '_id' not in data: self._path = None elif 'key' in data: self._path = '/'.join([call, data['key'], data['_id']]) @@ -203,12 +248,14 @@ def delete(self): def endpoint(self): return self._path + class UniFiClientData(UnifiData): def delete(self): ''' In order to "delete" a client we're actually forgetting it ''' return self._client.c_forget_client(mac=self.data['mac']) + class UnifiDeviceData(UnifiData): def __init__(self, *args, **kwargs): @@ -224,6 +271,7 @@ def force_provision(self): ''' force provision this device ''' return self._client.c_force_provision(mac=self.data['mac']) + class UnifiDynamicDNSData(UnifiData): def __init__(self, *args, **kwargs): @@ -231,19 +279,24 @@ def __init__(self, *args, **kwargs): # This is a confused endpoint that posts somewhere else self._stat_to_rest() + class UnifiSiteData(UnifiData): def to_site(self): try: - return self._site #pylint: disable=E0203 - except: pass - self._site = UnifiSite(session = self._client._s, - endpoint = '/'.join([self._client.endpoint, 'api/s', self.data['name']])) + return self._site # pylint: disable=E0203 + except BaseException: + pass + self._site = UnifiSite(session=self._client._s, + endpoint='/'.join([self._client.endpoint, + 'api/s', + self.data['name']])) return self._site - + def __call__(self): return self.to_site() + class UnifiAutoBackupData(UnifiData): def download(self): @@ -251,7 +304,8 @@ def download(self): Unifi doesn't make this easy since it's relative to the controller and not the site ''' p = urlparse(self._client.endpoint) - url = '{}://{}/dl/autobackup/{}'.format(p.scheme,p.netloc,self.data['filename']) + url = '{}://{}/dl/autobackup/{}'.format( + p.scheme, p.netloc, self.data['filename']) r = self._client._s.get(url, stream=True) return r.raw @@ -259,6 +313,7 @@ def delete(self): ''' Delete the referenced backup file ''' return self._client.c_delete_backup(filename=self.data['filename']) + class UnifiDPIData(UnifiData): def translate(self): @@ -270,9 +325,11 @@ def translate(self): if 'by_app' in self.data: for item in self.data['by_app']: code = cat_app_to_dpi(item['cat'], item['app']) - item['application'] = DPI['applications'].get(code, "Unknown-{}".format(code)) + item['application'] = DPI['applications'].get( + code, "Unknown-{}".format(code)) cat = item['cat'] - item['category'] = DPI['categories'].get(cat, "Unknown-{}".format(cat)) + item['category'] = DPI['categories'].get( + cat, "Unknown-{}".format(cat)) # For some responses we want to monkeypatch some of the calls to make @@ -291,19 +348,21 @@ def translate(self): 'cmd/backup': UnifiAutoBackupData, } + def data_factory(endpoint): return DATA_OVERRIDE.get(endpoint, UnifiData) -def imatch( x, y ): +def imatch(x, y): ''' case insensitive match which folds exceptions into False for simplicity ''' try: return x.lower() == y.lower() - except: + except BaseException: pass return False + class UnifiResponse(UserList): ''' Wrapper around Unifi api return values ''' @@ -312,17 +371,18 @@ def __init__(self, session, call, out): self._client = session self.endpoint = call - + # Identifiy the correct wrapper for the return values # this way we can patch in helpers as needed data_wrapper = data_factory(call) try: self._orig = out.json() - self.data = [ data_wrapper(session, call, x) for x in self._orig['data'] ] - except: + self.data = [data_wrapper(session, call, x) + for x in self._orig['data']] + except BaseException: raise - # In come cases the unifi api will return a result which does not match its + # In come cases the unifi api will return a result which does not match its # count, in these cases the results have been truncated if 'count' in self.meta and len(self.data) != int(self.meta['count']): logger.warning("Truncated API response") @@ -341,20 +401,20 @@ def __init__(self, session, call, out): else: self.keys = set() - for bar in ['key', 'name', 'mac' ]: + for bar in ['key', 'name', 'mac']: if bar in self.keys: - self.values = [ x[bar] for x in self.data ] + self.values = [x[bar] for x in self.data] break - def __getitem__(self, key): ''' Try to act as both list and dict where possible ''' if isinstance(key, int): return self.data[key] - for keying in [ 'key', 'name', 'mac' ]: + for keying in ['key', 'name', 'mac']: if keying in self.keys: foo = self.filter_by(keying, key, unwrap=True) - if foo: return foo + if foo: + return foo raise KeyError("{} not found".format(key)) @property @@ -370,17 +430,23 @@ def is_ok(self): return self.meta['rc'] == 'ok' def filter_by(self, tag, value, unwrap=False): - ret = list(filter(lambda x: x.get(tag,'') == value, self.data)) - if not unwrap: return ret - if not ret: return None - if len(ret) == 1: return ret[0] + ret = list(filter(lambda x: x.get(tag, '') == value, self.data)) + if not unwrap: + return ret + if not ret: + return None + if len(ret) == 1: + return ret[0] raise Exception("Asked to unwrap more than 1 value") def ifilter_by(self, tag, value, unwrap=False): - ret = list(filter(lambda x: imatch(x.get(tag,''), value), self.data)) - if not unwrap: return ret - if not ret: return None - if len(ret) == 1: return ret[0] + ret = list(filter(lambda x: imatch(x.get(tag, ''), value), self.data)) + if not unwrap: + return ret + if not ret: + return None + if len(ret) == 1: + return ret[0] raise Exception("Asked to unwrap more than 1 value") by_name = partialmethod(filter_by, 'name') @@ -388,7 +454,7 @@ def ifilter_by(self, tag, value, unwrap=False): by_type = partialmethod(filter_by, 'type') by_key = partialmethod(filter_by, 'key') - + class UnifiClientBase(object): ''' Bare bones Unifi RESTful client designed for utter simplicity ''' @@ -410,7 +476,7 @@ def __init__( quiet() def __str__(self): - return "{}: {}".format(self.__class__.__name__,self.endpoint) + return "{}: {}".format(self.__class__.__name__, self.endpoint) def request( self, @@ -436,7 +502,7 @@ def request( json=params, stream=stream, params=args) - + logger.debug("Results from %s status %i preview %s", out.url, out.status_code, out.text[:20]) if raise_on_error and out.status_code != requests.codes['ok']: @@ -445,7 +511,7 @@ def request( ret = UnifiResponse(self, endpoint, out) if raise_on_error and not ret.is_ok: raise Exception() - except: + except BaseException: raise UnifiApiError(out) return ret @@ -457,6 +523,7 @@ def request( put = partialmethod(request, 'PUT') delete = partialmethod(request, 'DELETE') + class UnifiController(UnifiClientBase): ''' UnifiController object that contains all of the controller specific calls ''' @@ -468,7 +535,8 @@ def __init__(self, *args, **kwargs): self._sites = None def __str__(self): - return "{}: {} - {}".format(self.__class__.__name__,self.endpoint,self.version) + return "{}: {} - {}".format(self.__class__.__name__, + self.endpoint, self.version) @property def version(self): @@ -495,7 +563,11 @@ def login(self, username=None, password=None, remember=True): If authentication succeeds, it will populate the sites attribute. ''' - ret = self.post('api/login', username=username, password=password, remember=remember) + ret = self.post( + 'api/login', + username=username, + password=password, + remember=remember) return ret def logout(self): @@ -506,11 +578,12 @@ def logout(self): def admins(self): ''' Get a list of admins for this controller ''' return self.get('api/stat/admin') - -class UnifiSite(UnifiClientBase): + + +class UnifiSite(UnifiClientBase): ''' Class to contain all the site specific endpoints and functions ''' - def __init__(self,*args, **kwargs): + def __init__(self, *args, **kwargs): UnifiClientBase.__init__(self, *args, **kwargs) self._ccodes = None self._channels = None @@ -527,21 +600,23 @@ def channels(self): ''' Listing of all RF channels at this site ''' if not self._channels: self._channels = self.get('stat/current-channel') - return self._channels + return self._channels def _api_cmd(self, mgr, command, _req_params='', **params): - ''' Wrapper for calling POST system commands that follow the pattern + ''' Wrapper for calling POST system commands that follow the pattern POST cmd/{manager} with the json {'cmd': '{command-name}'} ''' for param in _req_params: if param not in params: - raise ValueError("{mgr}.{command} requires paramater {param}".format(**locals())) + raise ValueError( + "{mgr}.{command} requires paramater {param}".format( + **locals())) params['cmd'] = command return self.post("cmd/{mgr}".format(mgr=mgr), **params) def mac_by_type(self, unifi_type): ''' find all macs by type, uses the device_basic endpoint for a more ligtweight call ''' - return [ x['mac'] for x in self.devices_basic().by_type(unifi_type) ] + return [x['mac'] for x in self.devices_basic().by_type(unifi_type)] def list_by_type(self, unifi_type): ''' find all devices by type, might be faster on large sites since it's @@ -550,15 +625,36 @@ def list_by_type(self, unifi_type): def __str__(self): health = self.health() - health_str = " ".join(( "{} {}".format(x['subsystem'], x['status']) for x in health )) - return '{}: {} - HEALTH {}'.format(__class__.__name__, self.endpoint, health_str) - - def _report(self, rtype='site', interval='hourly', attrs=None, start=None, end=None, **kwargs): - if rtype not in ['site', 'user', 'ap']: raise ValueError('Type must be site or user') - if interval not in ['5minutes', 'hourly', 'daily']: raise ValueError('Interval must be 5minutes, hourly, daily') + health_str = " ".join( + ("{} {}".format( + x['subsystem'], + x['status']) for x in health)) + return '{}: {} - HEALTH {}'.format(__class__.__name__, + self.endpoint, health_str) + + def _report( + self, + rtype='site', + interval='hourly', + attrs=None, + start=None, + end=None, + **kwargs): + if rtype not in ['site', 'user', 'ap']: + raise ValueError('Type must be site or user') + if interval not in ['5minutes', 'hourly', 'daily']: + raise ValueError('Interval must be 5minutes, hourly, daily') if not attrs: if rtype == 'site': - attrs = ['bytes', 'wan-tx_bytes', 'wan-rx_bytes', 'wlan_bytes', 'num_sta', 'lan-num_sta', 'wlan-num_sta', 'time'] + attrs = [ + 'bytes', + 'wan-tx_bytes', + 'wan-rx_bytes', + 'wlan_bytes', + 'num_sta', + 'lan-num_sta', + 'wlan-num_sta', + 'time'] elif rtype == 'user': attrs = ['rx_bytes', 'tx_bytes', 'time'] elif rtype == 'ap': @@ -569,77 +665,181 @@ def _report(self, rtype='site', interval='hourly', attrs=None, start=None, end=N if end: kwargs['end'] = end - return self.post('stat/report/{}.{}'.format(interval,rtype), **kwargs) + return self.post('stat/report/{}.{}'.format(interval, rtype), **kwargs) user_report = partialmethod(_report, rtype='user') site_report = partialmethod(_report, rtype='site') ap_report = partialmethod(_report, rtype='ap') # Restful commands - alerts = partialmethod(UnifiClientBase.request, 'GET', 'rest/alarm') - events = partialmethod(UnifiClientBase.request, 'GET', 'rest/event') - devices_basic = partialmethod(UnifiClientBase.request, 'GET', 'stat/device-basic') - devices = partialmethod(UnifiClientBase.request, 'GET', 'stat/device') - health = partialmethod(UnifiClientBase.request, 'GET', 'stat/health') - active_clients = partialmethod(UnifiClientBase.request, 'GET', 'stat/sta') - clients = partialmethod(UnifiClientBase.request, 'GET', 'rest/user') - sysinfo = partialmethod(UnifiClientBase.request, 'GET', 'stat/sysinfo') - this_user = partialmethod(UnifiClientBase.request, 'GET', 'self') - settings = partialmethod(UnifiClientBase.request, 'GET', 'rest/setting') - routing = partialmethod(UnifiClientBase.request, 'GET', 'rest/routing') - firewallrules = partialmethod(UnifiClientBase.request, 'GET', 'rest/firewallrule') - firewallgroups = partialmethod(UnifiClientBase.request, 'GET', 'rest/firewallgroup') - tags = partialmethod(UnifiClientBase.request, 'GET', 'rest/tag') - neighbors = partialmethod(UnifiClientBase.request, 'GET', 'stat/rogueap') - dpi = partialmethod(UnifiClientBase.request, 'GET', 'stat/sitedpi') - stadpi = partialmethod(UnifiClientBase.request, 'GET', 'stat/stadpi') - dynamicdns = partialmethod(UnifiClientBase.request, 'GET', 'stat/dynamicdns') - networks = partialmethod(UnifiClientBase.request, 'GET', 'rest/networkconf') - portprofiles = partialmethod(UnifiClientBase.request, 'GET', 'rest/portconf') - spectrumscan = partialmethod(UnifiClientBase.request, 'GET', 'stat/spectrumscan') - radiusprofiles = partialmethod(UnifiClientBase.request, 'GET', 'rest/radiusprofile') - account = partialmethod(UnifiClientBase.request, 'GET', 'rest/account') - - c_archive_events = partialmethod(_api_cmd, 'evtmgr', 'archive-all-alarms') - - c_create_site = partialmethod(_api_cmd, 'sitemgr', 'add-site', _req_params=['desc']) - c_delete_site = partialmethod(_api_cmd, 'sitemgr', 'delete-site', _req_params=['name']) - c_update_site = partialmethod(_api_cmd, 'sitemgr', 'update-site', _req_params=['desc']) - c_delete_device = partialmethod(_api_cmd, 'sitemgr', 'delete-device', _req_params=['mac']) - c_move_device = partialmethod(_api_cmd, 'sitemgr', 'move-device', _req_params=['mac', 'site_id']) - - c_block_client = partialmethod(_api_cmd, 'stamgr', 'block-sta', _req_params=['mac']) - c_unblock_client = partialmethod(_api_cmd, 'stamgr', 'unblock-sta', _req_params=['mac']) - c_disconnect_client = partialmethod(_api_cmd, 'stamgr', 'kick-sta', _req_params=['mac']) - c_forget_client = partialmethod(_api_cmd, 'stamgr', 'forget-sta', _req_params=['mac']) # Controller 5.9.X - - c_reboot = partialmethod(_api_cmd, 'devmgr', 'restart', _req_params=['mac']) - c_force_provision = partialmethod(_api_cmd, 'devmgr', 'force-provision', _req_params=['mac']) - c_poe_power_cycle = partialmethod(_api_cmd, 'devmgr', 'power-cycle', _req_params=['mac', 'port_idx']) - c_adopt = partialmethod(_api_cmd, 'devmgr', 'adopt', _req_params=['mac']) - c_speedtest = partialmethod(_api_cmd, 'devmgr', 'speedtest') - c_speedtest_status = partialmethod(_api_cmd, 'devmgr', 'speedtest-status') - c_set_locate = partialmethod(_api_cmd, 'devmgr', 'set-locate', _req_params=['mac']) - c_unset_locate = partialmethod(_api_cmd, 'devmgr', 'unset-locate', _req_params=['mac']) - c_upgrade = partialmethod(_api_cmd, 'devmgr', 'upgrade', _req_params=['mac']) - c_upgrade_external = partialmethod(_api_cmd, 'devmgr', 'upgrade-external', _req_params=['mac', 'url']) - c_spectrum_scan = partialmethod(_api_cmd, 'devmgr', 'spectrum-scan', _req_params=['mac']) - - c_backups = partialmethod(_api_cmd, 'backup', 'list-backups') - c_delete_backup = partialmethod(_api_cmd, 'backup', 'delete-backup', _req_params=['filename']) - - c_make_backup = partialmethod(_api_cmd, 'system', 'backup') - c_check_firmware = partialmethod(_api_cmd, 'system', 'check-firmware-update') - - c_clear_dpi = partialmethod(_api_cmd, 'stat', 'reset-dpi') - - -''' A little python magic to automatically add device_macs and list_device for all known device + alerts = partialmethod(UnifiClientBase.request, 'GET', 'rest/alarm') + events = partialmethod(UnifiClientBase.request, 'GET', 'rest/event') + devices_basic = partialmethod( + UnifiClientBase.request, + 'GET', + 'stat/device-basic') + devices = partialmethod(UnifiClientBase.request, 'GET', 'stat/device') + health = partialmethod(UnifiClientBase.request, 'GET', 'stat/health') + active_clients = partialmethod(UnifiClientBase.request, 'GET', 'stat/sta') + clients = partialmethod(UnifiClientBase.request, 'GET', 'rest/user') + sysinfo = partialmethod(UnifiClientBase.request, 'GET', 'stat/sysinfo') + this_user = partialmethod(UnifiClientBase.request, 'GET', 'self') + settings = partialmethod(UnifiClientBase.request, 'GET', 'rest/setting') + routing = partialmethod(UnifiClientBase.request, 'GET', 'rest/routing') + firewallrules = partialmethod( + UnifiClientBase.request, + 'GET', + 'rest/firewallrule') + firewallgroups = partialmethod( + UnifiClientBase.request, + 'GET', + 'rest/firewallgroup') + tags = partialmethod(UnifiClientBase.request, 'GET', 'rest/tag') + neighbors = partialmethod(UnifiClientBase.request, 'GET', 'stat/rogueap') + dpi = partialmethod(UnifiClientBase.request, 'GET', 'stat/sitedpi') + stadpi = partialmethod(UnifiClientBase.request, 'GET', 'stat/stadpi') + dynamicdns = partialmethod( + UnifiClientBase.request, + 'GET', + 'stat/dynamicdns') + networks = partialmethod( + UnifiClientBase.request, + 'GET', + 'rest/networkconf') + portprofiles = partialmethod( + UnifiClientBase.request, 'GET', 'rest/portconf') + spectrumscan = partialmethod( + UnifiClientBase.request, + 'GET', + 'stat/spectrumscan') + radiusprofiles = partialmethod( + UnifiClientBase.request, + 'GET', + 'rest/radiusprofile') + account = partialmethod(UnifiClientBase.request, 'GET', 'rest/account') + + c_archive_events = partialmethod(_api_cmd, 'evtmgr', 'archive-all-alarms') + + c_create_site = partialmethod( + _api_cmd, + 'sitemgr', + 'add-site', + _req_params=['desc']) + c_delete_site = partialmethod( + _api_cmd, + 'sitemgr', + 'delete-site', + _req_params=['name']) + c_update_site = partialmethod( + _api_cmd, + 'sitemgr', + 'update-site', + _req_params=['desc']) + c_delete_device = partialmethod( + _api_cmd, + 'sitemgr', + 'delete-device', + _req_params=['mac']) + c_move_device = partialmethod( + _api_cmd, + 'sitemgr', + 'move-device', + _req_params=[ + 'mac', + 'site_id']) + + c_block_client = partialmethod( + _api_cmd, + 'stamgr', + 'block-sta', + _req_params=['mac']) + c_unblock_client = partialmethod( + _api_cmd, + 'stamgr', + 'unblock-sta', + _req_params=['mac']) + c_disconnect_client = partialmethod( + _api_cmd, 'stamgr', 'kick-sta', _req_params=['mac']) + c_forget_client = partialmethod( + _api_cmd, + 'stamgr', + 'forget-sta', + _req_params=['mac']) # Controller 5.9.X + + c_reboot = partialmethod( + _api_cmd, + 'devmgr', + 'restart', + _req_params=['mac']) + c_force_provision = partialmethod( + _api_cmd, + 'devmgr', + 'force-provision', + _req_params=['mac']) + c_poe_power_cycle = partialmethod( + _api_cmd, + 'devmgr', + 'power-cycle', + _req_params=[ + 'mac', + 'port_idx']) + c_adopt = partialmethod(_api_cmd, 'devmgr', 'adopt', _req_params=['mac']) + c_speedtest = partialmethod(_api_cmd, 'devmgr', 'speedtest') + c_speedtest_status = partialmethod(_api_cmd, 'devmgr', 'speedtest-status') + c_set_locate = partialmethod( + _api_cmd, + 'devmgr', + 'set-locate', + _req_params=['mac']) + c_unset_locate = partialmethod( + _api_cmd, + 'devmgr', + 'unset-locate', + _req_params=['mac']) + c_upgrade = partialmethod( + _api_cmd, + 'devmgr', + 'upgrade', + _req_params=['mac']) + c_upgrade_external = partialmethod( + _api_cmd, + 'devmgr', + 'upgrade-external', + _req_params=[ + 'mac', + 'url']) + c_spectrum_scan = partialmethod( + _api_cmd, + 'devmgr', + 'spectrum-scan', + _req_params=['mac']) + + c_backups = partialmethod(_api_cmd, 'backup', 'list-backups') + c_delete_backup = partialmethod( + _api_cmd, + 'backup', + 'delete-backup', + _req_params=['filename']) + + c_make_backup = partialmethod(_api_cmd, 'system', 'backup') + c_check_firmware = partialmethod( + _api_cmd, 'system', 'check-firmware-update') + + c_clear_dpi = partialmethod(_api_cmd, 'stat', 'reset-dpi') + + +''' A little python magic to automatically add device_macs and list_device for all known device types into the UnifiSite object ''' -for dev_id in set(( x['type'] for x in DEVICES.values())): - setattr(UnifiSite, "{}_macs".format(dev_id), partialmethod(UnifiSite.mac_by_type, dev_id) ) - setattr(UnifiSite, "list_{}".format(dev_id), partialmethod(UnifiSite.list_by_type, dev_id) ) - - - - +for dev_id in set((x['type'] for x in DEVICES.values())): + setattr( + UnifiSite, + "{}_macs".format(dev_id), + partialmethod( + UnifiSite.mac_by_type, + dev_id)) + setattr( + UnifiSite, + "list_{}".format(dev_id), + partialmethod( + UnifiSite.list_by_type, + dev_id)) diff --git a/unifiapi/cmd.py b/unifiapi/cmd.py index 3db5d04..a87b51e 100644 --- a/unifiapi/cmd.py +++ b/unifiapi/cmd.py @@ -1,3 +1,21 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + from . import UnifiClient, quiet from json import dumps import sys diff --git a/webhook.py b/webhook.py index 5cef9f1..3140a28 100644 --- a/webhook.py +++ b/webhook.py @@ -1,8 +1,26 @@ +'''THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE +DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, +WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''' + +# Bitcoin Cash (BCH) qpz32c4lg7x7lnk9jg6qg7s4uavdce89myax5v5nuk +# Ether (ETH) - 0x843d3DEC2A4705BD4f45F674F641cE2D0022c9FB +# Litecoin (LTC) - Lfk5y4F7KZa9oRxpazETwjQnHszEPvqPvu +# Bitcoin (BTC) - 34L8qWiQyKr8k4TnHDacfjbaSqQASbBtTd + +# contact :- github@jamessawyer.co.uk + + + from unifiapi import controller from time import sleep from requests import post -# create a file secrets.py and define +# create a file secrets.py and define # url = "slack comptible endpoint url" from secrets import url @@ -13,37 +31,42 @@ hlth = None alerts = None + def find_name(alert): for key in alert: if 'name' in key: return alert[key] + def alert_to_attachment(alerts, previous=None): res = [] already_seen = [] if previous: - already_seen = set(( x['_id'] for x in previous )) + already_seen = set((x['_id'] for x in previous)) for alert in alerts: if alert['_id'] not in already_seen: # New alert name = find_name(alert) msg = alert['msg'] foo = f"{name} - {msg}" - res.append({'ts': alert['time']/1000, 'fallback': foo, 'text':foo}) + res.append({'ts': alert['time'] / 1000, + 'fallback': foo, 'text': foo}) return res + def status_to_color(status): if status == 'ok': return 'good' - if status in [ 'warning', 'unknown' ]: + if status in ['warning', 'unknown']: return 'warning' return 'danger' + def health_to_attachments(health, previous=None, ignore_unknown=False): - hlth = dict(((x['subsystem'],x)for x in health)) + hlth = dict(((x['subsystem'], x)for x in health)) res = [] if previous: - hlth2 = dict(((x['subsystem'],x)for x in previous)) + hlth2 = dict(((x['subsystem'], x)for x in previous)) for key in list(hlth.keys()): try: if hlth[key]['status'] == hlth2[key]['status']: @@ -57,29 +80,37 @@ def health_to_attachments(health, previous=None, ignore_unknown=False): del hlth[key] except KeyError: pass - for key, name in [('vpn', 'VPN'), ('www','Internet'), ('wan','Firewall'), ('lan', 'LAN'), ('wlan', 'WiFi')]: + for key, name in [('vpn', 'VPN'), ('www', 'Internet'), + ('wan', 'Firewall'), ('lan', 'LAN'), ('wlan', 'WiFi')]: if key in hlth: status = hlth[key]['status'] color = status_to_color(status) - res.append( { + res.append({ 'text': f'{name} is {status}', 'fallback': f'{name} is {status}', 'color': color - } ) - return res + }) + return res + while True: new = s.health() res = health_to_attachments(new, previous=hlth, ignore_unknown=False) if res: - data = { 'username': 'Health Status', 'text': f'Health for {site_name}', 'attachments': res } + data = { + 'username': 'Health Status', + 'text': f'Health for {site_name}', + 'attachments': res} post(url, json=data) hlth = new new = s.alerts(args={'archived': 'false'}) res = alert_to_attachment(new, alerts) if res: - data = { 'username': 'Alerts', 'text': f'Alerts for {site_name}', 'attachments': res } + data = { + 'username': 'Alerts', + 'text': f'Alerts for {site_name}', + 'attachments': res} post(url, json=data) - alerts = new - sleep(10) \ No newline at end of file + alerts = new + sleep(10)