pax_global_header00006660000000000000000000000064141301753670014521gustar00rootroot0000000000000052 comment=2706e7e1ba0ef405c78ac2212224bd9254e71872 proxmoxer-1.2.0/000077500000000000000000000000001413017536700135645ustar00rootroot00000000000000proxmoxer-1.2.0/.coveragerc000066400000000000000000000000471413017536700157060ustar00rootroot00000000000000[run] branch = True source = proxmoxer proxmoxer-1.2.0/.devcontainer/000077500000000000000000000000001413017536700163235ustar00rootroot00000000000000proxmoxer-1.2.0/.devcontainer/Dockerfile000066400000000000000000000014611413017536700203170ustar00rootroot00000000000000# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.166.1/containers/python-3/.devcontainer/base.Dockerfile # [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 ARG VARIANT="3" FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. COPY test_requirements.txt dev_requirements.txt /tmp/pip-tmp/ RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/test_requirements.txt -r /tmp/pip-tmp/dev_requirements.txt \ && rm -rf /tmp/pip-tmp # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends proxmoxer-1.2.0/.devcontainer/devcontainer.json000066400000000000000000000035571413017536700217110ustar00rootroot00000000000000// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.166.1/containers/python-3 { "name": "Proxmoxer Development", "build": { "dockerfile": "Dockerfile", "context": "..", "args": { // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 "VARIANT": "3", } }, // Set *default* container specific settings.json values on container create. "settings": { "terminal.integrated.shell.linux": "/bin/bash", "python.pythonPath": "/usr/local/bin/python", "python.linting.enabled": true, "python.linting.pylintEnabled": true, "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", "python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "ms-python.python", "streetsidesoftware.code-spell-checker", "mhutchie.git-graph" ], // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "pip3 install --user -r requirements.txt", // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode" } proxmoxer-1.2.0/.gitignore000066400000000000000000000041011413017536700155500ustar00rootroot00000000000000# IDE files .idea *.code-workspace coverage.* # generated files README.txt # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ proxmoxer-1.2.0/.travis.yml000066400000000000000000000004031413017536700156720ustar00rootroot00000000000000language: python python: - 2.7 - 3.4 - 3.8 - 3.9 install: - pip install . - pip install -r test_requirements.txt script: - nosetests --with-coverage --cover-erase --cover-branches --cover-package=proxmoxer -w tests after_success: - coveralls proxmoxer-1.2.0/.vscode/000077500000000000000000000000001413017536700151255ustar00rootroot00000000000000proxmoxer-1.2.0/.vscode/settings.json000066400000000000000000000001771413017536700176650ustar00rootroot00000000000000{ "cSpell.words": [ "Butovich", "cpus", "Oleg", "onboot", "Paramiko", "proxmoxer", "vmid" ], } proxmoxer-1.2.0/LICENSE.txt000066400000000000000000000020611413017536700154060ustar00rootroot00000000000000The MIT License Copyright (c) 2013 Oleg Butovich Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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 AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.proxmoxer-1.2.0/MANIFEST.in000066400000000000000000000001421413017536700153170ustar00rootroot00000000000000include LICENSE.txt include README.txt include README.rst global-exclude *.orig *.pyc *.log *.swp proxmoxer-1.2.0/README.rst000066400000000000000000000331051413017536700152550ustar00rootroot00000000000000========================================= Proxmoxer: A wrapper for Proxmox REST API ========================================= master branch: |master_build_status| |master_coverage_status| |pypi_version| |pypi_downloads| develop branch: |develop_build_status| |develop_coverage_status| What does it do and what's different? ------------------------------------- Proxmoxer is a wrapper around the `Proxmox REST API v2 `_. Works with Proxmox Virtual Environment (PVE) and Proxmox Mail Gateway (PMG) API. It was inspired by slumber, but it is dedicated only to Proxmox. It allows not only REST API use over HTTPS, but the same api over ssh and pvesh utility. Like `Proxmoxia `_, it dynamically creates attributes which responds to the attributes you've attempted to reach. Installation ------------ .. code-block:: bash pip install proxmoxer To use the 'https' backend, install requests .. code-block:: bash pip install requests To use the 'ssh_paramiko' backend, install paramiko .. code-block:: bash pip install paramiko To use the 'openssh' backend, install openssh_wrapper .. code-block:: bash pip install openssh_wrapper Short usage information ----------------------- The first thing to do is import the proxmoxer library and create ProxmoxAPI instance. .. code-block:: python from proxmoxer import ProxmoxAPI proxmox = ProxmoxAPI('proxmox_host', user='admin@pam', password='secret_word', verify_ssl=False) This will connect by default to PVE through the 'https' backend. To select a different service, use the `service` argument (currently supports 'PVE', 'PMG', and): **Define PVE connection:** .. code-block:: python from proxmoxer import ProxmoxAPI proxmox = ProxmoxAPI('proxmox_host', user='admin@pam', password='secret_word', verify_ssl=False, service='PVE') **Define PMG connection:** .. code-block:: python from proxmoxer import ProxmoxAPI proxmox = ProxmoxAPI('proxmox_host', user='admin@pam', password='secret_word', verify_ssl=False, service='PMG') You can also setup `API Tokens `_ which allow tighter access controls. API Tokens are also stateless, so they much better for long-lived programs that might have the standard username/password authentication timeout. API tokens can be created through the web UI or through the `API `_. .. code-block:: python from proxmoxer import ProxmoxAPI proxmox = ProxmoxAPI('proxmox_host', user='admin', token_name='test_token', token_value='ab27beeb-9ac4-4df1-aa19-62639f27031e') For SSH access, it is possible to use pre-prepared public/private key authentication and ssh-agent. .. code-block:: python from proxmoxer import ProxmoxAPI proxmox = ProxmoxAPI('proxmox_host', user='proxmox_admin', backend='ssh_paramiko') **Note: ensure you have the required libraries (listed above) for the connection method you are using** Queries are exposed via the access methods **get**, **post**, **put** and **delete**. For convenience two synonyms are available: **create** for **post**, and **set** for **put**. Using the paths from the `Proxmox REST API v2 `_, you can create API calls using the access methods above. .. code-block:: python for node in proxmox.nodes.get(): for vm in proxmox.nodes(node['node']).openvz.get(): print "{0}. {1} => {2}" .format(vm['vmid'], vm['name'], vm['status']) >>> 141. puppet-2.london.example.com => running 101. munki.london.example.com => running 102. redmine.london.example.com => running 140. dns-1.london.example.com => running 126. ns-3.london.example.com => running 113. rabbitmq.london.example.com => running same code can be rewritten in the next way: .. code-block:: python for node in proxmox.get('nodes'): for vm in proxmox.get('nodes/%s/openvz' % node['node']): print "%s. %s => %s" % (vm['vmid'], vm['name'], vm['status']) As a demonstration of the flexibility of usage of this library, the following lines accomplish the equivalent function: .. code-block:: python proxmox.nodes(node['node']).openvz.get() proxmox.nodes(node['node']).get('openvz') proxmox.get('nodes/%s/openvz' % node['node']) proxmox.get('nodes', node['node'], 'openvz') Some more examples: Listing VMs: .. code-block:: python for vm in proxmox.cluster.resources.get(type='vm'): print("{0}. {1} => {2}" .format(vm['vmid'], vm['name'], vm['status'])) Listing contents of the ``local`` storage on the ``proxmox_node`` node (method 1): .. code-block:: python node = proxmox.nodes('proxmox_node') pprint(node.storage('local').content.get()) Listing contents of the ``local`` storage on the ``proxmox_node`` node (method 2): .. code-block:: python node = proxmox.nodes.proxmox_node() pprint(node.storage.local.content.get()) creating a new lxc container: .. code-block:: python node = proxmox.nodes('proxmox_node') node.lxc.create(vmid=202, ostemplate='local:vztmpl/debian-9.0-standard_20170530_amd64.tar.gz', hostname='debian-stretch', storage='local', memory=512, swap=512, cores=1, password='secret', net0='name=eth0,bridge=vmbr0,ip=192.168.22.1/20,gw=192.168.16.1') The same lxc container can be created with options set in a dictionary. This approach allows adding ``ssh-public-keys`` without getting syntax errors. .. code-block:: python newcontainer = { 'vmid': 202, 'ostemplate': 'local:vztmpl/debian-9.0-standard_20170530_amd64.tar.gz', 'hostname': 'debian-stretch', 'storage': 'local', 'memory': 512, 'swap': 512, 'cores': 1, 'password': 'secret', 'net0': 'name=eth0,bridge=vmbr0,ip=192.168.22.1/20,gw=192.168.16.1' } node = proxmox.nodes('proxmox_node') node.lxc.create(**newcontainer) Uploading a template: .. code-block:: python local_storage = proxmox.nodes('proxmox_node').storage('local') local_storage.upload.create(content='vztmpl', filename=open(os.path.expanduser('~/templates/debian-6-my-core_1.0-1_i386.tar.gz'),'rb'))) NOTE: for large file uploads, please ensure the ``requests_toolbelt`` pip module is installed. This provides support for larger files and reduces the memory requirement of uploads. Downloading rrd CPU image data to a file: .. code-block:: python response = proxmox.nodes('proxmox').rrd.get(ds='cpu', timeframe='hour') with open('cpu.png', 'wb') as f: f.write(response['image'].encode('raw_unicode_escape')) Example of usage of logging: .. code-block:: python # now logging debug info will be written to stdout logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s:%(name)s: %(message)s') Example of PMG usage: .. code-block:: python from proxmoxer import ProxmoxAPI import json proxmox = ProxmoxAPI('proxmox_host', user='admin@pam', password='secret_word', verify_ssl=False, service='PMG') a = proxmox.statistics.sender.get() c = json_formatted_str = json.dumps(a, indent=2) print(c) Changelog --------- 1.2.0 (2021-10-07) .................. * Addition (https): Added OTP code support to authentication (`John Hollowell `_) * Addition (https): Added support for large file uploads using requests_toolbelt module (`John Hollowell `_) * Addition (all): Added support for Proxmox Mail Gateway (PMG) and Proxmox Backup Server (PBS) with parameter validation (`Gabriel Cardoso de Faria `_ and `John Hollowell `_) * Addition (all): Added detailed information to ResourceException (`mihailstoynov `_) * Bugfix (base_ssh): Resolved issue with values containing spaces by encapsulating values in quotes (`mihailstoynov `_) * Bugfix (all): Resolved issue with using get/post/push/delete on a base ProxmoxAPI object (`John Hollowell `_) * Bugfix (all): Added support for responses which are not JSON (`John Hollowell `_) * Improvement: Added and updated documentation (`Ananias Filho `_ and `Thomas Baag `_) * Improvement: Tests are now not installed when using PIP (`Ville Skyttä `_) * Addition: Devcontainer definition now available to make development easier (`John Hollowell `_) 1.1.1 (2020-06-23) .................. * Bugfix (https): correctly renew ticket in the session, not just the auth (`John Hollowell `_) 1.1.0 (2020-05-22) .................. * Addition (https): Added API Token authentication (`John Hollowell `_) * Improvement (https): user/password authentication refreshes ticket to prevent expiration (`CompileNix `_ and `John Hollowell `_) * Bugfix (ssh_paramiko): Handle empty stderr from ssh connections (`morph027 `_) * DEPRECATED (https): using ``auth_token`` and ``csrf_token`` (ProxmoxHTTPTicketAuth) is now deprecated. Either pass the ``auth_token`` as the ``password`` or use the API Tokens. 1.0.4 (2020-01-24) .................. * Improvement (https): Added timeout to authentication (James Lin) * Improvement (https): Handle AnyEvent::HTTP status codes gracefully (Georges Martin) * Improvement (https): Advanced error message with error code >=400 (`ssi444 `_) * Bugfix (ssh): Fix pvesh output format for version > 5.3 (`timansky `_) * Transfered development to proxmoxer organization 1.0.3 (2018-09-10) .................. * Improvement: Added option to specify port in hostname parameter (`pvanagtmaal `_) * Improvement: Added stderr to the Response content (`Jérôme Schneider `_) * Bugfix: Paramiko python3: stdout and stderr must be a str not bytes (`Jérôme Schneider `_) * New lxc example in docu (`Geert Stappers `_) 1.0.2 (2017-12-02) .................. * Tarball repackaged with tests 1.0.1 (2017-12-02) .................. * LICENSE file now included in tarball * Added verify_ssl parameter to ProxmoxHTTPAuth (`Walter Doekes `_) 1.0.0 (2017-11-12) .................. * Update Proxmoxer readme (`Emmanuel Kasper `_) * Display the reason of API calls errors (`Emmanuel Kasper `_, `kantsdog `_) * Filter for ssh response code (`Chris Plock `_) 0.2.5 (2017-02-12) .................. * Adding sudo to execute CLI with paramiko ssh backend (`Jason Meridth `_) * Proxmoxer/backends/ssh_paramiko: improve file upload (`Jérôme Schneider `_) 0.2.4 (2016-05-02) .................. * Removed newline in tmp_filename string (`Jérôme Schneider `_) * Fix to avoid module reloading (`jklang `_) 0.2.3 (2016-01-20) .................. * Minor typo fix (`Srinivas Sakhamuri `_) 0.2.2 (2016-01-19) .................. * Adding sudo to execute pvesh CLI in openssh backend (`Wei Tie `_, `Srinivas Sakhamuri `_) * Add support to specify an identity file for ssh connections (`Srinivas Sakhamuri `_) 0.2.1 (2015-05-02) .................. * fix for python 3.4 (`kokuev `_) 0.2.0 (2015-03-21) .................. * Https will now raise AuthenticationError when appropriate. (`scap1784 `_) * Preliminary python 3 compatibility. (`wdoekes `_) * Additional example. (`wdoekes `_) 0.1.7 (2014-11-16) .................. * Added ignore of "InsecureRequestWarning: Unverified HTTPS request is being made..." warning while using https (requests) backend. 0.1.4 (2013-06-01) .................. * Added logging * Added openssh backend * Tests are reorganized 0.1.3 (2013-05-30) .................. * Added next tests * Bugfixes 0.1.2 (2013-05-27) .................. * Added first tests * Added support for travis and coveralls * Bugfixes 0.1.1 (2013-05-13) .................. * Initial try. .. |master_build_status| image:: https://travis-ci.org/proxmoxer/proxmoxer.png?branch=master :target: https://travis-ci.org/proxmoxer/proxmoxer .. |master_coverage_status| image:: https://coveralls.io/repos/proxmoxer/proxmoxer/badge.png?branch=master :target: https://coveralls.io/r/proxmoxer/proxmoxer .. |develop_build_status| image:: https://travis-ci.org/proxmoxer/proxmoxer.png?branch=develop :target: https://travis-ci.org/proxmoxer/proxmoxer .. |develop_coverage_status| image:: https://coveralls.io/repos/proxmoxer/proxmoxer/badge.png?branch=develop :target: https://coveralls.io/r/proxmoxer/proxmoxer .. |pypi_version| image:: https://img.shields.io/pypi/v/proxmoxer.svg :target: https://pypi.python.org/pypi/proxmoxer .. |pypi_downloads| image:: https://img.shields.io/pypi/dm/proxmoxer.svg :target: https://pypi.python.org/pypi/proxmoxer proxmoxer-1.2.0/dev_requirements.txt000066400000000000000000000000211413017536700176770ustar00rootroot00000000000000twine setuptools proxmoxer-1.2.0/proxmoxer/000077500000000000000000000000001413017536700156275ustar00rootroot00000000000000proxmoxer-1.2.0/proxmoxer/__init__.py000066400000000000000000000002121413017536700177330ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __version__ = '1.2.0' __licence__ = 'MIT' from .core import * proxmoxer-1.2.0/proxmoxer/backends/000077500000000000000000000000001413017536700174015ustar00rootroot00000000000000proxmoxer-1.2.0/proxmoxer/backends/__init__.py000066400000000000000000000001371413017536700215130ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' proxmoxer-1.2.0/proxmoxer/backends/base_ssh.py000066400000000000000000000053271413017536700215510ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' from itertools import chain import json import re import logging import sys from proxmoxer.core import SERVICES logger = logging.getLogger(__name__) logger.setLevel(level=logging.WARNING) class Response(object): def __init__(self, content, status_code): self.status_code = status_code self.content = content self.text = str(content) self.headers = {"content-type": "application/json"} class ProxmoxBaseSSHSession(object): def _exec(self, cmd): raise NotImplementedError() # noinspection PyUnusedLocal def request(self, method, url, data=None, params=None, headers=None): method = method.lower() data = data or {} params = params or {} url = url.strip() cmd = {'post': 'create', 'put': 'set'}.get(method, method) # for 'upload' call some workaround tmp_filename = '' if url.endswith('upload'): # copy file to temporary location on proxmox host tmp_filename, _ = self._exec( "python -c 'import tempfile; import sys; tf = tempfile.NamedTemporaryFile(); sys.stdout.write(tf.name)'") self.upload_file_obj(data['filename'], tmp_filename) data['filename'] = data['filename'].name data['tmpfilename'] = tmp_filename translated_data = ' '.join(["-{0} '{1}'".format(k, v) for k, v in chain(data.items(), params.items())]) additional_options = SERVICES[self.service.upper()].get("ssh_additional_options", "") full_cmd = '{0}sh {1} {2}'.format(self.service, ' '.join(filter(None, (cmd, url, translated_data))), additional_options) stdout, stderr = self._exec(full_cmd) def match(s): return re.match(r'\d\d\d [a-zA-Z]', s) if stderr: # sometimes contains extra text like 'trying to acquire lock...OK' status_code = next( (int(s.split()[0]) for s in stderr.splitlines() if match(s)), 500) else: status_code = 200 if stdout: return Response(stdout, status_code) return Response(stderr, status_code) def upload_file_obj(self, file_obj, remote_path): raise NotImplementedError() class JsonSimpleSerializer(object): def loads(self, response): try: return json.loads(response.content) except (UnicodeDecodeError, ValueError): return {"errors": response.content} class BaseBackend(object): def get_session(self): return self.session def get_base_url(self): return '' def get_serializer(self): return JsonSimpleSerializer() proxmoxer-1.2.0/proxmoxer/backends/https.py000066400000000000000000000241261413017536700211220ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' import json import os import sys import time import logging from proxmoxer.core import SERVICES, config_failure logger = logging.getLogger(__name__) logger.setLevel(level=logging.WARNING) STREAMING_SIZE_THRESHOLD = 100 * 1024 * 1024 # 10 MiB try: import requests urllib3 = requests.packages.urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) from requests.auth import AuthBase from requests.cookies import cookiejar_from_dict except ImportError: logger.error("Chosen backend requires 'requests' module\n") sys.exit(1) if sys.version_info[0] >= 3: import io def is_file(obj): return isinstance(obj, io.IOBase) # prefer using monoatomic time if available def get_time(): return time.monotonic() else: def is_file(obj): return isinstance(obj, file) def get_time(): return time.time() class AuthenticationError(Exception): def __init__(self, msg): super(AuthenticationError, self).__init__(msg) self.msg = msg def __str__(self): return self.msg def __repr__(self): return self.__str__() class ProxmoxHTTPAuthBase(AuthBase): def get_cookies(self): return cookiejar_from_dict({}) def get_tokens(self): return None, None class ProxmoxHTTPAuth(ProxmoxHTTPAuthBase): # number of seconds between renewing access tickets (must be less than 7200 to function correctly) # if calls are made less frequently than 2 hrs, using the API token auth is reccomended renew_age = 3600 def __init__(self, base_url, username, password, otp=None, verify_ssl=False, timeout=5, service='PVE'): self.base_url = base_url self.username = username self.verify_ssl = verify_ssl self.timeout = timeout self.service = service self.pve_auth_ticket = "" self._getNewTokens(password=password, otp=otp) def _getNewTokens(self, password=None, otp=None): if password == None: # refresh from existing (unexpired) ticket password = self.pve_auth_ticket data = {"username": self.username, "password": password} if otp: data["otp"] = otp response_data = requests.post(self.base_url + "/access/ticket", verify=self.verify_ssl, timeout=self.timeout, data=data).json()["data"] if response_data is None: raise AuthenticationError("Couldn't authenticate user: {0} to {1}".format(self.username, self.base_url + "/access/ticket")) self.birth_time = get_time() self.pve_auth_ticket = response_data["ticket"] self.csrf_prevention_token = response_data["CSRFPreventionToken"] def get_cookies(self): return cookiejar_from_dict({self.service + "AuthCookie": self.pve_auth_ticket}) def get_tokens(self): return self.pve_auth_ticket, self.csrf_prevention_token def __call__(self, r): #refresh ticket if older than `renew_age` if (get_time() - self.birth_time) >= self.renew_age: logger.debug("refreshing ticket (age {0})".format(get_time() - self.birth_time)) self._getNewTokens() # only attach CSRF token if needed (reduce interception risk) if r.method != 'GET': r.headers["CSRFPreventionToken"] = self.csrf_prevention_token return r # DEPRECATED(1.1.0) - either use a password or the API Tokens class ProxmoxHTTPTicketAuth(ProxmoxHTTPAuth): """Use existing ticket/token to create a session. Overrides ProxmoxHTTPAuth so that an existing auth ticket and csrf token may be used instead of passing username/password. """ def __init__(self, auth_ticket, csrf_token): self.pve_auth_ticket = auth_ticket self.csrf_prevention_token = csrf_token self.birth_time = get_time() # deprecation notice logger.warning("** Existing token auth is Deprecated as of 1.1.0\n** Please use the API Token Auth for long-running programs or pass existing ticket as password to the user/password auth") class ProxmoxHTTPApiTokenAuth(ProxmoxHTTPAuthBase): def __init__(self, username, token_name, token_value, service): self.service = service self.username = username self.token_name = token_name self.token_value = token_value def __call__(self, r): r.headers["Authorization"] = "{0}APIToken={1}!{2}{3}{4}".format(self.service, self.username, self.token_name, SERVICES[self.service]["token_separator"], self.token_value) return r class JsonSerializer(object): content_types = [ "application/json", "application/x-javascript", "text/javascript", "text/x-javascript", "text/x-json" ] def get_accept_types(self): return ", ".join(self.content_types) def loads(self, response): try: return json.loads(response.content.decode('utf-8'))['data'] except (UnicodeDecodeError, ValueError): return {"errors": response.content} class ProxmoxHttpSession(requests.Session): def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, timeout=None, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None, serializer=None): a = auth or self.auth c = cookies or self.cookies # take set verify flag from session if request does not have this parameter explicitly if verify is None: verify = self.verify # pull cookies from auth if not present if (not c) and a: cookies = a.get_cookies() # filter out streams files = files or {} data = data or {} isLargePayload = False totalFileSize = 0 for k, v in data.copy().items(): if is_file(v): totalFileSize += getFileSize(v) if totalFileSize > STREAMING_SIZE_THRESHOLD: isLargePayload = True # add in filename from file pointer (patch for https://github.com/requests/toolbelt/pull/316) files[k] = (requests.utils.guess_filename(v), v) del data[k] # if there are any large file, send all data and files using streaming multipart encoding if isLargePayload: try: from requests_toolbelt import MultipartEncoder encoder = MultipartEncoder(fields=mergeDicts(data, files)) data = encoder files = None headers = {'Content-Type': encoder.content_type} except ImportError: # if the files will cause issues with the SSL 2GiB limit (https://bugs.python.org/issue42853#msg384566) if totalFileSize > 2147483135: #2^31 - 1 - 512 logger.warn( "Install 'requests_toolbelt' to add support for files larger than 2GiB") raise OverflowError("Unable to upload a payload larger than 2 GiB") else: logger.info("Installing 'requests_toolbelt' will deacrease memory used during upload") if not files and serializer: headers = {"Content-Type": 'application/x-www-form-urlencoded'} return super(ProxmoxHttpSession, self).request(method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert) class Backend(object): def __init__(self, host, user=None, password=None, otp=None, port=None, verify_ssl=True, mode='json', timeout=5, auth_token=None, csrf_token=None, token_name=None, token_value=None, service='PVE'): if ':' in host: host, host_port = host.split(':') port = host_port if host_port.isdigit() else port # if a port is not specified, use the default port for this service if not port: port = SERVICES[service]["default_port"] self.base_url = "https://{0}:{1}/api2/{2}".format(host, port, mode) if auth_token is not None: # DEPRECATED(1.1.0) - either use a password or the API Tokens self.auth = ProxmoxHTTPTicketAuth(auth_token, csrf_token) elif token_name is not None: if not "token" in SERVICES[service]["supported_https_auths"]: config_failure("{} does not support API Token authentication", service) self.auth = ProxmoxHTTPApiTokenAuth(user, token_name, token_value, service) elif password is not None: if not "password" in SERVICES[service]["supported_https_auths"]: config_failure("{} does not support password authentication", service) self.auth = ProxmoxHTTPAuth(self.base_url, user, password, otp, verify_ssl, timeout, service) self.verify_ssl = verify_ssl self.mode = mode self.timeout = timeout def get_session(self): session = ProxmoxHttpSession() session.verify = self.verify_ssl session.auth = self.auth # cookies are taken from the auth session.headers['Connection'] = 'keep-alive' session.headers["accept"] = self.get_serializer().get_accept_types() return session def get_base_url(self): return self.base_url def get_serializer(self): assert self.mode == 'json' return JsonSerializer() def get_tokens(self): """Return the in-use auth and csrf tokens if using user/password auth.""" return self.auth.get_tokens() def getFileSize(fileObj): # store existing file cursor location startingCursor = fileObj.tell() # get size size = fileObj.seek(0, os.SEEK_END) # reset cursor fileObj.seek(startingCursor) return size def mergeDicts(*dicts): # compatibility function for missing unpack operator # synonymous with {**dict for dict in dicts} return {k: v for d in dicts for k, v in d.items()} proxmoxer-1.2.0/proxmoxer/backends/openssh.py000066400000000000000000000045031413017536700214340ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' from proxmoxer.backends.base_ssh import ProxmoxBaseSSHSession, BaseBackend try: import openssh_wrapper except ImportError: import sys sys.stderr.write("Chosen backend requires 'openssh_wrapper' module\n") sys.exit(1) class ProxmoxOpenSSHSession(ProxmoxBaseSSHSession): def __init__(self, host, username, service='PVE', configfile=None, port=22, timeout=5, forward_ssh_agent=False, sudo=False, identity_file=None): self.host = host self.username = username self.configfile = configfile self.service = service.lower() self.port = port self.timeout = timeout self.forward_ssh_agent = forward_ssh_agent self.sudo = sudo self.identity_file = identity_file self.ssh_client = openssh_wrapper.SSHConnection(self.host, login=self.username, port=self.port, timeout=self.timeout, identity_file=self.identity_file) def _exec(self, cmd): if self.sudo: cmd = "sudo " + cmd ret = self.ssh_client.run(cmd, forward_ssh_agent=self.forward_ssh_agent) return ret.stdout, ret.stderr def upload_file_obj(self, file_obj, remote_path): self.ssh_client.scp((file_obj,), target=remote_path) class Backend(BaseBackend): def __init__(self, host, user, configfile=None, port=22, timeout=5, forward_ssh_agent=False, sudo=False, identity_file=None, service='PVE'): self.session = ProxmoxOpenSSHSession(host, user, configfile=configfile, port=port, timeout=timeout, forward_ssh_agent=forward_ssh_agent, sudo=sudo, identity_file=identity_file, service=service) proxmoxer-1.2.0/proxmoxer/backends/ssh_paramiko.py000066400000000000000000000052541413017536700224410ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' import os from proxmoxer.backends.base_ssh import ProxmoxBaseSSHSession, BaseBackend try: import paramiko except ImportError: import sys sys.stderr.write("Chosen backend requires 'paramiko' module\n") sys.exit(1) class ProxmoxParamikoSession(ProxmoxBaseSSHSession): def __init__(self, host, username, service='PVE', password=None, private_key_file=None, port=22, timeout=5, sudo=False): self.host = host self.username = username self.password = password self.service = service.lower() self.private_key_file = private_key_file self.port = port self.timeout = timeout self.sudo = sudo self.ssh_client = self._connect() def _connect(self): ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) if self.private_key_file: key_filename = os.path.expanduser(self.private_key_file) else: key_filename = None ssh_client.connect(self.host, username=self.username, allow_agent=(not self.password), look_for_keys=True, key_filename=key_filename, password=self.password, timeout=self.timeout, port=self.port) return ssh_client def _exec(self, cmd): if self.sudo: cmd = 'sudo ' + cmd session = self.ssh_client.get_transport().open_session() session.exec_command(cmd) stdout = session.makefile('rb', -1).read().decode() stderr = session.makefile_stderr('rb', -1).read().decode() return stdout, stderr def upload_file_obj(self, file_obj, remote_path): sftp = self.ssh_client.open_sftp() sftp.putfo(file_obj, remote_path) sftp.close() class Backend(BaseBackend): def __init__(self, host, user, password=None, private_key_file=None, port=22, timeout=5, sudo=False, service='PVE'): self.session = ProxmoxParamikoSession(host, user, password=password, private_key_file=private_key_file, port=port, timeout=timeout, sudo=sudo, service=service) proxmoxer-1.2.0/proxmoxer/core.py000066400000000000000000000137601413017536700171400ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' import importlib import posixpath import logging # Python 3 compatibility: try: import httplib except ImportError: # py3 from http import client as httplib try: import urlparse except ImportError: # py3 from urllib import parse as urlparse try: basestring except NameError: # py3 basestring = (bytes, str) logger = logging.getLogger(__name__) logger.setLevel(level=logging.WARNING) # https://metacpan.org/pod/AnyEvent::HTTP ANYEVENT_HTTP_STATUS_CODES = { 595: "Errors during connection establishment, proxy handshake", 596: "Errors during TLS negotiation, request sending and header processing", 597: "Errors during body receiving or processing", 598: "User aborted request via on_header or on_body", 599: "Other, usually nonretryable, errors (garbled URL etc.)" } SERVICES = { "PVE": {"supported_backends": ["https", "openssh", "ssh_paramiko"], "supported_https_auths": ["password", "token"], "default_port": 8006, "token_separator": "=", "ssh_additional_options": "--output-format json"}, "PMG": {"supported_backends": ["https", "openssh", "ssh_paramiko"], "supported_https_auths": ["password"], "default_port": 8006}, "PBS": {"supported_backends": ["https"], "supported_https_auths": ["password", "token"], "default_port": 8007, "token_separator": ":"}} def config_failure(message, *args): raise NotImplementedError(message.format(*args)) class ProxmoxResourceBase(object): def __getattr__(self, item): if item.startswith("_"): raise AttributeError(item) kwargs = self._store.copy() kwargs['base_url'] = self.url_join(self._store["base_url"], item) return ProxmoxResource(**kwargs) def url_join(self, base, *args): scheme, netloc, path, query, fragment = urlparse.urlsplit(base) path = path if len(path) else "/" path = posixpath.join(path, *[('%s' % x) for x in args]) return urlparse.urlunsplit([scheme, netloc, path, query, fragment]) class ResourceException(Exception): def __init__(self, status_code, status_message, content, errors=None): self.status_code = status_code self.status_message = status_message self.content = content self.errors = errors if errors != None: content += " - {0}".format(errors) message = "{0} {1}: {2}".format(status_code, status_message, content).strip() super(ResourceException, self).__init__(message) class ProxmoxResource(ProxmoxResourceBase): def __init__(self, **kwargs): self._store = kwargs def __call__(self, resource_id=None): if not resource_id: return self if isinstance(resource_id, basestring): resource_id = resource_id.split("/") elif not isinstance(resource_id, (tuple, list)): resource_id = [str(resource_id)] kwargs = self._store.copy() if resource_id is not None: kwargs["base_url"] = self.url_join(self._store["base_url"], *resource_id) return ProxmoxResource(**kwargs) def _request(self, method, data=None, params=None): url = self._store["base_url"] if data: logger.info('%s %s %r', method, url, data) else: logger.info('%s %s', method, url) resp = self._store["session"].request(method, url, data=data or None, params=params) logger.debug('Status code: %s, output: %s', resp.status_code, resp.content) if resp.status_code >= 400: if hasattr(resp, 'reason'): raise ResourceException( resp.status_code, httplib.responses.get(resp.status_code, ANYEVENT_HTTP_STATUS_CODES.get(resp.status_code)), resp.reason, (self._store["serializer"].loads(resp) or {}).get('errors') ) else: raise ResourceException( resp.status_code, httplib.responses.get(resp.status_code, ANYEVENT_HTTP_STATUS_CODES.get(resp.status_code)), resp.text ) elif 200 <= resp.status_code <= 299: return self._store["serializer"].loads(resp) def get(self, *args, **params): return self(args)._request("GET", params=params) def post(self, *args, **data): return self(args)._request("POST", data=data) def put(self, *args, **data): return self(args)._request("PUT", data=data) def delete(self, *args, **params): return self(args)._request("DELETE", params=params) def create(self, *args, **data): return self.post(*args, **data) def set(self, *args, **data): return self.put(*args, **data) class ProxmoxAPI(ProxmoxResource): def __init__(self, host, backend='https', service='PVE', **kwargs): service = service.upper() # throw error for unsupported services if not service in SERVICES.keys(): config_failure("{} service is not supported", service) # throw error for unsupported backend for service if not backend in SERVICES[service]["supported_backends"]: config_failure("{} does not support {} backend", service, backend) #load backend module self._backend = importlib.import_module('.backends.%s' % backend, 'proxmoxer').Backend(host, service=service, **kwargs) self._backend_name = backend self._store = { "base_url": self._backend.get_base_url(), "session": self._backend.get_session(), "serializer": self._backend.get_serializer(), } def get_tokens(self): """Return the auth and csrf tokens. Returns (None, None) if the backend is not https using password authentication. """ if self._backend_name != 'https': return None, None return self._backend.get_tokens() proxmoxer-1.2.0/setup.py000066400000000000000000000035511413017536700153020ustar00rootroot00000000000000#!/usr/bin/env python import codecs import re import sys import proxmoxer import os from setuptools import setup if not os.path.exists('README.txt') and 'sdist' in sys.argv: with codecs.open('README.rst', encoding='utf8') as f: rst = f.read() code_block = '(:\n\n)?\.\. code-block::.*' rst = re.sub(code_block, '::', rst) with codecs.open('README.txt', encoding='utf8', mode='wb') as f: f.write(rst) try: readme = 'README.txt' if os.path.exists('README.txt') else 'README.rst' long_description = codecs.open(readme, encoding='utf-8').read() except: long_description = 'Could not read README.txt' setup( name = 'proxmoxer', version = proxmoxer.__version__, description = 'Python Wrapper for the Proxmox 2.x API (HTTP and SSH)', author = 'Oleg Butovich', author_email = 'obutovich@gmail.com', license = "MIT", url = 'https://github.com/proxmoxer/proxmoxer', download_url = 'http://pypi.python.org/pypi/proxmoxer', keywords = ['proxmox', 'api'], packages=['proxmoxer', 'proxmoxer.backends'], classifiers = [ #http://pypi.python.org/pypi?%3Aaction=list_classifiers "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Clustering", "Topic :: System :: Monitoring", "Topic :: System :: Systems Administration", ], long_description = long_description ) proxmoxer-1.2.0/test_requirements.txt000066400000000000000000000000751413017536700201110ustar00rootroot00000000000000mock nose requests < 2.9 coveralls paramiko openssh_wrapper proxmoxer-1.2.0/tests/000077500000000000000000000000001413017536700147265ustar00rootroot00000000000000proxmoxer-1.2.0/tests/__init__.py000066400000000000000000000001401413017536700170320ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' proxmoxer-1.2.0/tests/base/000077500000000000000000000000001413017536700156405ustar00rootroot00000000000000proxmoxer-1.2.0/tests/base/__init__.py000066400000000000000000000001401413017536700177440ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' proxmoxer-1.2.0/tests/base/base_ssh_suite.py000066400000000000000000000144311413017536700212150ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' from itertools import islice try: import itertools.izip as zip except ImportError: pass from nose.tools import eq_, ok_, raises from proxmoxer.core import ResourceException class BaseSSHSuite(object): proxmox = None client = None session = None def __init__(self, sudo=False): self.sudo = sudo def _split_cmd(self, cmd): splitted = cmd.split() if not self.sudo: eq_(splitted[0], 'pvesh') else: eq_(splitted[0], 'sudo') eq_(splitted[1], 'pvesh') splitted.pop(0) options_set = set((' '.join((k, v)) for k, v in zip(islice(splitted, 3, None, 2), islice(splitted, 4, None, 2)))) return ' '.join(splitted[1:3]), options_set def _get_called_cmd(self): raise NotImplementedError() def _set_stdout(self, stdout): raise NotImplementedError() def _set_stderr(self, stderr): raise NotImplementedError() def test_get(self): self._set_stdout(""" [ { "subdir" : "status" }, { "subdir" : "content" }, { "subdir" : "upload" }, { "subdir" : "rrd" }, { "subdir" : "rrddata" } ]""") result = self.proxmox.nodes('proxmox').storage('local').get() eq_(self._get_called_cmd(), self._called_cmd('pvesh get /nodes/proxmox/storage/local --output-format json')) eq_(result[0]['subdir'], 'status') eq_(result[1]['subdir'], 'content') eq_(result[2]['subdir'], 'upload') eq_(result[3]['subdir'], 'rrd') eq_(result[4]['subdir'], 'rrddata') def test_delete(self): self.proxmox.nodes('proxmox').openvz(100).delete() eq_(self._get_called_cmd(), self._called_cmd('pvesh delete /nodes/proxmox/openvz/100 --output-format json')) self._set_stderr("200 OK") self.proxmox.nodes('proxmox').openvz('101').delete() eq_(self._get_called_cmd(), self._called_cmd('pvesh delete /nodes/proxmox/openvz/101 --output-format json')) self._set_stderr("200 OK") self.proxmox.nodes('proxmox').openvz.delete('102') eq_(self._get_called_cmd(), self._called_cmd('pvesh delete /nodes/proxmox/openvz/102 --output-format json')) def test_post(self): self._set_stderr("200 OK") node = self.proxmox.nodes('proxmox') node.openvz.create(vmid=800, ostemplate='local:vztmpl/debian-6-turnkey-core_12.0-1_i386.tar.gz', hostname='test', storage='local', memory=512, swap=512, cpus=1, disk=4, password='secret', ip_address='10.0.100.222') cmd, options = self._split_cmd(self._get_called_cmd()) eq_(cmd, 'create /nodes/proxmox/openvz') ok_("-cpus '1'" in options) ok_("-disk '4'" in options) ok_("-hostname 'test'" in options) ok_("-ip_address '10.0.100.222'" in options) ok_("-memory '512'" in options) ok_("-ostemplate 'local:vztmpl/debian-6-turnkey-core_12.0-1_i386.tar.gz'" in options) ok_("-password 'secret'" in options) ok_("-storage 'local'" in options) ok_("-swap '512'" in options) ok_("-vmid '800'" in options) self._set_stderr("200 OK") node = self.proxmox.nodes('proxmox1') node.openvz.post(vmid=900, ostemplate='local:vztmpl/debian-7-turnkey-core_12.0-1_i386.tar.gz', hostname='test1', storage='local1', memory=1024, swap=1024, cpus=2, disk=8, password='secret1', ip_address='10.0.100.111') cmd, options = self._split_cmd(self._get_called_cmd()) eq_(cmd, 'create /nodes/proxmox1/openvz') ok_("-cpus '2'" in options) ok_("-disk '8'" in options) ok_("-hostname 'test1'" in options) ok_("-ip_address '10.0.100.111'" in options) ok_("-memory '1024'" in options) ok_("-ostemplate 'local:vztmpl/debian-7-turnkey-core_12.0-1_i386.tar.gz'" in options) ok_("-password 'secret1'" in options) ok_("-storage 'local1'" in options) ok_("-swap '1024'" in options) ok_("-vmid '900'" in options) def test_put(self): self._set_stderr("200 OK") node = self.proxmox.nodes('proxmox') node.openvz(101).config.set(cpus=4, memory=1024, ip_address='10.0.100.100', onboot=True) cmd, options = self._split_cmd(self._get_called_cmd()) eq_(cmd, 'set /nodes/proxmox/openvz/101/config') ok_("-memory '1024'" in options) ok_("-ip_address '10.0.100.100'" in options) ok_("-onboot 'True'" in options) ok_("-cpus '4'" in options) self._set_stderr("200 OK") node = self.proxmox.nodes('proxmox1') node.openvz('102').config.put(cpus=2, memory=512, ip_address='10.0.100.200', onboot=False) cmd, options = self._split_cmd(self._get_called_cmd()) eq_(cmd, 'set /nodes/proxmox1/openvz/102/config') ok_("-memory '512'" in options) ok_("-ip_address '10.0.100.200'" in options) ok_("-onboot 'False'" in options) ok_("-cpus '2'" in options) @raises(ResourceException) def test_error(self): self._set_stderr("500 whoops") self.proxmox.nodes('proxmox').get() def test_no_error_with_extra_output(self): self._set_stderr("Extra output\n200 OK") self.proxmox.nodes('proxmox').get() @raises(ResourceException) def test_error_with_extra_output(self): self._set_stderr("Extra output\n500 whoops") self.proxmox.nodes('proxmox').get() def _called_cmd(self, cmd): called_cmd = cmd if self.sudo: called_cmd = 'sudo ' + cmd return called_cmd proxmoxer-1.2.0/tests/https_tests.py000066400000000000000000000172011413017536700176650ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' from mock import patch, MagicMock from nose.tools import eq_, ok_, assert_raises from proxmoxer import ProxmoxAPI @patch('requests.sessions.Session') def test_https_connection(req_session): response = {'ticket': 'ticket', 'CSRFPreventionToken': 'CSRFPreventionToken'} req_session.request.return_value = response ProxmoxAPI('proxmox', user='root@pam', password='secret', port=123, verify_ssl=False) call = req_session.return_value.request.call_args[1] eq_(call['url'], 'https://proxmox:123/api2/json/access/ticket') eq_(call['data'], {'username': 'root@pam', 'password': 'secret'}) eq_(call['method'], 'post') eq_(call['verify'], False) @patch('requests.sessions.Session') def test_https_connection_with_port_in_host(req_session): response = {'ticket': 'ticket', 'CSRFPreventionToken': 'CSRFPreventionToken'} req_session.request.return_value = response ProxmoxAPI('proxmox:123', user='root@pam', password='secret', port=124, verify_ssl=False) call = req_session.return_value.request.call_args[1] eq_(call['url'], 'https://proxmox:123/api2/json/access/ticket') eq_(call['data'], {'username': 'root@pam', 'password': 'secret'}) eq_(call['method'], 'post') eq_(call['verify'], False) @patch('requests.sessions.Session') def test_https_connection_with_bad_port_in_host(req_session): response = {'ticket': 'ticket', 'CSRFPreventionToken': 'CSRFPreventionToken'} req_session.request.return_value = response ProxmoxAPI('proxmox:notaport', user='root@pam', password='secret', port=124, verify_ssl=False) call = req_session.return_value.request.call_args[1] eq_(call['url'], 'https://proxmox:124/api2/json/access/ticket') eq_(call['data'], {'username': 'root@pam', 'password': 'secret'}) eq_(call['method'], 'post') eq_(call['verify'], False) def test_https_api_token(): p = ProxmoxAPI('proxmox', user='root@pam', token_name='test', token_value='ab27beeb-9ac4-4df1-aa19-62639f27031e', verify_ssl=False) eq_(p.get_tokens()[0], None) eq_(p.get_tokens()[1], None) def test_https_pmg_token(): with assert_raises(NotImplementedError): ProxmoxAPI('proxmox', user='root@pam', token_name='test', token_value='ab27beeb-9ac4-4df1-aa19-62639f27031e', verify_ssl=False, service='PMG') def test_https_invalid_service(): with assert_raises(NotImplementedError): ProxmoxAPI('nothing', user='root@pam', token_name='test', token_value='ab27beeb-9ac4-4df1-aa19-62639f27031e', verify_ssl=False, service='asdf') @patch('requests.sessions.Session') def test_pbs_https_connection(req_session): response = {'ticket': 'ticket', 'CSRFPreventionToken': 'CSRFPreventionToken'} req_session.request.return_value = response ProxmoxAPI('proxmox', user='root@pam', password='secret', verify_ssl=False, service='pbs') call = req_session.return_value.request.call_args[1] eq_(call['url'], 'https://proxmox:8007/api2/json/access/ticket') eq_(call['data'], {'username': 'root@pam', 'password': 'secret'}) eq_(call['method'], 'post') eq_(call['verify'], False) class TestSuite(): proxmox = None serializer = None session = None # noinspection PyMethodOverriding @patch('requests.sessions.Session') def setUp(self, session): response = {'ticket': 'ticket', 'CSRFPreventionToken': 'CSRFPreventionToken'} session.request.return_value = response self.proxmox = ProxmoxAPI('proxmox', user='root@pam', password='secret', port=123, verify_ssl=False) self.serializer = MagicMock() self.session = MagicMock() self.session.request.return_value.status_code = 200 self.proxmox._store['session'] = self.session self.proxmox._store['serializer'] = self.serializer def test_get(self): self.proxmox.nodes('proxmox').storage('local').get() eq_(self.session.request.call_args[0], ('GET', 'https://proxmox:123/api2/json/nodes/proxmox/storage/local')) def test_delete(self): self.proxmox.nodes('proxmox').openvz(100).delete() eq_(self.session.request.call_args[0], ('DELETE', 'https://proxmox:123/api2/json/nodes/proxmox/openvz/100')) self.proxmox.nodes('proxmox').openvz('101').delete() eq_(self.session.request.call_args[0], ('DELETE', 'https://proxmox:123/api2/json/nodes/proxmox/openvz/101')) def test_post(self): node = self.proxmox.nodes('proxmox') node.openvz.create(vmid=800, ostemplate='local:vztmpl/debian-6-turnkey-core_12.0-1_i386.tar.gz', hostname='test', storage='local', memory=512, swap=512, cpus=1, disk=4, password='secret', ip_address='10.0.100.222') eq_(self.session.request.call_args[0], ('POST', 'https://proxmox:123/api2/json/nodes/proxmox/openvz')) ok_('data' in self.session.request.call_args[1]) data = self.session.request.call_args[1]['data'] eq_(data['cpus'], 1) eq_(data['disk'], 4) eq_(data['hostname'], 'test') eq_(data['ip_address'], '10.0.100.222') eq_(data['memory'], 512) eq_(data['ostemplate'], 'local:vztmpl/debian-6-turnkey-core_12.0-1_i386.tar.gz') eq_(data['password'], 'secret') eq_(data['storage'], 'local') eq_(data['swap'], 512) eq_(data['vmid'], 800) node = self.proxmox.nodes('proxmox1') node.openvz.post(vmid=900, ostemplate='local:vztmpl/debian-7-turnkey-core_12.0-1_i386.tar.gz', hostname='test1', storage='local1', memory=1024, swap=1024, cpus=2, disk=8, password='secret1', ip_address='10.0.100.111') eq_(self.session.request.call_args[0], ('POST', 'https://proxmox:123/api2/json/nodes/proxmox1/openvz')) ok_('data' in self.session.request.call_args[1]) data = self.session.request.call_args[1]['data'] eq_(data['cpus'], 2) eq_(data['disk'], 8) eq_(data['hostname'], 'test1') eq_(data['ip_address'], '10.0.100.111') eq_(data['memory'], 1024) eq_(data['ostemplate'], 'local:vztmpl/debian-7-turnkey-core_12.0-1_i386.tar.gz') eq_(data['password'], 'secret1') eq_(data['storage'], 'local1') eq_(data['swap'], 1024) eq_(data['vmid'], 900) def test_put(self): node = self.proxmox.nodes('proxmox') node.openvz(101).config.set(cpus=4, memory=1024, ip_address='10.0.100.100', onboot=True) eq_(self.session.request.call_args[0], ('PUT', 'https://proxmox:123/api2/json/nodes/proxmox/openvz/101/config')) data = self.session.request.call_args[1]['data'] eq_(data['cpus'], 4) eq_(data['memory'], 1024) eq_(data['ip_address'], '10.0.100.100') eq_(data['onboot'], True) node = self.proxmox.nodes('proxmox1') node.openvz(102).config.put(cpus=2, memory=512, ip_address='10.0.100.200', onboot=False) eq_(self.session.request.call_args[0], ('PUT', 'https://proxmox:123/api2/json/nodes/proxmox1/openvz/102/config')) data = self.session.request.call_args[1]['data'] eq_(data['cpus'], 2) eq_(data['memory'], 512) eq_(data['ip_address'], '10.0.100.200') eq_(data['onboot'], False) proxmoxer-1.2.0/tests/openssh_tests.py000066400000000000000000000015331413017536700202030ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' from mock import patch from proxmoxer import ProxmoxAPI from tests.base.base_ssh_suite import BaseSSHSuite class TestOpenSSHSuite(BaseSSHSuite): proxmox = None client = None # noinspection PyMethodOverriding @patch('openssh_wrapper.SSHConnection') def setUp(self, _): self.proxmox = ProxmoxAPI('proxmox', user='root', backend='openssh', port=123) self.client = self.proxmox._store['session'].ssh_client self._set_stderr('200 OK') self._set_stdout('') def _get_called_cmd(self): return self.client.run.call_args[0][0] def _set_stdout(self, stdout): self.client.run.return_value.stdout = stdout def _set_stderr(self, stderr): self.client.run.return_value.stderr = stderr proxmoxer-1.2.0/tests/paramiko_tests.py000066400000000000000000000056651413017536700203410ustar00rootroot00000000000000__author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' import io from mock import patch from nose.tools import eq_, assert_raises from proxmoxer import ProxmoxAPI from tests.base.base_ssh_suite import BaseSSHSuite @patch('paramiko.SSHClient') def test_paramiko_connection(_): proxmox = ProxmoxAPI('proxmox', user='root', backend='ssh_paramiko', port=123) session = proxmox._store['session'] eq_(session.ssh_client.connect.call_args[0], ('proxmox',)) eq_(session.ssh_client.connect.call_args[1], {'username': 'root', 'allow_agent': True, 'key_filename': None, 'look_for_keys': True, 'timeout': 5, 'password': None, 'port': 123}) @patch('paramiko.SSHClient') def test_paramiko_invalid_backend(_): with assert_raises(NotImplementedError): ProxmoxAPI('proxmox', user='root', backend='ssh_paramiko', port=123, service='PBS') @patch('paramiko.SSHClient') def test_paramiko_tokens(_): p = ProxmoxAPI('proxmox', user='root', backend='ssh_paramiko', port=123) eq_(p.get_tokens()[0], None) eq_(p.get_tokens()[1], None) class TestParamikoSuite(BaseSSHSuite): # noinspection PyMethodOverriding @patch('paramiko.SSHClient') def setUp(self, _): self.proxmox = ProxmoxAPI('proxmox', user='root', backend='ssh_paramiko', port=123) self.client = self.proxmox._store['session'].ssh_client self.session = self.client.get_transport().open_session() self._set_stderr('200 OK') self._set_stdout('') def _get_called_cmd(self): return self.session.exec_command.call_args[0][0] def _set_stdout(self, stdout): self.session.makefile.return_value = io.BytesIO(stdout.encode('utf-8')) def _set_stderr(self, stderr): self.session.makefile_stderr.return_value = io.BytesIO(stderr.encode('utf-8')) class TestParamikoSuiteWithSudo(BaseSSHSuite): # noinspection PyMethodOverriding @patch('paramiko.SSHClient') def setUp(self, _): super(TestParamikoSuiteWithSudo, self).__init__(sudo=True) self.proxmox = ProxmoxAPI('proxmox', user='root', backend='ssh_paramiko', port=123, sudo=True) self.client = self.proxmox._store['session'].ssh_client self.session = self.client.get_transport().open_session() self._set_stderr('200 OK') self._set_stdout('') def _get_called_cmd(self): return self.session.exec_command.call_args[0][0] def _set_stdout(self, stdout): self.session.makefile.return_value = io.BytesIO(stdout.encode('utf-8')) def _set_stderr(self, stderr): self.session.makefile_stderr.return_value = io.BytesIO(stderr.encode('utf-8'))