mirror of
https://github.com/uroni/urbackup-server-python-web-api-wrapper.git
synced 2025-10-26 11:38:16 +00:00
Merge d84c488027 into 9fa5012d32
This commit is contained in:
commit
4dbdc5e722
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
urbackup_server_web_api_wrapper.egg-info/*
|
||||
dist/urbackup-server-web-api-wrapper-*.zip
|
||||
urbackup_api/__pycache__/*
|
||||
urbackup_server_web_api_wrapper.egg-info/*
|
||||
dist/urbackup-server-web-api-wrapper-*.zip
|
||||
urbackup_api/__pycache__/*
|
||||
test.py
|
||||
*.pyc
|
||||
__pycache__
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
include README.md
|
||||
include README.md
|
||||
include LICENSE.txt
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
requests
|
||||
urllib3
|
||||
158
setup.py
158
setup.py
@ -1,79 +1,79 @@
|
||||
# Always prefer setuptools over distutils
|
||||
from setuptools import setup, find_packages
|
||||
# To use a consistent encoding
|
||||
from codecs import open
|
||||
from os import path
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
# Get the long description from the README file
|
||||
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name='urbackup-server-web-api-wrapper',
|
||||
|
||||
# Versions should comply with PEP440. For a discussion on single-sourcing
|
||||
# the version across setup.py and the project code, see
|
||||
# https://packaging.python.org/en/latest/single_source_version.html
|
||||
version='0.6',
|
||||
|
||||
description='Python wrapper to access and control an UrBackup server',
|
||||
long_description=long_description,
|
||||
|
||||
# The project's main homepage.
|
||||
url='https://github.com/uroni/urbackup-server-python-web-api-wrapper',
|
||||
|
||||
# Author details
|
||||
author='Martin Raiber',
|
||||
author_email='martin@urbackup.org',
|
||||
|
||||
# Choose your license
|
||||
license='Apache License 2.0',
|
||||
|
||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
# How mature is this project? Common values are
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
'Development Status :: 3 - Alpha',
|
||||
|
||||
# Indicate who your project is intended for
|
||||
'Intended Audience :: Developers',
|
||||
|
||||
# Pick your license as you wish (should match "license" above)
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
|
||||
# Specify the Python versions you support here. In particular, ensure
|
||||
# that you indicate whether you support Python 2, Python 3 or both.
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
],
|
||||
|
||||
# What does your project relate to?
|
||||
keywords='urbackup web api client',
|
||||
|
||||
# You can just specify the packages manually here if your project is
|
||||
# simple. Or you can use find_packages().
|
||||
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
|
||||
|
||||
# Alternatively, if you want to distribute just a my_module.py, uncomment
|
||||
# this:
|
||||
# py_modules=["my_module"],
|
||||
|
||||
# List run-time dependencies here. These will be installed by pip when
|
||||
# your project is installed. For an analysis of "install_requires" vs pip's
|
||||
# requirements files see:
|
||||
# https://packaging.python.org/en/latest/requirements.html
|
||||
install_requires=[],
|
||||
|
||||
# List additional groups of dependencies here (e.g. development
|
||||
# dependencies). You can install these using the following syntax,
|
||||
# for example:
|
||||
# $ pip install -e .[dev,test]
|
||||
extras_require={
|
||||
'dev': [],
|
||||
'test': [],
|
||||
}
|
||||
)
|
||||
# Always prefer setuptools over distutils
|
||||
from setuptools import setup, find_packages
|
||||
# To use a consistent encoding
|
||||
from codecs import open
|
||||
from os import path
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
# Get the long description from the README file
|
||||
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name='urbackup-server-web-api-wrapper',
|
||||
|
||||
# Versions should comply with PEP440. For a discussion on single-sourcing
|
||||
# the version across setup.py and the project code, see
|
||||
# https://packaging.python.org/en/latest/single_source_version.html
|
||||
version='0.7',
|
||||
|
||||
description='Python wrapper to access and control an UrBackup server',
|
||||
long_description=long_description,
|
||||
|
||||
# The project's main homepage.
|
||||
url='https://github.com/uroni/urbackup-server-python-web-api-wrapper',
|
||||
|
||||
# Author details
|
||||
author='Martin Raiber',
|
||||
author_email='martin@urbackup.org',
|
||||
|
||||
# Choose your license
|
||||
license='Apache License 2.0',
|
||||
|
||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
classifiers=[
|
||||
# How mature is this project? Common values are
|
||||
# 3 - Alpha
|
||||
# 4 - Beta
|
||||
# 5 - Production/Stable
|
||||
'Development Status :: 3 - Alpha',
|
||||
|
||||
# Indicate who your project is intended for
|
||||
'Intended Audience :: Developers',
|
||||
|
||||
# Pick your license as you wish (should match "license" above)
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
|
||||
# Specify the Python versions you support here. In particular, ensure
|
||||
# that you indicate whether you support Python 2, Python 3 or both.
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
],
|
||||
|
||||
# What does your project relate to?
|
||||
keywords='urbackup web api client',
|
||||
|
||||
# You can just specify the packages manually here if your project is
|
||||
# simple. Or you can use find_packages().
|
||||
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
|
||||
|
||||
# Alternatively, if you want to distribute just a my_module.py, uncomment
|
||||
# this:
|
||||
# py_modules=["my_module"],
|
||||
|
||||
# List run-time dependencies here. These will be installed by pip when
|
||||
# your project is installed. For an analysis of "install_requires" vs pip's
|
||||
# requirements files see:
|
||||
# https://packaging.python.org/en/latest/requirements.html
|
||||
install_requires=[],
|
||||
|
||||
# List additional groups of dependencies here (e.g. development
|
||||
# dependencies). You can install these using the following syntax,
|
||||
# for example:
|
||||
# $ pip install -e .[dev,test]
|
||||
extras_require={
|
||||
'dev': [],
|
||||
'test': [],
|
||||
}
|
||||
)
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import urbackup_api
|
||||
|
||||
|
||||
server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo")
|
||||
|
||||
for action in server.get_actions():
|
||||
a = action["action"]
|
||||
if a ==server.action_full_file or a==server.action_resumed_full_file:
|
||||
print("Running full file backup: "+action["name"])
|
||||
|
||||
print("Stopping...")
|
||||
import urbackup_api
|
||||
|
||||
|
||||
server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo")
|
||||
|
||||
for action in server.get_actions():
|
||||
a = action["action"]
|
||||
if a ==server.action_full_file or a==server.action_resumed_full_file:
|
||||
print("Running full file backup: "+action["name"])
|
||||
|
||||
print("Stopping...")
|
||||
server.stop_action(action)
|
||||
@ -1,72 +1,72 @@
|
||||
import urbackup_api
|
||||
import datetime
|
||||
import time
|
||||
|
||||
|
||||
server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo")
|
||||
|
||||
for extra_client in server.get_extra_clients():
|
||||
server.remove_extra_client(extra_client["id"])
|
||||
|
||||
computernames = """2.2.2.2
|
||||
3.3.3.3"""
|
||||
|
||||
for line in computernames:
|
||||
server.add_extra_client(line)
|
||||
|
||||
|
||||
clients = server.get_status()
|
||||
usage = server.get_usage()
|
||||
|
||||
if len(clients) != len(usage):
|
||||
print("Failed to retreive usage or status information. Length of both lists is different.")
|
||||
|
||||
# Uncomment to format time differently
|
||||
# locale.setlocale(locale.LC_TIME, "german")
|
||||
|
||||
diff_time = 3*24*60*60 # 3 days
|
||||
for client in clients:
|
||||
|
||||
if client["lastbackup"]=="-" or client["lastbackup"] < time.time() - diff_time:
|
||||
|
||||
if client["lastbackup"]=="-" or client["lastbackup"]==0:
|
||||
lastbackup = "Never"
|
||||
else:
|
||||
lastbackup = datetime.datetime.fromtimestamp(client["lastbackup"]).strftime("%x %X")
|
||||
|
||||
print("Last file backup at {lastbackup} of client {clientname} is older than three days".format(
|
||||
lastbackup=lastbackup, clientname=client["name"] ) )
|
||||
|
||||
|
||||
#if server.start_incr_file_backup("Johnwin7test-PC2"):
|
||||
# print("Started file backup successfully")
|
||||
#else:
|
||||
# print("Failed to start file backup")
|
||||
|
||||
|
||||
if not server.get_livelog():
|
||||
print("Failed to get livelog contents"
|
||||
|
||||
settings = server.get_client_settings("Johnwin7test-PC2")
|
||||
|
||||
for key in settings:
|
||||
print("{key}={value}".format(key=key, value=settings[key]))
|
||||
|
||||
print("Authkey: "+server.get_client_authkey("Johnwin7test-PC2"))
|
||||
|
||||
if server.change_client_setting("Johnwin7test-PC2", "max_image_incr", "40"):
|
||||
print("Changed setting successfully")
|
||||
else:
|
||||
print("Failed to change setting")
|
||||
|
||||
|
||||
settings = server.get_global_settings()
|
||||
|
||||
for key in settings:
|
||||
print("Global: {key}={value}".format(key=key, value=settings[key]))
|
||||
|
||||
|
||||
if server.set_global_setting("max_image_incr", "40"):
|
||||
print("Changed global setting successfully")
|
||||
else:
|
||||
print("Failed to change global setting")
|
||||
import urbackup_api
|
||||
import datetime
|
||||
import time
|
||||
|
||||
|
||||
server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo")
|
||||
|
||||
for extra_client in server.get_extra_clients():
|
||||
server.remove_extra_client(extra_client["id"])
|
||||
|
||||
computernames = """2.2.2.2
|
||||
3.3.3.3"""
|
||||
|
||||
for line in computernames:
|
||||
server.add_extra_client(line)
|
||||
|
||||
|
||||
clients = server.get_status()
|
||||
usage = server.get_usage()
|
||||
|
||||
if len(clients) != len(usage):
|
||||
print("Failed to retreive usage or status information. Length of both lists is different.")
|
||||
|
||||
# Uncomment to format time differently
|
||||
# locale.setlocale(locale.LC_TIME, "german")
|
||||
|
||||
diff_time = 3*24*60*60 # 3 days
|
||||
for client in clients:
|
||||
|
||||
if client["lastbackup"]=="-" or client["lastbackup"] < time.time() - diff_time:
|
||||
|
||||
if client["lastbackup"]=="-" or client["lastbackup"]==0:
|
||||
lastbackup = "Never"
|
||||
else:
|
||||
lastbackup = datetime.datetime.fromtimestamp(client["lastbackup"]).strftime("%x %X")
|
||||
|
||||
print("Last file backup at {lastbackup} of client {clientname} is older than three days".format(
|
||||
lastbackup=lastbackup, clientname=client["name"] ) )
|
||||
|
||||
|
||||
#if server.start_incr_file_backup("Johnwin7test-PC2"):
|
||||
# print("Started file backup successfully")
|
||||
#else:
|
||||
# print("Failed to start file backup")
|
||||
|
||||
|
||||
if not server.get_livelog():
|
||||
print("Failed to get livelog contents"
|
||||
|
||||
settings = server.get_client_settings("Johnwin7test-PC2")
|
||||
|
||||
for key in settings:
|
||||
print("{key}={value}".format(key=key, value=settings[key]))
|
||||
|
||||
print("Authkey: "+server.get_client_authkey("Johnwin7test-PC2"))
|
||||
|
||||
if server.change_client_setting("Johnwin7test-PC2", "max_image_incr", "40"):
|
||||
print("Changed setting successfully")
|
||||
else:
|
||||
print("Failed to change setting")
|
||||
|
||||
|
||||
settings = server.get_global_settings()
|
||||
|
||||
for key in settings:
|
||||
print("Global: {key}={value}".format(key=key, value=settings[key]))
|
||||
|
||||
|
||||
if server.set_global_setting("max_image_incr", "40"):
|
||||
print("Changed global setting successfully")
|
||||
else:
|
||||
print("Failed to change global setting")
|
||||
|
||||
@ -1,482 +1,470 @@
|
||||
import http.client as http
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlencode
|
||||
from base64 import b64encode
|
||||
import hashlib
|
||||
import socket
|
||||
import shutil
|
||||
import os
|
||||
import binascii
|
||||
import logging
|
||||
logger = logging.getLogger('urbackup-server-python-api-wrapper')
|
||||
|
||||
|
||||
class urbackup_server:
|
||||
|
||||
def __init__(self, server_url, server_username, server_password):
|
||||
self._server_url=server_url
|
||||
self._server_username=server_username
|
||||
self._server_password=server_password
|
||||
|
||||
|
||||
#If you have basic authentication via .htpasswd
|
||||
server_basic_username = ''
|
||||
server_basic_password = ''
|
||||
|
||||
|
||||
_session=""
|
||||
|
||||
_logged_in = False
|
||||
|
||||
_lastlogid = 0
|
||||
|
||||
def _get_response(self, action, params, method="POST"):
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
|
||||
if('server_basic_username' in globals() and len(self.server_basic_username)>0):
|
||||
userAndPass = b64encode(str.encode(self.server_basic_username+":"+self.server_basic_password)).decode("ascii")
|
||||
headers['Authorization'] = 'Basic %s' % userAndPass
|
||||
|
||||
curr_server_url=self._server_url+"?"+urlencode({"a": action});
|
||||
|
||||
if(len(self._session)>0):
|
||||
params["ses"]=self._session
|
||||
|
||||
if method==None:
|
||||
method = 'POST'
|
||||
|
||||
if method=="GET":
|
||||
curr_server_url+="&"+urlencode(params);
|
||||
|
||||
target = urlparse(curr_server_url)
|
||||
|
||||
if method=='POST':
|
||||
body = urlencode(params)
|
||||
else:
|
||||
body = ''
|
||||
|
||||
http_timeout = 10*60;
|
||||
|
||||
if(target.scheme=='http'):
|
||||
h = http.HTTPConnection(target.hostname, target.port, timeout=http_timeout)
|
||||
elif(target.scheme=='https'):
|
||||
h = http.HTTPSConnection(target.hostname, target.port, timeout=http_timeout)
|
||||
else:
|
||||
logger.error('Unkown scheme: '+target.scheme)
|
||||
raise Exception("Unkown scheme: "+target.scheme)
|
||||
|
||||
h.request(
|
||||
method,
|
||||
target.path+"?"+target.query,
|
||||
body,
|
||||
headers)
|
||||
|
||||
return h.getresponse();
|
||||
|
||||
def _get_json(self, action, params = {}):
|
||||
tries = 50
|
||||
|
||||
while tries>0:
|
||||
response = self._get_response(action, params)
|
||||
|
||||
if(response.status == 200):
|
||||
break
|
||||
|
||||
tries=tries-1
|
||||
if(tries==0):
|
||||
return None
|
||||
else:
|
||||
logger.error("API call failed. Retrying...")
|
||||
|
||||
data = response.read();
|
||||
|
||||
response.close()
|
||||
|
||||
return json.loads(data.decode("utf-8","ignore"))
|
||||
|
||||
def _download_file(self, action, outputfn, params):
|
||||
|
||||
response = self.get_response(action, params, "GET");
|
||||
|
||||
if(response.status!=200):
|
||||
return False
|
||||
|
||||
with open(outputfn, 'wb') as outputf:
|
||||
shutil.copyfileobj(response, outputf)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def _md5(self, s):
|
||||
return hashlib.md5(s.encode()).hexdigest()
|
||||
|
||||
def login(self):
|
||||
|
||||
if( not self._logged_in):
|
||||
|
||||
logger.debug("Trying anonymous login...")
|
||||
|
||||
login = self._get_json("login", {});
|
||||
|
||||
if(not login or 'success' not in login or not login['success']):
|
||||
|
||||
logger.debug("Logging in...")
|
||||
|
||||
salt = self._get_json("salt", {"username": self._server_username})
|
||||
|
||||
if( not salt or not ('ses' in salt) ):
|
||||
logger.warning('Username does not exist')
|
||||
return False
|
||||
|
||||
self._session = salt["ses"];
|
||||
|
||||
if( 'salt' in salt ):
|
||||
password_md5_bin = hashlib.md5((salt["salt"]+self._server_password).encode()).digest()
|
||||
password_md5 = binascii.hexlify(password_md5_bin).decode()
|
||||
|
||||
if "pbkdf2_rounds" in salt:
|
||||
pbkdf2_rounds = int(salt["pbkdf2_rounds"])
|
||||
if pbkdf2_rounds>0:
|
||||
password_md5 = binascii.hexlify(hashlib.pbkdf2_hmac('sha256', password_md5_bin,
|
||||
salt["salt"].encode(), pbkdf2_rounds)).decode()
|
||||
|
||||
password_md5 = self._md5(salt["rnd"]+password_md5)
|
||||
|
||||
login = self._get_json("login", { "username": self._server_username,
|
||||
"password": password_md5 })
|
||||
|
||||
if(not login or 'success' not in login or not login['success']):
|
||||
logger.warning('Error during login. Password wrong?')
|
||||
return False
|
||||
|
||||
else:
|
||||
self._logged_in=True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
self._logged_in=True
|
||||
self._session = login["session"];
|
||||
return True
|
||||
else:
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_client_status(self, clientname):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
status = self._get_json("status")
|
||||
|
||||
if not status:
|
||||
return None
|
||||
|
||||
if not "status" in status:
|
||||
return None
|
||||
|
||||
for client in status["status"]:
|
||||
|
||||
if (client["name"] == clientname):
|
||||
|
||||
return client;
|
||||
|
||||
logger.warning("Could not find client status. No permission?")
|
||||
return None
|
||||
|
||||
def download_installer(self, installer_fn, new_clientname):
|
||||
|
||||
if not self.login():
|
||||
return False
|
||||
|
||||
new_client = self._get_json("add_client", { "clientname": new_clientname})
|
||||
|
||||
if "already_exists" in new_client:
|
||||
|
||||
status = self.get_client_status(new_clientname)
|
||||
|
||||
if status == None:
|
||||
return False
|
||||
|
||||
return self._download_file("download_client", installer_fn,
|
||||
{"clientid": status["id"] })
|
||||
|
||||
|
||||
if not "new_authkey" in new_client:
|
||||
return False
|
||||
|
||||
return self._download_file("download_client", installer_fn,
|
||||
{"clientid": new_client["new_clientid"],
|
||||
"authkey": new_client["new_authkey"]
|
||||
})
|
||||
|
||||
def add_client(self, clientname):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json("add_client", { "clientname": clientname})
|
||||
if ret==None or "already_exists" in ret:
|
||||
return None
|
||||
|
||||
return ret
|
||||
|
||||
def get_global_settings(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
settings = self._get_json("settings", {"sa": "general"} )
|
||||
|
||||
if not settings or not "settings" in settings:
|
||||
return None
|
||||
|
||||
return settings["settings"]
|
||||
|
||||
def set_global_setting(self, key, new_value):
|
||||
if not self.login():
|
||||
return False
|
||||
|
||||
settings = self._get_json("settings", {"sa": "general"} )
|
||||
|
||||
if not settings or not "settings" in settings:
|
||||
return False
|
||||
|
||||
settings["settings"][key] = new_value
|
||||
settings["settings"]["sa"] = "general_save"
|
||||
|
||||
ret = self._get_json("settings", settings["settings"])
|
||||
|
||||
return ret!=None and "saved_ok" in ret
|
||||
|
||||
def get_client_settings(self, clientname):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
client = self.get_client_status(clientname)
|
||||
|
||||
if client == None:
|
||||
return None
|
||||
|
||||
clientid = client["id"];
|
||||
|
||||
settings = self._get_json("settings", {"sa": "clientsettings",
|
||||
"t_clientid": clientid})
|
||||
|
||||
if not settings or not "settings" in settings:
|
||||
return None
|
||||
|
||||
return settings["settings"]
|
||||
|
||||
def change_client_setting(self, clientname, key, new_value):
|
||||
if not self.login():
|
||||
return False
|
||||
|
||||
client = self.get_client_status(clientname)
|
||||
|
||||
if client == None:
|
||||
return False
|
||||
|
||||
clientid = client["id"];
|
||||
|
||||
settings = self._get_json("settings", {"sa": "clientsettings",
|
||||
"t_clientid": clientid})
|
||||
|
||||
if not settings or not "settings" in settings:
|
||||
return False
|
||||
|
||||
settings["settings"][key] = new_value
|
||||
settings["settings"]["overwrite"] = "true"
|
||||
settings["settings"]["sa"] = "clientsettings_save"
|
||||
settings["settings"]["t_clientid"] = clientid
|
||||
|
||||
ret = self._get_json("settings", settings["settings"])
|
||||
|
||||
return ret!=None and "saved_ok" in ret
|
||||
|
||||
def get_client_authkey(self, clientname):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
settings = self.get_client_settings(clientname)
|
||||
|
||||
if settings:
|
||||
return settings["internet_authkey"]
|
||||
|
||||
return None
|
||||
|
||||
def get_server_identity(self):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
status = self._get_json("status")
|
||||
|
||||
if not status:
|
||||
return None
|
||||
|
||||
if not "server_identity" in status:
|
||||
return None
|
||||
|
||||
return status["server_identity"]
|
||||
|
||||
def get_status(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
status = self._get_json("status")
|
||||
|
||||
if not status:
|
||||
return None
|
||||
|
||||
if not "status" in status:
|
||||
return None
|
||||
|
||||
return status["status"]
|
||||
|
||||
def get_livelog(self, clientid = 0):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
log = self._get_json("livelog", {"clientid": clientid, "lastid": self._lastlogid})
|
||||
|
||||
if not log:
|
||||
return None
|
||||
|
||||
if not "logdata" in log:
|
||||
return None
|
||||
|
||||
self._lastlogid = log["logdata"][-1]['id']
|
||||
|
||||
return log["logdata"]
|
||||
|
||||
def get_usage(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
usage = self._get_json("usage")
|
||||
|
||||
if not usage:
|
||||
return None
|
||||
|
||||
if not "usage" in usage:
|
||||
return None
|
||||
|
||||
return usage["usage"]
|
||||
|
||||
def get_extra_clients(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
status = self._get_json("status")
|
||||
|
||||
if not status:
|
||||
return None
|
||||
|
||||
if not "extra_clients" in status:
|
||||
return None
|
||||
|
||||
return status["extra_clients"]
|
||||
|
||||
def _start_backup(self, clientname, backup_type):
|
||||
|
||||
client_info = self.get_client_status(clientname)
|
||||
|
||||
if not client_info:
|
||||
return False
|
||||
|
||||
ret = self._get_json("start_backup", {"start_client": client_info["id"],
|
||||
"start_type": backup_type } );
|
||||
|
||||
if ( ret==None
|
||||
or "result" not in ret
|
||||
or len(ret["result"])!=1
|
||||
or "start_ok" not in ret["result"][0]
|
||||
or not ret["result"][0]["start_ok"] ):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def start_incr_file_backup(self, clientname):
|
||||
return self._start_backup(clientname, 'incr_file');
|
||||
|
||||
def start_full_file_backup(self, clientname):
|
||||
return self._start_backup(clientname, 'full_file');
|
||||
|
||||
def start_incr_image_backup(self, clientname):
|
||||
return self._start_backup(clientname, 'incr_image');
|
||||
|
||||
def start_full_image_backup(self, clientname):
|
||||
return self._start_backup(clientname, 'full_image');
|
||||
|
||||
def add_extra_client(self, addr):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json("status", {"hostname": addr } );
|
||||
|
||||
if not ret:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def remove_extra_client(self, ecid):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json("status", {"hostname": ecid,
|
||||
"remove": "true" } );
|
||||
|
||||
if not ret:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
action_incr_file = 1
|
||||
action_full_file = 2
|
||||
action_incr_image = 3
|
||||
action_full_image = 4
|
||||
action_resumed_incr_file = 5
|
||||
action_resumed_full_file = 6
|
||||
action_file_restore = 8
|
||||
action_image_restore = 9
|
||||
action_client_update = 10
|
||||
action_check_db_integrity = 11
|
||||
action_backup_db = 12
|
||||
action_recalc_stats = 13
|
||||
|
||||
def get_actions(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json("progress")
|
||||
|
||||
if not ret or not "progress" in ret:
|
||||
return None
|
||||
|
||||
return ret["progress"]
|
||||
|
||||
def stop_action(self, action):
|
||||
if (not "clientid" in action
|
||||
or not "id" in action):
|
||||
return False
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json("progress",
|
||||
{"stop_clientid": action["clientid"],
|
||||
"stop_id": action["id"]})
|
||||
|
||||
if not ret or not "progress" in ret:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
import json
|
||||
import requests
|
||||
from base64 import b64encode
|
||||
import hashlib
|
||||
import socket
|
||||
import shutil
|
||||
import os
|
||||
import binascii
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('urbackup-server-python-api-wrapper')
|
||||
|
||||
|
||||
"""Backwards compatible imports"""
|
||||
try:
|
||||
import urllib
|
||||
except ImportError:
|
||||
import urllib.parse
|
||||
|
||||
|
||||
class urbackup_server:
|
||||
|
||||
#If you have basic authentication via .htpasswd
|
||||
server_basic_username = ''
|
||||
server_basic_password = ''
|
||||
|
||||
action_incr_file = 1
|
||||
action_full_file = 2
|
||||
action_incr_image = 3
|
||||
action_full_image = 4
|
||||
action_resumed_incr_file = 5
|
||||
action_resumed_full_file = 6
|
||||
action_file_restore = 8
|
||||
action_image_restore = 9
|
||||
action_client_update = 10
|
||||
action_check_db_integrity = 11
|
||||
action_backup_db = 12
|
||||
action_recalc_stats = 13
|
||||
|
||||
_session = ''
|
||||
_logged_in = False
|
||||
_lastlogid = 0
|
||||
|
||||
def __init__(self, url, username, password):
|
||||
self._server_url = url
|
||||
self._server_username = username
|
||||
self._server_password = password
|
||||
|
||||
def _get_response(self, action, params, method='POST'):
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
|
||||
if('server_basic_username' in globals() and len(self.server_basic_username)>0):
|
||||
userAndPass = b64encode(
|
||||
str.encode('%s:%s' % (self.server_basic_username, self.server_basic_password))
|
||||
).decode('ascii')
|
||||
headers['Authorization'] = 'Basic %s' % userAndPass
|
||||
|
||||
try:
|
||||
action_url = '%s?%s' % (self._server_url, urllib.urlencode({'a': action}))
|
||||
except AttributeError:
|
||||
action_url = '%s?%s' % (self._server_url, urllib.parse.urlencode({'a': action}))
|
||||
|
||||
if(len(self._session)>0):
|
||||
params['ses'] = self._session
|
||||
|
||||
if method == 'POST':
|
||||
r = requests.post(
|
||||
action_url,
|
||||
data=params
|
||||
)
|
||||
elif method == 'GET':
|
||||
r = requests.get(
|
||||
action_url,
|
||||
params=params
|
||||
)
|
||||
else:
|
||||
raise Exception('Request with method \'%s\' has not been implemented yet.' % method)
|
||||
|
||||
return r
|
||||
|
||||
def _get_json(self, action, params = {}):
|
||||
tries = 50
|
||||
|
||||
while tries>0:
|
||||
response = self._get_response(action, params)
|
||||
|
||||
if(response.status_code == 200):
|
||||
break
|
||||
|
||||
tries -= 1
|
||||
if tries <= 0:
|
||||
return None
|
||||
else:
|
||||
logger.error('API call failed. Retrying...')
|
||||
|
||||
return response.json()
|
||||
|
||||
def _download_file(self, action, outputfn, params):
|
||||
|
||||
req = self._get_response(action, params, 'GET');
|
||||
|
||||
if req.status_code != 200:
|
||||
return False
|
||||
|
||||
with open(outputfn, 'wb') as stream:
|
||||
for chunk in req.iter_content(chunk_size=128):
|
||||
stream.write(chunk)
|
||||
|
||||
return True
|
||||
|
||||
def _md5(self, s):
|
||||
return hashlib.md5(s.encode()).hexdigest()
|
||||
|
||||
def login(self):
|
||||
|
||||
if not self._logged_in:
|
||||
|
||||
logger.debug('Trying anonymous login...')
|
||||
|
||||
login = self._get_json('login', {});
|
||||
|
||||
if not login or 'success' not in login or not login['success'] :
|
||||
logger.debug('Logging in...')
|
||||
|
||||
salt = self._get_json('salt', {'username': self._server_username})
|
||||
if not salt or not ('ses' in salt):
|
||||
logger.warning('Username does not exist')
|
||||
return False
|
||||
|
||||
self._session = salt['ses'];
|
||||
|
||||
if 'salt' in salt:
|
||||
password_md5_bin = hashlib.md5((salt['salt']+self._server_password).encode()).digest()
|
||||
password_md5 = binascii.hexlify(password_md5_bin).decode()
|
||||
|
||||
if 'pbkdf2_rounds' in salt:
|
||||
pbkdf2_rounds = int(salt['pbkdf2_rounds'])
|
||||
if pbkdf2_rounds>0:
|
||||
password_md5 = binascii.hexlify(hashlib.pbkdf2_hmac('sha256', password_md5_bin,
|
||||
salt['salt'].encode(), pbkdf2_rounds)).decode()
|
||||
|
||||
password_md5 = self._md5(salt['rnd']+password_md5)
|
||||
|
||||
login = self._get_json('login', {
|
||||
'username': self._server_username,
|
||||
'password': password_md5 })
|
||||
|
||||
if not login or 'success' not in login or not login['success']:
|
||||
logger.warning('Error during login. Password wrong?')
|
||||
return False
|
||||
|
||||
else:
|
||||
self._logged_in = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
self._logged_in = True
|
||||
self._session = login['session'];
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
def get_client_status(self, clientname):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
status = self._get_json('status')
|
||||
|
||||
if not status:
|
||||
return None
|
||||
|
||||
if not 'status' in status:
|
||||
return None
|
||||
|
||||
for client in status['status']:
|
||||
|
||||
if client['name'] == clientname:
|
||||
|
||||
return client;
|
||||
|
||||
logger.warning('Could not find client status. No permission?')
|
||||
return None
|
||||
|
||||
def download_installer(self, installer_fn, new_clientname, os='linux'):
|
||||
"""Download installer for os, defaults to linux"""
|
||||
|
||||
if not os.lower() in ['linux', 'osx', 'mac', 'windows']:
|
||||
raise Exception('OS not supported')
|
||||
|
||||
if not self.login():
|
||||
return False
|
||||
|
||||
new_client = self._get_json('add_client', {'clientname': new_clientname})
|
||||
|
||||
if 'already_exists' in new_client:
|
||||
|
||||
status = self.get_client_status(new_clientname)
|
||||
|
||||
if status == None:
|
||||
return False
|
||||
|
||||
return self._download_file('download_client', installer_fn, {'clientid': status['id'] })
|
||||
|
||||
|
||||
if not 'new_authkey' in new_client:
|
||||
return False
|
||||
|
||||
return self._download_file('download_client', installer_fn,
|
||||
{
|
||||
'clientid': new_client['new_clientid'],
|
||||
'authkey': new_client['new_authkey'],
|
||||
'os': os
|
||||
})
|
||||
|
||||
def add_client(self, clientname):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json('add_client', { 'clientname': clientname})
|
||||
if ret==None or 'already_exists' in ret:
|
||||
return None
|
||||
|
||||
return ret
|
||||
|
||||
def get_global_settings(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
settings = self._get_json('settings', {'sa': 'general'} )
|
||||
|
||||
if not settings or not 'settings' in settings:
|
||||
return None
|
||||
|
||||
return settings['settings']
|
||||
|
||||
def set_global_setting(self, key, new_value):
|
||||
if not self.login():
|
||||
return False
|
||||
|
||||
settings = self._get_json('settings', {'sa': 'general'} )
|
||||
|
||||
if not settings or not 'settings' in settings:
|
||||
return False
|
||||
|
||||
settings['settings'][key] = new_value
|
||||
settings['settings']['sa'] = 'general_save'
|
||||
|
||||
ret = self._get_json('settings', settings['settings'])
|
||||
|
||||
return ret != None and 'saved_ok' in ret
|
||||
|
||||
def get_client_settings(self, clientname):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
client = self.get_client_status(clientname)
|
||||
|
||||
if client == None:
|
||||
return None
|
||||
|
||||
clientid = client['id'];
|
||||
|
||||
settings = self._get_json('settings', {'sa': 'clientsettings',
|
||||
't_clientid': clientid})
|
||||
|
||||
if not settings or not 'settings' in settings:
|
||||
return None
|
||||
|
||||
return settings['settings']
|
||||
|
||||
def change_client_setting(self, clientname, key, new_value):
|
||||
if not self.login():
|
||||
return False
|
||||
|
||||
client = self.get_client_status(clientname)
|
||||
|
||||
if client == None:
|
||||
return False
|
||||
|
||||
clientid = client['id'];
|
||||
|
||||
settings = self._get_json('settings', {'sa': 'clientsettings',
|
||||
't_clientid': clientid})
|
||||
|
||||
if not settings or not 'settings' in settings:
|
||||
return False
|
||||
|
||||
settings['settings'][key] = new_value
|
||||
settings['settings']['overwrite'] = 'true'
|
||||
settings['settings']['sa'] = 'clientsettings_save'
|
||||
settings['settings']['t_clientid'] = clientid
|
||||
|
||||
ret = self._get_json('settings', settings['settings'])
|
||||
|
||||
return ret != None and 'saved_ok' in ret
|
||||
|
||||
def get_client_authkey(self, clientname):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
settings = self.get_client_settings(clientname)
|
||||
|
||||
if settings:
|
||||
return settings['internet_authkey']
|
||||
|
||||
return None
|
||||
|
||||
def get_server_identity(self):
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
status = self._get_json('status')
|
||||
|
||||
if not status:
|
||||
return None
|
||||
|
||||
if not 'server_identity' in status:
|
||||
return None
|
||||
|
||||
return status['server_identity']
|
||||
|
||||
def get_status(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
status = self._get_json('status')
|
||||
|
||||
if not status:
|
||||
return None
|
||||
|
||||
if not 'status' in status:
|
||||
return None
|
||||
|
||||
return status['status']
|
||||
|
||||
def get_livelog(self, clientid = 0):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
log = self._get_json('livelog', {'clientid': clientid, 'lastid': self._lastlogid})
|
||||
|
||||
if not log:
|
||||
return None
|
||||
|
||||
if not 'logdata' in log:
|
||||
return None
|
||||
|
||||
self._lastlogid = log['logdata'][-1]['id']
|
||||
|
||||
return log['logdata']
|
||||
|
||||
def get_usage(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
usage = self._get_json('usage')
|
||||
|
||||
if not usage:
|
||||
return None
|
||||
|
||||
if not 'usage' in usage:
|
||||
return None
|
||||
|
||||
return usage['usage']
|
||||
|
||||
def get_extra_clients(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
status = self._get_json('status')
|
||||
|
||||
if not status:
|
||||
return None
|
||||
|
||||
if not 'extra_clients' in status:
|
||||
return None
|
||||
|
||||
return status['extra_clients']
|
||||
|
||||
def _start_backup(self, clientname, backup_type):
|
||||
|
||||
client_info = self.get_client_status(clientname)
|
||||
|
||||
if not client_info:
|
||||
return False
|
||||
|
||||
ret = self._get_json('start_backup', {'start_client': client_info['id'],
|
||||
'start_type': backup_type } );
|
||||
|
||||
if ( ret == None
|
||||
or 'result' not in ret
|
||||
or len(ret['result'])!=1
|
||||
or 'start_ok' not in ret['result'][0]
|
||||
or not ret['result'][0]['start_ok']):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def start_incr_file_backup(self, clientname):
|
||||
return self._start_backup(clientname, 'incr_file');
|
||||
|
||||
def start_full_file_backup(self, clientname):
|
||||
return self._start_backup(clientname, 'full_file');
|
||||
|
||||
def start_incr_image_backup(self, clientname):
|
||||
return self._start_backup(clientname, 'incr_image');
|
||||
|
||||
def start_full_image_backup(self, clientname):
|
||||
return self._start_backup(clientname, 'full_image');
|
||||
|
||||
def add_extra_client(self, addr):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json('status', {'hostname': addr } )
|
||||
|
||||
if not ret:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def remove_extra_client(self, ecid):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json('status', {'hostname': ecid,
|
||||
'remove': 'true' })
|
||||
|
||||
if not ret:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_actions(self):
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json('progress')
|
||||
|
||||
if not ret or not 'progress' in ret:
|
||||
return None
|
||||
|
||||
return ret['progress']
|
||||
|
||||
def stop_action(self, action):
|
||||
if (not 'clientid' in action
|
||||
or not 'id' in action):
|
||||
return False
|
||||
|
||||
if not self.login():
|
||||
return None
|
||||
|
||||
ret = self._get_json('progress',
|
||||
{'stop_clientid': action['clientid'],
|
||||
'stop_id': action['id']})
|
||||
|
||||
if not ret or not 'progress' in ret:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
Loading…
Reference in New Issue
Block a user