├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── setup.cfg ├── setup.py ├── test ├── list_backup_counts.py ├── stop_all.py └── urbackup_api_test.py └── urbackup_api └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | urbackup_server_web_api_wrapper.egg-info/* 2 | dist/urbackup-server-web-api-wrapper-*.zip 3 | urbackup_api/__pycache__/* 4 | 5 | # IDE 6 | .idea 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # urbackup-server-web-api-wrapper 2 | Python wrapper to access and control an UrBackup server 3 | 4 | ## Installation 5 | 6 | Install with: 7 | 8 | pip3 install urbackup-server-web-api-wrapper 9 | 10 | ## Usage 11 | 12 | Start a full file backup: 13 | 14 | ```python 15 | import urbackup_api 16 | 17 | server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo") 18 | 19 | server.start_full_file_backup("testclient0") 20 | ``` 21 | 22 | List clients with no file backup in the last three days: 23 | 24 | ```python 25 | import urbackup_api 26 | import time 27 | import datetime 28 | server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo") 29 | clients = server.get_status() 30 | diff_time = 3*24*60*60 # 3 days 31 | for client in clients: 32 | if client["lastbackup"]=="-" or client["lastbackup"] < time.time() - diff_time: 33 | 34 | if client["lastbackup"]=="-" or client["lastbackup"]==0: 35 | lastbackup = "Never" 36 | else: 37 | lastbackup = datetime.datetime.fromtimestamp(client["lastbackup"]).strftime("%x %X") 38 | 39 | print("Last file backup at {lastbackup} of client {clientname} is older than three days".format( 40 | lastbackup=lastbackup, clientname=client["name"] ) ) 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Always prefer setuptools over distutils 2 | from setuptools import setup, find_packages 3 | # To use a consistent encoding 4 | from codecs import open 5 | from os import path 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | # Get the long description from the README file 10 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 11 | long_description = f.read() 12 | 13 | setup( 14 | name='urbackup-server-web-api-wrapper', 15 | 16 | # Versions should comply with PEP440. For a discussion on single-sourcing 17 | # the version across setup.py and the project code, see 18 | # https://packaging.python.org/en/latest/single_source_version.html 19 | version='0.11', 20 | 21 | description='Python wrapper to access and control an UrBackup server', 22 | long_description=long_description, 23 | long_description_content_type="text/markdown", 24 | 25 | # The project's main homepage. 26 | url='https://github.com/uroni/urbackup-server-python-web-api-wrapper', 27 | 28 | # Author details 29 | author='Martin Raiber', 30 | author_email='martin@urbackup.org', 31 | 32 | # Choose your license 33 | license='Apache License 2.0', 34 | 35 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 36 | classifiers=[ 37 | # How mature is this project? Common values are 38 | # 3 - Alpha 39 | # 4 - Beta 40 | # 5 - Production/Stable 41 | 'Development Status :: 3 - Alpha', 42 | 43 | # Indicate who your project is intended for 44 | 'Intended Audience :: Developers', 45 | 46 | # Pick your license as you wish (should match "license" above) 47 | 'License :: OSI Approved :: Apache Software License', 48 | 49 | # Specify the Python versions you support here. In particular, ensure 50 | # that you indicate whether you support Python 2, Python 3 or both. 51 | 'Programming Language :: Python :: 3.4', 52 | 'Programming Language :: Python :: 3.5', 53 | ], 54 | 55 | # What does your project relate to? 56 | keywords='urbackup web api client', 57 | 58 | # You can just specify the packages manually here if your project is 59 | # simple. Or you can use find_packages(). 60 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 61 | 62 | # Alternatively, if you want to distribute just a my_module.py, uncomment 63 | # this: 64 | # py_modules=["my_module"], 65 | 66 | # List run-time dependencies here. These will be installed by pip when 67 | # your project is installed. For an analysis of "install_requires" vs pip's 68 | # requirements files see: 69 | # https://packaging.python.org/en/latest/requirements.html 70 | install_requires=[], 71 | 72 | # List additional groups of dependencies here (e.g. development 73 | # dependencies). You can install these using the following syntax, 74 | # for example: 75 | # $ pip install -e .[dev,test] 76 | extras_require={ 77 | 'dev': [], 78 | 'test': [], 79 | } 80 | ) -------------------------------------------------------------------------------- /test/list_backup_counts.py: -------------------------------------------------------------------------------- 1 | import urbackup_api 2 | 3 | server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo") 4 | 5 | clients = server.get_clients_with_group() 6 | 7 | for client in clients: 8 | 9 | file_backups = server.get_clientbackups(client["id"]) 10 | 11 | incr_file = 0 12 | full_file = 0 13 | 14 | for file_backup in file_backups: 15 | 16 | if file_backup["incremental"]>0: 17 | incr_file+=1 18 | else: 19 | full_file+=1 20 | 21 | incr_image = 0 22 | full_image = 0 23 | 24 | image_backups = server.get_clientimagebackups(client["id"]) 25 | 26 | for image_backup in image_backups: 27 | 28 | if image_backup["letter"]=="SYSVOL" or image_backup["letter"]=="ESP": 29 | continue 30 | 31 | if image_backup["incremental"]>0: 32 | incr_image+=1 33 | else: 34 | full_image+=1 35 | 36 | print("Client {clientname} in group {groupname} has {incr_file} incr file backups, {full_file} " 37 | "full file backups, {incr_image} incr image backups and " 38 | "{full_image} full image backups".format( 39 | incr_file=incr_file, clientname=client["name"], 40 | full_file=full_file, incr_image=incr_image, 41 | full_image=full_image, groupname=client["groupname"]) ) 42 | 43 | -------------------------------------------------------------------------------- /test/stop_all.py: -------------------------------------------------------------------------------- 1 | import urbackup_api 2 | 3 | 4 | server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo") 5 | 6 | for action in server.get_actions(): 7 | a = action["action"] 8 | if a ==server.action_full_file or a==server.action_resumed_full_file: 9 | print("Running full file backup: "+action["name"]) 10 | 11 | print("Stopping...") 12 | server.stop_action(action) -------------------------------------------------------------------------------- /test/urbackup_api_test.py: -------------------------------------------------------------------------------- 1 | import urbackup_api 2 | from urbackup_api import installer_os 3 | import datetime 4 | import time 5 | import sys 6 | 7 | 8 | server = urbackup_api.urbackup_server("http://127.0.0.1:55414/x", "admin", "foo") 9 | 10 | for extra_client in server.get_extra_clients(): 11 | server.remove_extra_client(extra_client["id"]) 12 | 13 | computernames = """2.2.2.2 14 | 3.3.3.3""" 15 | 16 | for line in computernames: 17 | server.add_extra_client(line) 18 | 19 | 20 | clients = server.get_status() 21 | usage = server.get_usage() 22 | 23 | if len(clients) != len(usage): 24 | print("Failed to retreive usage or status information. Length of both lists is different.") 25 | 26 | # Uncomment to format time differently 27 | # locale.setlocale(locale.LC_TIME, "german") 28 | 29 | diff_time = 3*24*60*60 # 3 days 30 | for client in clients: 31 | 32 | if client["lastbackup"]=="-" or client["lastbackup"] < time.time() - diff_time: 33 | 34 | if client["lastbackup"]=="-" or client["lastbackup"]==0: 35 | lastbackup = "Never" 36 | else: 37 | lastbackup = datetime.datetime.fromtimestamp(client["lastbackup"]).strftime("%x %X") 38 | 39 | print("Last file backup at {lastbackup} of client {clientname} is older than three days".format( 40 | lastbackup=lastbackup, clientname=client["name"] ) ) 41 | 42 | 43 | #if server.start_incr_file_backup("Johnwin7test-PC2"): 44 | # print("Started file backup successfully") 45 | #else: 46 | # print("Failed to start file backup") 47 | 48 | 49 | if not server.get_livelog(): 50 | print("Failed to get livelog contents") 51 | 52 | settings = server.get_client_settings("Johnwin7test-PC2") 53 | 54 | for key in settings: 55 | print("{key}={value}".format(key=key, value=settings[key])) 56 | 57 | print("Authkey: "+server.get_client_authkey("Johnwin7test-PC2")) 58 | 59 | if server.change_client_setting("Johnwin7test-PC2", "max_image_incr", "40"): 60 | print("Changed setting successfully") 61 | else: 62 | print("Failed to change setting") 63 | 64 | 65 | settings = server.get_global_settings() 66 | 67 | for key in settings: 68 | print("Global: {key}={value}".format(key=key, value=settings[key])) 69 | 70 | 71 | if server.set_global_setting("max_image_incr", "40"): 72 | print("Changed global setting successfully") 73 | else: 74 | print("Failed to change global setting") 75 | 76 | #Get all file backups for a specified client id 77 | backups = server.get_clientbackups('8') 78 | 79 | #Get all image backups for a specified client id 80 | backups_image = server.get_clientimagebackups('8') 81 | 82 | #Download a client installer 83 | server.download_installer("test.exe", "test", installer_os.Windows) 84 | -------------------------------------------------------------------------------- /urbackup_api/__init__.py: -------------------------------------------------------------------------------- 1 | import http.client as http 2 | import json 3 | from urllib.parse import urlparse 4 | from urllib.parse import urlencode 5 | from base64 import b64encode 6 | import hashlib 7 | import shutil 8 | import binascii 9 | import logging 10 | from enum import Enum 11 | 12 | logger = logging.getLogger('urbackup-server-python-api-wrapper') 13 | 14 | 15 | class installer_os(Enum): 16 | Windows = "windows", 17 | Linux = "linux" 18 | 19 | 20 | class urbackup_server: 21 | 22 | def __init__(self, server_url, server_username, server_password): 23 | self._server_url = server_url 24 | self._server_username = server_username 25 | self._server_password = server_password 26 | 27 | # If you have basic authentication via .htpasswd 28 | server_basic_username = '' 29 | server_basic_password = '' 30 | 31 | _session = "" 32 | 33 | _logged_in = False 34 | 35 | _lastlogid = 0 36 | 37 | def _get_response(self, action, params, method="POST"): 38 | 39 | headers = { 40 | 'Accept': 'application/json', 41 | 'Content-Type': 'application/json; charset=UTF-8' 42 | } 43 | 44 | if ('server_basic_username' in globals() and len(self.server_basic_username) > 0): 45 | userAndPass = b64encode(str.encode(self.server_basic_username + ":" + self.server_basic_password)).decode( 46 | "ascii") 47 | headers['Authorization'] = 'Basic %s' % userAndPass 48 | 49 | curr_server_url = self._server_url + "?" + urlencode({"a": action}); 50 | 51 | if (len(self._session) > 0): 52 | params["ses"] = self._session 53 | 54 | if method == None: 55 | method = 'POST' 56 | 57 | if method == "GET": 58 | curr_server_url += "&" + urlencode(params); 59 | 60 | target = urlparse(curr_server_url) 61 | 62 | if method == 'POST': 63 | body = urlencode(params) 64 | else: 65 | body = '' 66 | 67 | http_timeout = 10 * 60; 68 | 69 | if (target.scheme == 'http'): 70 | h = http.HTTPConnection(target.hostname, target.port, timeout=http_timeout) 71 | elif (target.scheme == 'https'): 72 | h = http.HTTPSConnection(target.hostname, target.port, timeout=http_timeout) 73 | else: 74 | logger.error('Unkown scheme: ' + target.scheme) 75 | raise Exception("Unkown scheme: " + target.scheme) 76 | 77 | h.request( 78 | method, 79 | target.path + "?" + target.query, 80 | body, 81 | headers) 82 | 83 | return h.getresponse(); 84 | 85 | def _get_json(self, action, params={}): 86 | tries = 50 87 | 88 | while tries > 0: 89 | response = self._get_response(action, params) 90 | 91 | if (response.status == 200): 92 | break 93 | 94 | tries = tries - 1 95 | if (tries == 0): 96 | return None 97 | else: 98 | logger.error("API call failed. Retrying...") 99 | 100 | data = response.read(); 101 | 102 | response.close() 103 | 104 | return json.loads(data.decode("utf-8", "ignore")) 105 | 106 | def _download_file(self, action, outputfn, params): 107 | 108 | response = self._get_response(action, params, "GET"); 109 | 110 | if (response.status != 200): 111 | return False 112 | 113 | with open(outputfn, 'wb') as outputf: 114 | shutil.copyfileobj(response, outputf) 115 | 116 | return True 117 | 118 | def _md5(self, s): 119 | return hashlib.md5(s.encode()).hexdigest() 120 | 121 | def login(self): 122 | 123 | if (not self._logged_in): 124 | 125 | logger.debug("Trying anonymous login...") 126 | 127 | login = self._get_json("login", {}); 128 | 129 | if (not login or 'success' not in login or not login['success']): 130 | 131 | logger.debug("Logging in...") 132 | 133 | salt = self._get_json("salt", {"username": self._server_username}) 134 | 135 | if (not salt or not ('ses' in salt)): 136 | logger.warning('Username does not exist') 137 | return False 138 | 139 | self._session = salt["ses"]; 140 | 141 | if ('salt' in salt): 142 | password_md5_bin = hashlib.md5((salt["salt"] + self._server_password).encode()).digest() 143 | password_md5 = binascii.hexlify(password_md5_bin).decode() 144 | 145 | if "pbkdf2_rounds" in salt: 146 | pbkdf2_rounds = int(salt["pbkdf2_rounds"]) 147 | if pbkdf2_rounds > 0: 148 | password_md5 = binascii.hexlify(hashlib.pbkdf2_hmac('sha256', password_md5_bin, 149 | salt["salt"].encode(), 150 | pbkdf2_rounds)).decode() 151 | 152 | password_md5 = self._md5(salt["rnd"] + password_md5) 153 | 154 | login = self._get_json("login", {"username": self._server_username, 155 | "password": password_md5}) 156 | 157 | if (not login or 'success' not in login or not login['success']): 158 | logger.warning('Error during login. Password wrong?') 159 | return False 160 | 161 | else: 162 | self._logged_in = True 163 | return True 164 | else: 165 | return False 166 | else: 167 | self._logged_in = True 168 | self._session = login["session"]; 169 | return True 170 | else: 171 | 172 | return True 173 | 174 | def get_client_status(self, clientname): 175 | 176 | if not self.login(): 177 | return None 178 | 179 | status = self._get_json("status") 180 | 181 | if not status: 182 | return None 183 | 184 | if not "status" in status: 185 | return None 186 | 187 | for client in status["status"]: 188 | 189 | if (client["name"] == clientname): 190 | return client; 191 | 192 | logger.warning("Could not find client status. No permission?") 193 | return None 194 | 195 | def download_installer(self, installer_fn, new_clientname, e_installer_os): 196 | 197 | if not self.login(): 198 | return False 199 | 200 | new_client = self._get_json("add_client", {"clientname": new_clientname}) 201 | if "already_exists" in new_client: 202 | 203 | status = self.get_client_status(new_clientname) 204 | 205 | if status == None: 206 | return False 207 | 208 | return self._download_file("download_client", installer_fn, 209 | {"clientid": status["id"], 210 | "os": e_installer_os.value}) 211 | 212 | if not "new_authkey" in new_client: 213 | return False 214 | 215 | return self._download_file("download_client", installer_fn, 216 | {"clientid": new_client["new_clientid"], 217 | "authkey": new_client["new_authkey"], 218 | "os": e_installer_os.value 219 | }) 220 | 221 | def add_client(self, clientname, groupname=None): 222 | if not self.login(): 223 | return None 224 | 225 | data = {"clientname": clientname} 226 | if groupname is not None: 227 | data['group_name'] = groupname 228 | 229 | ret = self._get_json("add_client", data) 230 | if ret == None or "already_exists" in ret: 231 | return None 232 | 233 | return ret 234 | 235 | def get_global_settings(self): 236 | if not self.login(): 237 | return None 238 | 239 | settings = self._get_json("settings", {"sa": "general"}) 240 | 241 | if not settings or not "settings" in settings: 242 | return None 243 | 244 | return settings["settings"] 245 | 246 | def set_global_setting(self, key, new_value): 247 | if not self.login(): 248 | return False 249 | 250 | settings = self._get_json("settings", {"sa": "general"}) 251 | 252 | if not settings or not "settings" in settings: 253 | return False 254 | 255 | settings["settings"][key] = new_value 256 | settings["settings"]["sa"] = "general_save" 257 | 258 | ret = self._get_json("settings", settings["settings"]) 259 | 260 | return ret != None and "saved_ok" in ret 261 | 262 | def get_client_settings(self, clientname): 263 | 264 | if not self.login(): 265 | return None 266 | 267 | client = self.get_client_status(clientname) 268 | 269 | if client == None: 270 | return None 271 | 272 | clientid = client["id"]; 273 | 274 | settings = self._get_json("settings", {"sa": "clientsettings", 275 | "t_clientid": clientid}) 276 | 277 | if not settings or not "settings" in settings: 278 | return None 279 | 280 | return settings["settings"] 281 | 282 | def change_client_setting(self, clientname, key, new_value): 283 | if not self.login(): 284 | return False 285 | 286 | client = self.get_client_status(clientname) 287 | 288 | if client == None: 289 | return False 290 | 291 | clientid = client["id"]; 292 | 293 | settings = self._get_json("settings", {"sa": "clientsettings", 294 | "t_clientid": clientid}) 295 | 296 | if not settings or not "settings" in settings: 297 | return False 298 | 299 | settings["settings"][key] = new_value 300 | settings["settings"]["overwrite"] = "true" 301 | settings["settings"]["sa"] = "clientsettings_save" 302 | settings["settings"]["t_clientid"] = clientid 303 | 304 | ret = self._get_json("settings", settings["settings"]) 305 | 306 | return ret != None and "saved_ok" in ret 307 | 308 | def get_client_authkey(self, clientname): 309 | 310 | if not self.login(): 311 | return None 312 | 313 | settings = self.get_client_settings(clientname) 314 | 315 | if settings: 316 | return settings["internet_authkey"] 317 | 318 | return None 319 | 320 | def get_server_identity(self): 321 | 322 | if not self.login(): 323 | return None 324 | 325 | status = self._get_json("status") 326 | 327 | if not status: 328 | return None 329 | 330 | if not "server_identity" in status: 331 | return None 332 | 333 | return status["server_identity"] 334 | 335 | def get_status(self): 336 | if not self.login(): 337 | return None 338 | 339 | status = self._get_json("status") 340 | 341 | if not status: 342 | return None 343 | 344 | if not "status" in status: 345 | return None 346 | 347 | return status["status"] 348 | 349 | def get_users(self): 350 | if not self.login(): 351 | return None 352 | 353 | users = self._get_json("settings", {"sa": "listusers"}) 354 | 355 | if not "users" in users: 356 | return None 357 | 358 | return users["users"] 359 | 360 | def get_livelog(self, clientid=0): 361 | if not self.login(): 362 | return None 363 | 364 | log = self._get_json("livelog", {"clientid": clientid, "lastid": self._lastlogid}) 365 | 366 | if not log: 367 | return None 368 | 369 | if not "logdata" in log: 370 | return None 371 | 372 | self._lastlogid = log["logdata"][-1]['id'] 373 | 374 | return log["logdata"] 375 | 376 | def get_usage(self): 377 | if not self.login(): 378 | return None 379 | 380 | usage = self._get_json("usage") 381 | 382 | if not usage: 383 | return None 384 | 385 | if not "usage" in usage: 386 | return None 387 | 388 | return usage["usage"] 389 | 390 | def get_extra_clients(self): 391 | if not self.login(): 392 | return None 393 | 394 | status = self._get_json("status") 395 | 396 | if not status: 397 | return None 398 | 399 | if not "extra_clients" in status: 400 | return None 401 | 402 | return status["extra_clients"] 403 | 404 | def _start_backup(self, clientname, backup_type): 405 | 406 | client_info = self.get_client_status(clientname) 407 | 408 | if not client_info: 409 | return False 410 | 411 | ret = self._get_json("start_backup", {"start_client": client_info["id"], 412 | "start_type": backup_type}); 413 | 414 | if (ret == None 415 | or "result" not in ret 416 | or len(ret["result"]) != 1 417 | or "start_ok" not in ret["result"][0] 418 | or not ret["result"][0]["start_ok"]): 419 | return False 420 | 421 | return True 422 | 423 | def start_incr_file_backup(self, clientname): 424 | return self._start_backup(clientname, 'incr_file'); 425 | 426 | def start_full_file_backup(self, clientname): 427 | return self._start_backup(clientname, 'full_file'); 428 | 429 | def start_incr_image_backup(self, clientname): 430 | return self._start_backup(clientname, 'incr_image'); 431 | 432 | def start_full_image_backup(self, clientname): 433 | return self._start_backup(clientname, 'full_image'); 434 | 435 | def get_clientimagebackups(self, clientid=0): 436 | if not self.login(): 437 | return None 438 | 439 | backups = self._get_json("backups", {"sa": "backups", "clientid": clientid}) 440 | 441 | return backups["backup_images"] 442 | 443 | def get_clientbackups(self, clientid=0): 444 | if not self.login(): 445 | return None 446 | 447 | backups = self._get_json("backups", {"sa": "backups", "clientid": clientid}) 448 | 449 | return backups["backups"] 450 | 451 | def get_backup_content(self, clientid, backupid, path="/"): 452 | if not self.login(): 453 | return None 454 | 455 | content = self._get_json("backups", { 456 | "sa": "files", 457 | "clientid": clientid, 458 | "backupid": backupid, 459 | "path": path 460 | }) 461 | 462 | return content["files"] 463 | 464 | def download_backup_file(self, clientid, backupid, path="/"): 465 | if not self.login(): 466 | return None 467 | 468 | response = self._get_response("backups", { 469 | "sa": "filesdl", 470 | "clientid": clientid, 471 | "backupid": backupid, 472 | "path": path 473 | }, "GET") 474 | 475 | if response.status != 200: 476 | return None 477 | return response.read() 478 | 479 | def get_groups(self): 480 | if not self.login(): 481 | return None 482 | 483 | settings = self._get_json("settings") 484 | 485 | return settings["navitems"]["groups"] 486 | 487 | def get_clients_with_group(self): 488 | if not self.login(): 489 | return None 490 | 491 | settings = self._get_json("settings") 492 | 493 | return settings["navitems"]["clients"] 494 | 495 | def add_extra_client(self, addr): 496 | if not self.login(): 497 | return None 498 | 499 | ret = self._get_json("status", {"hostname": addr}) 500 | 501 | if not ret: 502 | return False 503 | 504 | return True 505 | 506 | def remove_extra_client(self, ecid): 507 | if not self.login(): 508 | return None 509 | 510 | ret = self._get_json("status", {"hostname": ecid, 511 | "remove": "true"}) 512 | 513 | if not ret: 514 | return False 515 | 516 | return True 517 | 518 | action_incr_file = 1 519 | action_full_file = 2 520 | action_incr_image = 3 521 | action_full_image = 4 522 | action_resumed_incr_file = 5 523 | action_resumed_full_file = 6 524 | action_file_restore = 8 525 | action_image_restore = 9 526 | action_client_update = 10 527 | action_check_db_integrity = 11 528 | action_backup_db = 12 529 | action_recalc_stats = 13 530 | 531 | def get_actions(self): 532 | if not self.login(): 533 | return None 534 | 535 | ret = self._get_json("progress") 536 | 537 | if not ret or not "progress" in ret: 538 | return None 539 | 540 | return ret["progress"] 541 | 542 | def stop_action(self, action): 543 | if (not "clientid" in action 544 | or not "id" in action): 545 | return False 546 | 547 | if not self.login(): 548 | return None 549 | 550 | ret = self._get_json("progress", 551 | {"stop_clientid": action["clientid"], 552 | "stop_id": action["id"]}) 553 | 554 | if not ret or not "progress" in ret: 555 | return False 556 | 557 | return True 558 | --------------------------------------------------------------------------------