Skip to content

Commit 4b05e15

Browse files
authored
Version 0.2.0 (#1)
* tabs to spaces * various improvements * format improvments * added post/get methods * added build file * format * added config merge methods * added diff method * moved ansible file to examples * added license file * added pyproject.toml * fixed project name * updated readme * updated readme * removed old build file Co-authored-by: michael.salathe <info@murxs.ch>
1 parent 4f8319a commit 4b05e15

File tree

6 files changed

+1561
-98
lines changed

6 files changed

+1561
-98
lines changed

LICENSE

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
This is free and unencumbered software released into the public domain.
2+
3+
Anyone is free to copy, modify, publish, use, compile, sell, or
4+
distribute this software, either in source code form or as a compiled
5+
binary, for any purpose, commercial or non-commercial, and by any
6+
means.
7+
8+
In jurisdictions that recognize copyright laws, the author or authors
9+
of this software dedicate any and all copyright interest in the
10+
software to the public domain. We make this dedication for the benefit
11+
of the public at large and to the detriment of our heirs and
12+
successors. We intend this dedication to be an overt act of
13+
relinquishment in perpetuity of all present and future rights to this
14+
software under copyright law.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22+
OTHER DEALINGS IN THE SOFTWARE.
23+
24+
For more information, please refer to <http://unlicense.org/>

README.md

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Python class to access Netonix® WISP Switch WebAPI
33

44
**NEITHER THIS CODE NOR THE AUTHOR IS ASSOCIATED WITH NETONIX® IN ANY WAY.**
5-
5+
66
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
77
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
88
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
@@ -14,20 +14,11 @@ OTHER DEALINGS IN THE SOFTWARE.
1414
# Description
1515
The Netonix® WISP Switches provide a WebAPI as backend for their webinterface. This python script allows to access this API directly.
1616

17-
Current methods:
18-
```open(ip,user,password)
19-
getConfig()
20-
backup()
21-
getMAC()
22-
getStatus()
23-
```
24-
Not tested:
25-
```putConfig()
26-
restore()
27-
```
2817

29-
to change configuration, you need to fetch the full configuratoin first, change it and push it back up:
18+
to change configuration, you need to fetch the full configuration first, change it and push it back up:
3019
```
20+
from netonix_api import Netonix
21+
3122
n=Netonix()
3223
n.open(ip,user,pw)
3324
n.getConfig()

netonix_api.py

Lines changed: 197 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -31,94 +31,206 @@
3131
"""
3232

3333
import requests
34+
from requests.exceptions import Timeout
35+
from copy import deepcopy
3436
import time
3537
import json
38+
try:
39+
from deepdiff import DeepDiff
40+
DIFF = True
41+
except:
42+
DIFF = False
3643

3744
class Netonix():
38-
def __init__(self):
39-
self.ip=None
40-
self.s=None
41-
self.url={}
42-
self.url["login"]="/index.php"
43-
self.url["backup"]="/api/v1/backup"
44-
self.url["config"]="/api/v1/config"
45-
self.url["apply"]="/api/v1/apply"
46-
self.url["confirm"]="/api/v1/applystatus"
47-
self.url["reboot"]="/api/v1/reboot"
48-
self.url["restore"]="/api/v1/restore"
49-
self.url["mac"]="/api/v1/mactable"
50-
self.url["status"]="/api/v1/status/30sec"
51-
self.url["id"]="/api/v1/bootid"
52-
self.config={}
53-
self.mac={}
54-
self.status={}
55-
self.id=""
56-
pass
57-
def open(self,ip,user,password):
58-
self.ip=ip
59-
self.s = requests.session()
60-
self.s.verify=False
61-
data={}
62-
data["username"]=user
63-
data["password"]=password
64-
r = self.s.post("https://"+self.ip+self.url["login"], data)
65-
if("Invalid username or password" in r.text):
66-
raise Exception("Invalid username or password")
67-
def getConfig(self):
68-
r = self.s.get("https://"+self.ip+self.url["config"])
69-
result=r.json()
70-
if("Config_Version" in result):
71-
self.config=result
72-
def putConfig(self):
73-
raise Exception("the putConfig method is still untested.")
74-
r = self.s.post("https://"+self.ip+self.url["config"], json=self.config)
75-
print(r.json())
76-
r = self.s.post("https://"+self.ip+self.url["apply"])
77-
time.sleep(2)
78-
r = self.s.post("https://"+self.ip+self.url["confirm"])
79-
return r.json()
80-
def backup(self,output):
81-
r = self.s.get("https://"+self.ip+self.url["backup"]+"/"+self.ip)
82-
if(r.status_code != requests.codes.ok):
83-
raise Exception("Backup Request Failed")
84-
newFile = open(output, "wb")
85-
newFile.write(r.content)
86-
newFile.close()
87-
def restore(self,i):
88-
raise Exception("the restore method is still untested.")
89-
newFile = open(i, "rb")
90-
data=""
91-
for a in newFile:
92-
data+=a
93-
newFile.close()
94-
r = self.s.post("https://"+self.ip+self.url["restore"],data)
95-
print(r.json())
96-
if(r.status_code != requests.codes.ok):
97-
raise Exception("Restore Request Failed")
98-
r = self.s.get("https://"+self.ip+self.url["reboot"])
99-
return r.json()
100-
def getMAC(self):
101-
r = self.s.get("https://"+self.ip+self.url["mac"])
102-
self.mac=r.json()["MACTable"]
103-
def getID(self):
104-
r = self.s.get("https://"+self.ip+self.url["id"]+"?_=%d"%time.time())
105-
self.id=r.json()["BootID"]
106-
def getStatus(self):
107-
if(self.id==""):
108-
self.getID()
109-
r = self.s.get("https://"+self.ip+self.url["status"]+"?%s&_=%d"%(self.id,time.time()))
110-
self.status=r.json()
45+
def __init__(self):
46+
self.ip = None
47+
self.s = None
48+
self.url = {}
49+
self.url["login"] = "/index.php"
50+
self.url["backup"] = "/api/v1/backup"
51+
self.url["config"] = "/api/v1/config"
52+
self.url["apply"] = "/api/v1/apply"
53+
self.url["confirm"] = "/api/v1/applystatus"
54+
self.url["reboot"] = "/api/v1/reboot"
55+
self.url["restore"] = "/api/v1/restore"
56+
self.url["mac"] = "/api/v1/mactable"
57+
self.url["status"] = "/api/v1/status/30sec"
58+
self.url["id"] = "/api/v1/bootid"
59+
self.url["update"] = "/api/v1/uploadfirmware"
60+
self.url["doupdate"] = "/api/v1/upgradefirmware"
61+
self.config = {}
62+
self.orig_config = None
63+
self.mac = {}
64+
self.status = {}
65+
self.id = ""
66+
67+
def _get(self, url, params=None, timeout=15, **kwargs):
68+
full_url = "https://"+self.ip+self.url[url]
69+
return self.s.get(full_url, params=params, timeout=timeout, **kwargs)
70+
71+
def _post(self, url, data=None, json=None, timeout=15, **kwargs):
72+
full_url = "https://"+self.ip+self.url[url]
73+
return self.s.post(
74+
full_url,
75+
data=data,
76+
json=json,
77+
timeout=timeout,
78+
**kwargs
79+
)
80+
81+
@staticmethod
82+
def _merge_by_key(old, new, key="Number", append=True):
83+
for item in new:
84+
found = False
85+
for old_item in old:
86+
if(key not in old_item):
87+
continue
88+
if(old_item[key] != item[key]):
89+
continue
90+
old_item.update(item)
91+
found = True
92+
break
93+
if(found is False):
94+
if(append is True):
95+
old_item.append(new)
96+
else:
97+
raise LookupError()
98+
99+
def open(self, ip, user, password):
100+
self.ip = ip
101+
self.s = requests.session()
102+
self.s.verify = False
103+
data = {}
104+
data["username"] = user
105+
data["password"] = password
106+
r = self._post("login", data)
107+
if("Invalid username or password" in r.text):
108+
raise Exception("Invalid username or password")
109+
110+
def getConfig(self):
111+
r = self._get("config")
112+
result = r.json()
113+
if("Config_Version" in result):
114+
self.config = result
115+
116+
def putConfig(self):
117+
r = self._post("config", json=self.config)
118+
try:
119+
r = self._post("apply")
120+
except Timeout:
121+
pass
122+
self.ip = self.config["IPv4_Address"]
123+
for a in range(5):
124+
try:
125+
r = self._post("confirm")
126+
except Timeout:
127+
continue
128+
break
129+
if(r.status_code != requests.codes.ok):
130+
raise Exception("Config Confirm Request Failed")
131+
# return r.json()
132+
133+
def backup(self, output):
134+
r = self.s.get("https://"+self.ip+self.url["backup"]+"/"+self.ip)
135+
if(r.status_code != requests.codes.ok):
136+
raise Exception("Backup Request Failed")
137+
newFile = open(output, "wb")
138+
newFile.write(r.content)
139+
newFile.close()
140+
141+
def restore(self, i):
142+
raise Exception("the restore method is still untested.")
143+
newFile = open(i, "rb")
144+
data = ""
145+
for a in newFile:
146+
data += a
147+
newFile.close()
148+
r = self._post("restore", data)
149+
print(r.json())
150+
if(r.status_code != requests.codes.ok):
151+
raise Exception("Restore Request Failed")
152+
r = self._get("reboot")
153+
return r.json()
154+
155+
def getMAC(self):
156+
r = self._get("mac")
157+
if(r.status_code != requests.codes.ok):
158+
raise Exception("Action failed")
159+
self.mac = r.json()["MACTable"]
160+
161+
def getID(self):
162+
r = self._get("id", params={"_": time.time()})
163+
if(r.status_code != requests.codes.ok):
164+
raise Exception("Action failed")
165+
self.id = r.json()["BootID"]
166+
167+
def getStatus(self):
168+
if(self.id == ""):
169+
self.getID()
170+
r = self.s.get("https://"+self.ip+self.url["status"]+"?%s&_=%d" % (self.id, time.time()))
171+
if(r.status_code != requests.codes.ok):
172+
raise Exception("Action failed")
173+
self.status = r.json()
174+
175+
def update(self, i):
176+
data = ""
177+
with open(i, mode='rb') as file: # b is important -> binary
178+
data = file.read()
179+
r = self._post("update", data)
180+
if(r.status_code != requests.codes.ok):
181+
raise Exception("Firmware Upload Failed")
182+
r = self._get("doupdate")
183+
if(r.status_code != requests.codes.ok):
184+
raise Exception("Update Request Failed")
185+
186+
def mergeConfig(self, config):
187+
self.orig_config = deepcopy(self.config)
188+
189+
for k, v in config.items():
190+
if(k == "Ports"):
191+
self._merge_by_key(self.config[k], v, key="Number")
192+
continue
193+
if(k == "LACP"):
194+
self._merge_by_key(self.config[k], v, key="Port")
195+
continue
196+
if(k == "VLANs"):
197+
self._merge_by_key(self.config[k], v, key="ID")
198+
continue
199+
if(type(v) is dict):
200+
continue
201+
if(type(v) is list):
202+
self.config[k] += v
203+
continue
204+
self.config[k] = v
205+
206+
def replaceConfig(self, config):
207+
self.orig_config = deepcopy(self.config)
208+
209+
if("Config_Version" in config):
210+
del config["Config_Version"]
211+
self.config.update(config)
212+
213+
def getDiff(self):
214+
if(self.orig_config is None):
215+
return {}
216+
if(DIFF is False):
217+
raise ImportError("Missing DeepDiff Module")
218+
return DeepDiff(
219+
self.orig_config,
220+
self.config,
221+
exclude_paths="root['Config_Version']"
222+
)
223+
111224

112225
if __name__ == '__main__':
113-
import getpass
114-
import traceback
115-
import urllib3
116-
import sys
117-
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
118-
ip=str(input("switch ip:"))
119-
user=str(input("user:"))
120-
pw= getpass.getpass("password:")
121-
n=Netonix()
122-
n.open(ip,user,pw)
123-
n.getStatus()
124-
print(json.dumps(n.status,indent=4))
226+
import getpass
227+
import urllib3
228+
229+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
230+
ip = str(input("switch ip:"))
231+
user = str(input("user:"))
232+
pw = getpass.getpass("password:")
233+
n = Netonix()
234+
n.open(ip, user, pw)
235+
n.getStatus()
236+
print(json.dumps(n.status, indent=4))

pyproject.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "python_netonix_api"
7+
version = "0.2.0"
8+
authors = [
9+
{ name="shrank", email="info@murxs.ch" },
10+
]
11+
license = { file = "LICENSE" }
12+
readme = "README.md"
13+
description = "Python class to access Netonix WISP Switch WebAPI"
14+
requires-python = ">=3.0"
15+
classifiers = [
16+
"Programming Language :: Python :: 3",
17+
"Operating System :: OS Independent",
18+
]
19+
keywords = ["network", "automation", "netonix"]
20+
dependencies = [
21+
"requests"
22+
]
23+
24+
[project.optional-dependencies]
25+
dev = ["deepdiff", "getpass", "urllib3"]
26+
27+
[project.urls]
28+
Homepage = "https://github.com/shrank/python_netonix_api"

0 commit comments

Comments
 (0)