├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── proxmoxer ├── __init__.py ├── backends │ ├── __init__.py │ ├── base_ssh.py │ ├── https.py │ ├── openssh.py │ └── ssh_paramiko.py └── core.py ├── setup.py ├── test_requirements.txt └── tests ├── __init__.py ├── base ├── __init__.py └── base_ssh_suite.py ├── https_tests.py ├── openssh_tests.py └── paramiko_tests.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = proxmoxer 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env/ 2 | *.egg-info/ 3 | *.pyc 4 | .idea 5 | dist 6 | build 7 | .venv 8 | .coverage 9 | README.txt 10 | test.py 11 | upload.sh 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.4 5 | install: 6 | - pip install . 7 | - pip install -r test_requirements.txt 8 | script: 9 | - nosetests --with-coverage --cover-erase --cover-branches --cover-package=proxmoxer -w tests 10 | after_success: 11 | - coveralls 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Oleg Butovich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.txt 3 | include README.rst 4 | global-exclude *.orig *.pyc *.log *.swp 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================================= 2 | Proxmoxer: A wrapper for Proxmox REST API 3 | ========================================= 4 | 5 | repository is moved to https://github.com/proxmoxer/proxmoxer 6 | -------------------------------------------------------------------------------- /proxmoxer/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __version__ = '1.0.3' 4 | __licence__ = 'MIT' 5 | 6 | from .core import * 7 | -------------------------------------------------------------------------------- /proxmoxer/backends/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | -------------------------------------------------------------------------------- /proxmoxer/backends/base_ssh.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | 6 | from itertools import chain 7 | import json 8 | import re 9 | 10 | 11 | class Response(object): 12 | def __init__(self, content, status_code): 13 | self.status_code = status_code 14 | self.content = content 15 | self.headers = {"content-type": "application/json"} 16 | 17 | 18 | class ProxmoxBaseSSHSession(object): 19 | 20 | def _exec(self, cmd): 21 | raise NotImplementedError() 22 | 23 | # noinspection PyUnusedLocal 24 | def request(self, method, url, data=None, params=None, headers=None): 25 | method = method.lower() 26 | data = data or {} 27 | params = params or {} 28 | url = url.strip() 29 | 30 | cmd = {'post': 'create', 31 | 'put': 'set'}.get(method, method) 32 | 33 | #for 'upload' call some workaround 34 | tmp_filename = '' 35 | if url.endswith('upload'): 36 | #copy file to temporary location on proxmox host 37 | tmp_filename, _ = self._exec( 38 | "python -c 'import tempfile; import sys; tf = tempfile.NamedTemporaryFile(); sys.stdout.write(tf.name)'") 39 | self.upload_file_obj(data['filename'], tmp_filename) 40 | data['filename'] = data['filename'].name 41 | data['tmpfilename'] = tmp_filename 42 | 43 | translated_data = ' '.join(["-{0} {1}".format(k, v) for k, v in chain(data.items(), params.items())]) 44 | full_cmd = 'pvesh {0}'.format(' '.join(filter(None, (cmd, url, translated_data)))) 45 | 46 | stdout, stderr = self._exec(full_cmd) 47 | match = lambda s: re.match('\d\d\d [a-zA-Z]', s) 48 | # sometimes contains extra text like 'trying to acquire lock...OK' 49 | status_code = next( 50 | (int(s.split()[0]) for s in stderr.splitlines() if match(s)), 51 | 500) 52 | if stdout: 53 | return Response(stdout, status_code) 54 | else: 55 | return Response(stderr, status_code) 56 | 57 | def upload_file_obj(self, file_obj, remote_path): 58 | raise NotImplementedError() 59 | 60 | 61 | class JsonSimpleSerializer(object): 62 | 63 | def loads(self, response): 64 | try: 65 | return json.loads(response.content) 66 | except ValueError: 67 | return response.content 68 | 69 | 70 | class BaseBackend(object): 71 | 72 | def get_session(self): 73 | return self.session 74 | 75 | def get_base_url(self): 76 | return '' 77 | 78 | def get_serializer(self): 79 | return JsonSimpleSerializer() 80 | -------------------------------------------------------------------------------- /proxmoxer/backends/https.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | 6 | import json 7 | import sys 8 | 9 | try: 10 | import requests 11 | urllib3 = requests.packages.urllib3 12 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 13 | from requests.auth import AuthBase 14 | from requests.cookies import cookiejar_from_dict 15 | except ImportError: 16 | import sys 17 | sys.stderr.write("Chosen backend requires 'requests' module\n") 18 | sys.exit(1) 19 | 20 | 21 | if sys.version_info[0] >= 3: 22 | import io 23 | def is_file(obj): return isinstance(obj, io.IOBase) 24 | else: 25 | def is_file(obj): return isinstance(obj, file) 26 | 27 | 28 | class AuthenticationError(Exception): 29 | def __init__(self, msg): 30 | super(AuthenticationError, self).__init__(msg) 31 | self.msg = msg 32 | 33 | def __str__(self): 34 | return self.msg 35 | 36 | def __repr__(self): 37 | return self.__str__() 38 | 39 | 40 | class ProxmoxHTTPAuth(AuthBase): 41 | def __init__(self, base_url, username, password, verify_ssl=False): 42 | response_data = requests.post(base_url + "/access/ticket", 43 | verify=verify_ssl, 44 | data={"username": username, "password": password}).json()["data"] 45 | if response_data is None: 46 | raise AuthenticationError("Couldn't authenticate user: {0} to {1}".format(username, base_url + "/access/ticket")) 47 | 48 | self.pve_auth_cookie = response_data["ticket"] 49 | self.csrf_prevention_token = response_data["CSRFPreventionToken"] 50 | 51 | def __call__(self, r): 52 | r.headers["CSRFPreventionToken"] = self.csrf_prevention_token 53 | return r 54 | 55 | 56 | class ProxmoxHTTPTokenAuth(ProxmoxHTTPAuth): 57 | """Use existing ticket/token to create a session. 58 | 59 | Overrides ProxmoxHTTPAuth so that an existing auth cookie and csrf token 60 | may be used instead of passing username/password. 61 | """ 62 | def __init__(self, auth_token, csrf_token): 63 | self.pve_auth_cookie = auth_token 64 | self.csrf_prevention_token = csrf_token 65 | 66 | 67 | class JsonSerializer(object): 68 | 69 | content_types = [ 70 | "application/json", 71 | "application/x-javascript", 72 | "text/javascript", 73 | "text/x-javascript", 74 | "text/x-json" 75 | ] 76 | 77 | def get_accept_types(self): 78 | return ", ".join(self.content_types) 79 | 80 | def loads(self, response): 81 | try: 82 | return json.loads(response.content.decode('utf-8'))['data'] 83 | except (UnicodeDecodeError, ValueError): 84 | return response.content 85 | 86 | 87 | class ProxmoxHttpSession(requests.Session): 88 | 89 | def request(self, method, url, params=None, data=None, headers=None, cookies=None, files=None, auth=None, 90 | timeout=None, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None, 91 | serializer=None): 92 | 93 | # take set verify flag from session request does not have this parameter explicitly 94 | if verify is None: 95 | verify = self.verify 96 | 97 | #filter out streams 98 | files = files or {} 99 | data = data or {} 100 | for k, v in data.copy().items(): 101 | if is_file(v): 102 | files[k] = v 103 | del data[k] 104 | 105 | headers = None 106 | if not files and serializer: 107 | headers = {"content-type": 'application/x-www-form-urlencoded'} 108 | 109 | return super(ProxmoxHttpSession, self).request(method, url, params, data, headers, cookies, files, auth, 110 | timeout, allow_redirects, proxies, hooks, stream, verify, cert) 111 | 112 | 113 | class Backend(object): 114 | def __init__(self, host, user, password, port=8006, verify_ssl=True, 115 | mode='json', timeout=5, auth_token=None, csrf_token=None): 116 | if ':' in host: 117 | host, host_port = host.split(':') 118 | port = host_port if host_port.isdigit() else port 119 | 120 | self.base_url = "https://{0}:{1}/api2/{2}".format(host, port, mode) 121 | 122 | if auth_token is not None: 123 | self.auth = ProxmoxHTTPTokenAuth(auth_token, csrf_token) 124 | else: 125 | self.auth = ProxmoxHTTPAuth(self.base_url, user, password, verify_ssl) 126 | self.verify_ssl = verify_ssl 127 | self.mode = mode 128 | self.timeout = timeout 129 | 130 | def get_session(self): 131 | session = ProxmoxHttpSession() 132 | session.verify = self.verify_ssl 133 | session.auth = self.auth 134 | session.cookies = cookiejar_from_dict({"PVEAuthCookie": self.auth.pve_auth_cookie}) 135 | session.headers['Connection'] = 'keep-alive' 136 | session.headers["accept"] = self.get_serializer().get_accept_types() 137 | return session 138 | 139 | def get_base_url(self): 140 | return self.base_url 141 | 142 | def get_serializer(self): 143 | assert self.mode == 'json' 144 | return JsonSerializer() 145 | 146 | def get_tokens(self): 147 | """Return the in-use auth and csrf tokens.""" 148 | return self.auth.pve_auth_cookie, self.auth.csrf_prevention_token 149 | -------------------------------------------------------------------------------- /proxmoxer/backends/openssh.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | 6 | from proxmoxer.backends.base_ssh import ProxmoxBaseSSHSession, BaseBackend 7 | 8 | try: 9 | import openssh_wrapper 10 | except ImportError: 11 | import sys 12 | sys.stderr.write("Chosen backend requires 'openssh_wrapper' module\n") 13 | sys.exit(1) 14 | 15 | 16 | class ProxmoxOpenSSHSession(ProxmoxBaseSSHSession): 17 | def __init__(self, host, 18 | username, 19 | configfile=None, 20 | port=22, 21 | timeout=5, 22 | forward_ssh_agent=False, 23 | sudo=False, 24 | identity_file=None): 25 | self.host = host 26 | self.username = username 27 | self.configfile = configfile 28 | self.port = port 29 | self.timeout = timeout 30 | self.forward_ssh_agent = forward_ssh_agent 31 | self.sudo = sudo 32 | self.identity_file = identity_file 33 | self.ssh_client = openssh_wrapper.SSHConnection(self.host, 34 | login=self.username, 35 | port=self.port, 36 | timeout=self.timeout, 37 | identity_file=self.identity_file) 38 | 39 | def _exec(self, cmd): 40 | if self.sudo: 41 | cmd = "sudo " + cmd 42 | ret = self.ssh_client.run(cmd, forward_ssh_agent=self.forward_ssh_agent) 43 | return ret.stdout, ret.stderr 44 | 45 | def upload_file_obj(self, file_obj, remote_path): 46 | self.ssh_client.scp((file_obj,), target=remote_path) 47 | 48 | 49 | class Backend(BaseBackend): 50 | def __init__(self, host, user, configfile=None, port=22, timeout=5, forward_ssh_agent=False, sudo=False, identity_file=None): 51 | self.session = ProxmoxOpenSSHSession(host, user, 52 | configfile=configfile, 53 | port=port, 54 | timeout=timeout, 55 | forward_ssh_agent=forward_ssh_agent, 56 | sudo=sudo, 57 | identity_file=identity_file) 58 | -------------------------------------------------------------------------------- /proxmoxer/backends/ssh_paramiko.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | 6 | import os 7 | from proxmoxer.backends.base_ssh import ProxmoxBaseSSHSession, BaseBackend 8 | 9 | try: 10 | import paramiko 11 | except ImportError: 12 | import sys 13 | sys.stderr.write("Chosen backend requires 'paramiko' module\n") 14 | sys.exit(1) 15 | 16 | 17 | class ProxmoxParamikoSession(ProxmoxBaseSSHSession): 18 | def __init__(self, host, 19 | username, 20 | password=None, 21 | private_key_file=None, 22 | port=22, 23 | timeout=5, 24 | sudo=False): 25 | self.host = host 26 | self.username = username 27 | self.password = password 28 | self.private_key_file = private_key_file 29 | self.port = port 30 | self.timeout = timeout 31 | self.sudo = sudo 32 | self.ssh_client = self._connect() 33 | 34 | def _connect(self): 35 | ssh_client = paramiko.SSHClient() 36 | ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 37 | 38 | if self.private_key_file: 39 | key_filename = os.path.expanduser(self.private_key_file) 40 | else: 41 | key_filename = None 42 | 43 | ssh_client.connect(self.host, 44 | username=self.username, 45 | allow_agent=(not self.password), 46 | look_for_keys=True, 47 | key_filename=key_filename, 48 | password=self.password, 49 | timeout=self.timeout, 50 | port=self.port) 51 | 52 | return ssh_client 53 | 54 | def _exec(self, cmd): 55 | if self.sudo: 56 | cmd = 'sudo ' + cmd 57 | session = self.ssh_client.get_transport().open_session() 58 | session.exec_command(cmd) 59 | stdout = session.makefile('rb', -1).read().decode() 60 | stderr = session.makefile_stderr('rb', -1).read().decode() 61 | return stdout, stderr 62 | 63 | def upload_file_obj(self, file_obj, remote_path): 64 | sftp = self.ssh_client.open_sftp() 65 | sftp.putfo(file_obj, remote_path) 66 | sftp.close() 67 | 68 | 69 | class Backend(BaseBackend): 70 | def __init__(self, host, user, password=None, private_key_file=None, port=22, timeout=5, sudo=False): 71 | self.session = ProxmoxParamikoSession(host, user, 72 | password=password, 73 | private_key_file=private_key_file, 74 | port=port, 75 | timeout=timeout, 76 | sudo=sudo) 77 | 78 | 79 | -------------------------------------------------------------------------------- /proxmoxer/core.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | import importlib 6 | import posixpath 7 | import logging 8 | 9 | # Python 3 compatibility: 10 | try: 11 | import httplib 12 | except ImportError: # py3 13 | from http import client as httplib 14 | try: 15 | import urlparse 16 | except ImportError: # py3 17 | from urllib import parse as urlparse 18 | try: 19 | basestring 20 | except NameError: # py3 21 | basestring = (bytes, str) 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | class ProxmoxResourceBase(object): 27 | 28 | def __getattr__(self, item): 29 | if item.startswith("_"): 30 | raise AttributeError(item) 31 | 32 | kwargs = self._store.copy() 33 | kwargs['base_url'] = self.url_join(self._store["base_url"], item) 34 | 35 | return ProxmoxResource(**kwargs) 36 | 37 | def url_join(self, base, *args): 38 | scheme, netloc, path, query, fragment = urlparse.urlsplit(base) 39 | path = path if len(path) else "/" 40 | path = posixpath.join(path, *[('%s' % x) for x in args]) 41 | return urlparse.urlunsplit([scheme, netloc, path, query, fragment]) 42 | 43 | 44 | class ResourceException(Exception): 45 | pass 46 | 47 | 48 | class ProxmoxResource(ProxmoxResourceBase): 49 | 50 | def __init__(self, **kwargs): 51 | self._store = kwargs 52 | 53 | def __call__(self, resource_id=None): 54 | if not resource_id: 55 | return self 56 | 57 | if isinstance(resource_id, basestring): 58 | resource_id = resource_id.split("/") 59 | elif not isinstance(resource_id, (tuple, list)): 60 | resource_id = [str(resource_id)] 61 | 62 | kwargs = self._store.copy() 63 | if resource_id is not None: 64 | kwargs["base_url"] = self.url_join(self._store["base_url"], *resource_id) 65 | 66 | return self.__class__(**kwargs) 67 | 68 | def _request(self, method, data=None, params=None): 69 | url = self._store["base_url"] 70 | if data: 71 | logger.info('%s %s %r', method, url, data) 72 | else: 73 | logger.info('%s %s', method, url) 74 | resp = self._store["session"].request(method, url, data=data or None, params=params) 75 | logger.debug('Status code: %s, output: %s', resp.status_code, resp.content) 76 | 77 | if resp.status_code >= 400: 78 | raise ResourceException("{0} {1}: {2}".format(resp.status_code, httplib.responses[resp.status_code], 79 | resp.content)) 80 | elif 200 <= resp.status_code <= 299: 81 | return self._store["serializer"].loads(resp) 82 | 83 | def get(self, *args, **params): 84 | return self(args)._request("GET", params=params) 85 | 86 | def post(self, *args, **data): 87 | return self(args)._request("POST", data=data) 88 | 89 | def put(self, *args, **data): 90 | return self(args)._request("PUT", data=data) 91 | 92 | def delete(self, *args, **params): 93 | return self(args)._request("DELETE", params=params) 94 | 95 | def create(self, *args, **data): 96 | return self.post(*args, **data) 97 | 98 | def set(self, *args, **data): 99 | return self.put(*args, **data) 100 | 101 | 102 | class ProxmoxAPI(ProxmoxResourceBase): 103 | def __init__(self, host, backend='https', **kwargs): 104 | 105 | #load backend module 106 | self._backend = importlib.import_module('.backends.%s' % backend, 'proxmoxer').Backend(host, **kwargs) 107 | self._backend_name = backend 108 | 109 | self._store = { 110 | "base_url": self._backend.get_base_url(), 111 | "session": self._backend.get_session(), 112 | "serializer": self._backend.get_serializer(), 113 | } 114 | 115 | def get_tokens(self): 116 | """Return the auth and csrf tokens. 117 | 118 | Returns (None, None) if the backend is not https. 119 | """ 120 | if self._backend_name != 'https': 121 | return None, None 122 | 123 | return self._backend.get_tokens() 124 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import codecs 3 | import re 4 | import sys 5 | import proxmoxer 6 | import os 7 | from setuptools import setup 8 | 9 | 10 | if not os.path.exists('README.txt') and 'sdist' in sys.argv: 11 | with codecs.open('README.rst', encoding='utf8') as f: 12 | rst = f.read() 13 | code_block = '(:\n\n)?\.\. code-block::.*' 14 | rst = re.sub(code_block, '::', rst) 15 | with codecs.open('README.txt', encoding='utf8', mode='wb') as f: 16 | f.write(rst) 17 | 18 | 19 | try: 20 | readme = 'README.txt' if os.path.exists('README.txt') else 'README.rst' 21 | long_description = codecs.open(readme, encoding='utf-8').read() 22 | except: 23 | long_description = 'Could not read README.txt' 24 | 25 | 26 | setup( 27 | name = 'proxmoxer', 28 | version = proxmoxer.__version__, 29 | description = 'Python Wrapper for the Proxmox 2.x API (HTTP and SSH)', 30 | author = 'Oleg Butovich', 31 | author_email = 'obutovich@gmail.com', 32 | license = "MIT", 33 | url = 'https://github.com/swayf/proxmoxer', 34 | download_url = 'http://pypi.python.org/pypi/proxmoxer', 35 | keywords = ['proxmox', 'api'], 36 | packages=['proxmoxer', 'proxmoxer.backends', 'tests', 'tests.base'], 37 | classifiers = [ #http://pypi.python.org/pypi?%3Aaction=list_classifiers 38 | "Development Status :: 4 - Beta", 39 | "Programming Language :: Python", 40 | "Programming Language :: Python :: 2", 41 | "Programming Language :: Python :: 2.7", 42 | "Programming Language :: Python :: 3", 43 | "Programming Language :: Python :: 3.4", 44 | "Intended Audience :: Developers", 45 | "Intended Audience :: System Administrators", 46 | "License :: OSI Approved :: MIT License", 47 | "Operating System :: OS Independent", 48 | "Topic :: Software Development :: Libraries :: Python Modules", 49 | ], 50 | long_description = long_description 51 | ) 52 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | nose 3 | requests < 2.9 4 | coveralls 5 | paramiko 6 | openssh_wrapper 7 | 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | -------------------------------------------------------------------------------- /tests/base/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | -------------------------------------------------------------------------------- /tests/base/base_ssh_suite.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | from itertools import islice 6 | 7 | try: 8 | import itertools.izip as zip 9 | except ImportError: 10 | pass 11 | 12 | from nose.tools import eq_, ok_, raises 13 | from proxmoxer.core import ResourceException 14 | 15 | class BaseSSHSuite(object): 16 | proxmox = None 17 | client = None 18 | session = None 19 | 20 | def __init__(self, sudo=False): 21 | self.sudo = sudo 22 | 23 | def _split_cmd(self, cmd): 24 | splitted = cmd.split() 25 | if not self.sudo: 26 | eq_(splitted[0], 'pvesh') 27 | else: 28 | eq_(splitted[0], 'sudo') 29 | eq_(splitted[1], 'pvesh') 30 | splitted.pop(0) 31 | options_set = set((' '.join((k, v)) for k, v in 32 | zip(islice(splitted, 3, None, 2), 33 | islice(splitted, 4, None, 2)))) 34 | return ' '.join(splitted[1:3]), options_set 35 | 36 | def _get_called_cmd(self): 37 | raise NotImplementedError() 38 | 39 | def _set_stdout(self, stdout): 40 | raise NotImplementedError() 41 | 42 | def _set_stderr(self, stderr): 43 | raise NotImplementedError() 44 | 45 | def test_get(self): 46 | self._set_stdout(""" 47 | [ 48 | { 49 | "subdir" : "status" 50 | }, 51 | { 52 | "subdir" : "content" 53 | }, 54 | { 55 | "subdir" : "upload" 56 | }, 57 | { 58 | "subdir" : "rrd" 59 | }, 60 | { 61 | "subdir" : "rrddata" 62 | } 63 | ]""") 64 | result = self.proxmox.nodes('proxmox').storage('local').get() 65 | eq_(self._get_called_cmd(), self._called_cmd('pvesh get /nodes/proxmox/storage/local')) 66 | eq_(result[0]['subdir'], 'status') 67 | eq_(result[1]['subdir'], 'content') 68 | eq_(result[2]['subdir'], 'upload') 69 | eq_(result[3]['subdir'], 'rrd') 70 | eq_(result[4]['subdir'], 'rrddata') 71 | 72 | def test_delete(self): 73 | self.proxmox.nodes('proxmox').openvz(100).delete() 74 | eq_(self._get_called_cmd(), self._called_cmd('pvesh delete /nodes/proxmox/openvz/100')) 75 | self._set_stderr("200 OK") 76 | self.proxmox.nodes('proxmox').openvz('101').delete() 77 | eq_(self._get_called_cmd(), self._called_cmd('pvesh delete /nodes/proxmox/openvz/101')) 78 | self._set_stderr("200 OK") 79 | self.proxmox.nodes('proxmox').openvz.delete('102') 80 | eq_(self._get_called_cmd(), self._called_cmd('pvesh delete /nodes/proxmox/openvz/102')) 81 | 82 | def test_post(self): 83 | self._set_stderr("200 OK") 84 | node = self.proxmox.nodes('proxmox') 85 | node.openvz.create(vmid=800, 86 | ostemplate='local:vztmpl/debian-6-turnkey-core_12.0-1_i386.tar.gz', 87 | hostname='test', 88 | storage='local', 89 | memory=512, 90 | swap=512, 91 | cpus=1, 92 | disk=4, 93 | password='secret', 94 | ip_address='10.0.100.222') 95 | cmd, options = self._split_cmd(self._get_called_cmd()) 96 | eq_(cmd, 'create /nodes/proxmox/openvz') 97 | ok_('-cpus 1' in options) 98 | ok_('-disk 4' in options) 99 | ok_('-hostname test' in options) 100 | ok_('-ip_address 10.0.100.222' in options) 101 | ok_('-memory 512' in options) 102 | ok_('-ostemplate local:vztmpl/debian-6-turnkey-core_12.0-1_i386.tar.gz' in options) 103 | ok_('-password secret' in options) 104 | ok_('-storage local' in options) 105 | ok_('-swap 512' in options) 106 | ok_('-vmid 800' in options) 107 | 108 | self._set_stderr("200 OK") 109 | node = self.proxmox.nodes('proxmox1') 110 | node.openvz.post(vmid=900, 111 | ostemplate='local:vztmpl/debian-7-turnkey-core_12.0-1_i386.tar.gz', 112 | hostname='test1', 113 | storage='local1', 114 | memory=1024, 115 | swap=1024, 116 | cpus=2, 117 | disk=8, 118 | password='secret1', 119 | ip_address='10.0.100.111') 120 | cmd, options = self._split_cmd(self._get_called_cmd()) 121 | eq_(cmd, 'create /nodes/proxmox1/openvz') 122 | ok_('-cpus 2' in options) 123 | ok_('-disk 8' in options) 124 | ok_('-hostname test1' in options) 125 | ok_('-ip_address 10.0.100.111' in options) 126 | ok_('-memory 1024' in options) 127 | ok_('-ostemplate local:vztmpl/debian-7-turnkey-core_12.0-1_i386.tar.gz' in options) 128 | ok_('-password secret1' in options) 129 | ok_('-storage local1' in options) 130 | ok_('-swap 1024' in options) 131 | ok_('-vmid 900' in options) 132 | 133 | def test_put(self): 134 | self._set_stderr("200 OK") 135 | node = self.proxmox.nodes('proxmox') 136 | node.openvz(101).config.set(cpus=4, memory=1024, ip_address='10.0.100.100', onboot=True) 137 | cmd, options = self._split_cmd(self._get_called_cmd()) 138 | eq_(cmd, 'set /nodes/proxmox/openvz/101/config') 139 | ok_('-memory 1024' in options) 140 | ok_('-ip_address 10.0.100.100' in options) 141 | ok_('-onboot True' in options) 142 | ok_('-cpus 4' in options) 143 | 144 | self._set_stderr("200 OK") 145 | node = self.proxmox.nodes('proxmox1') 146 | node.openvz('102').config.put(cpus=2, memory=512, ip_address='10.0.100.200', onboot=False) 147 | cmd, options = self._split_cmd(self._get_called_cmd()) 148 | eq_(cmd, 'set /nodes/proxmox1/openvz/102/config') 149 | ok_('-memory 512' in options) 150 | ok_('-ip_address 10.0.100.200' in options) 151 | ok_('-onboot False' in options) 152 | ok_('-cpus 2' in options) 153 | 154 | @raises(ResourceException) 155 | def test_error(self): 156 | self._set_stderr("500 whoops") 157 | self.proxmox.nodes('proxmox').get() 158 | 159 | def test_no_error_with_extra_output(self): 160 | self._set_stderr("Extra output\n200 OK") 161 | self.proxmox.nodes('proxmox').get() 162 | 163 | @raises(ResourceException) 164 | def test_error_with_extra_output(self): 165 | self._set_stderr("Extra output\n500 whoops") 166 | self.proxmox.nodes('proxmox').get() 167 | 168 | def _called_cmd(self, cmd): 169 | called_cmd = cmd 170 | if self.sudo: 171 | called_cmd = 'sudo ' + cmd 172 | return called_cmd 173 | -------------------------------------------------------------------------------- /tests/https_tests.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | from mock import patch, MagicMock 6 | from nose.tools import eq_, ok_ 7 | from proxmoxer import ProxmoxAPI 8 | 9 | 10 | @patch('requests.sessions.Session') 11 | def test_https_connection(req_session): 12 | response = {'ticket': 'ticket', 13 | 'CSRFPreventionToken': 'CSRFPreventionToken'} 14 | req_session.request.return_value = response 15 | ProxmoxAPI('proxmox', user='root@pam', password='secret', port=123, verify_ssl=False) 16 | call = req_session.return_value.request.call_args[1] 17 | eq_(call['url'], 'https://proxmox:123/api2/json/access/ticket') 18 | eq_(call['data'], {'username': 'root@pam', 'password': 'secret'}) 19 | eq_(call['method'], 'post') 20 | eq_(call['verify'], False) 21 | 22 | 23 | @patch('requests.sessions.Session') 24 | def test_https_connection_wth_port_in_host(req_session): 25 | response = {'ticket': 'ticket', 26 | 'CSRFPreventionToken': 'CSRFPreventionToken'} 27 | req_session.request.return_value = response 28 | ProxmoxAPI('proxmox:123', user='root@pam', password='secret', port=124, verify_ssl=False) 29 | call = req_session.return_value.request.call_args[1] 30 | eq_(call['url'], 'https://proxmox:123/api2/json/access/ticket') 31 | eq_(call['data'], {'username': 'root@pam', 'password': 'secret'}) 32 | eq_(call['method'], 'post') 33 | eq_(call['verify'], False) 34 | 35 | 36 | @patch('requests.sessions.Session') 37 | def test_https_connection_wth_bad_port_in_host(req_session): 38 | response = {'ticket': 'ticket', 39 | 'CSRFPreventionToken': 'CSRFPreventionToken'} 40 | req_session.request.return_value = response 41 | ProxmoxAPI('proxmox:notaport', user='root@pam', password='secret', port=124, verify_ssl=False) 42 | call = req_session.return_value.request.call_args[1] 43 | eq_(call['url'], 'https://proxmox:124/api2/json/access/ticket') 44 | eq_(call['data'], {'username': 'root@pam', 'password': 'secret'}) 45 | eq_(call['method'], 'post') 46 | eq_(call['verify'], False) 47 | 48 | 49 | class TestSuite(): 50 | proxmox = None 51 | serializer = None 52 | session = None 53 | 54 | # noinspection PyMethodOverriding 55 | @patch('requests.sessions.Session') 56 | def setUp(self, session): 57 | response = {'ticket': 'ticket', 58 | 'CSRFPreventionToken': 'CSRFPreventionToken'} 59 | session.request.return_value = response 60 | self.proxmox = ProxmoxAPI('proxmox', user='root@pam', password='secret', port=123, verify_ssl=False) 61 | self.serializer = MagicMock() 62 | self.session = MagicMock() 63 | self.session.request.return_value.status_code = 200 64 | self.proxmox._store['session'] = self.session 65 | self.proxmox._store['serializer'] = self.serializer 66 | 67 | def test_get(self): 68 | self.proxmox.nodes('proxmox').storage('local').get() 69 | eq_(self.session.request.call_args[0], ('GET', 'https://proxmox:123/api2/json/nodes/proxmox/storage/local')) 70 | 71 | def test_delete(self): 72 | self.proxmox.nodes('proxmox').openvz(100).delete() 73 | eq_(self.session.request.call_args[0], ('DELETE', 'https://proxmox:123/api2/json/nodes/proxmox/openvz/100')) 74 | self.proxmox.nodes('proxmox').openvz('101').delete() 75 | eq_(self.session.request.call_args[0], ('DELETE', 'https://proxmox:123/api2/json/nodes/proxmox/openvz/101')) 76 | 77 | def test_post(self): 78 | node = self.proxmox.nodes('proxmox') 79 | node.openvz.create(vmid=800, 80 | ostemplate='local:vztmpl/debian-6-turnkey-core_12.0-1_i386.tar.gz', 81 | hostname='test', 82 | storage='local', 83 | memory=512, 84 | swap=512, 85 | cpus=1, 86 | disk=4, 87 | password='secret', 88 | ip_address='10.0.100.222') 89 | eq_(self.session.request.call_args[0], ('POST', 'https://proxmox:123/api2/json/nodes/proxmox/openvz')) 90 | ok_('data' in self.session.request.call_args[1]) 91 | data = self.session.request.call_args[1]['data'] 92 | eq_(data['cpus'], 1) 93 | eq_(data['disk'], 4) 94 | eq_(data['hostname'], 'test') 95 | eq_(data['ip_address'], '10.0.100.222') 96 | eq_(data['memory'], 512) 97 | eq_(data['ostemplate'], 'local:vztmpl/debian-6-turnkey-core_12.0-1_i386.tar.gz') 98 | eq_(data['password'], 'secret') 99 | eq_(data['storage'], 'local') 100 | eq_(data['swap'], 512) 101 | eq_(data['vmid'], 800) 102 | 103 | node = self.proxmox.nodes('proxmox1') 104 | node.openvz.post(vmid=900, 105 | ostemplate='local:vztmpl/debian-7-turnkey-core_12.0-1_i386.tar.gz', 106 | hostname='test1', 107 | storage='local1', 108 | memory=1024, 109 | swap=1024, 110 | cpus=2, 111 | disk=8, 112 | password='secret1', 113 | ip_address='10.0.100.111') 114 | eq_(self.session.request.call_args[0], ('POST', 'https://proxmox:123/api2/json/nodes/proxmox1/openvz')) 115 | ok_('data' in self.session.request.call_args[1]) 116 | data = self.session.request.call_args[1]['data'] 117 | eq_(data['cpus'], 2) 118 | eq_(data['disk'], 8) 119 | eq_(data['hostname'], 'test1') 120 | eq_(data['ip_address'], '10.0.100.111') 121 | eq_(data['memory'], 1024) 122 | eq_(data['ostemplate'], 'local:vztmpl/debian-7-turnkey-core_12.0-1_i386.tar.gz') 123 | eq_(data['password'], 'secret1') 124 | eq_(data['storage'], 'local1') 125 | eq_(data['swap'], 1024) 126 | eq_(data['vmid'], 900) 127 | 128 | def test_put(self): 129 | node = self.proxmox.nodes('proxmox') 130 | node.openvz(101).config.set(cpus=4, memory=1024, ip_address='10.0.100.100', onboot=True) 131 | eq_(self.session.request.call_args[0], ('PUT', 'https://proxmox:123/api2/json/nodes/proxmox/openvz/101/config')) 132 | data = self.session.request.call_args[1]['data'] 133 | eq_(data['cpus'], 4) 134 | eq_(data['memory'], 1024) 135 | eq_(data['ip_address'], '10.0.100.100') 136 | eq_(data['onboot'], True) 137 | 138 | node = self.proxmox.nodes('proxmox1') 139 | node.openvz(102).config.put(cpus=2, memory=512, ip_address='10.0.100.200', onboot=False) 140 | eq_(self.session.request.call_args[0], 141 | ('PUT', 'https://proxmox:123/api2/json/nodes/proxmox1/openvz/102/config')) 142 | data = self.session.request.call_args[1]['data'] 143 | eq_(data['cpus'], 2) 144 | eq_(data['memory'], 512) 145 | eq_(data['ip_address'], '10.0.100.200') 146 | eq_(data['onboot'], False) 147 | 148 | -------------------------------------------------------------------------------- /tests/openssh_tests.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | from mock import patch 6 | from proxmoxer import ProxmoxAPI 7 | from tests.base.base_ssh_suite import BaseSSHSuite 8 | 9 | 10 | class TestOpenSSHSuite(BaseSSHSuite): 11 | proxmox = None 12 | client = None 13 | 14 | # noinspection PyMethodOverriding 15 | @patch('openssh_wrapper.SSHConnection') 16 | def setUp(self, _): 17 | self.proxmox = ProxmoxAPI('proxmox', user='root', backend='openssh', port=123) 18 | self.client = self.proxmox._store['session'].ssh_client 19 | self._set_stderr('200 OK') 20 | self._set_stdout('') 21 | 22 | def _get_called_cmd(self): 23 | return self.client.run.call_args[0][0] 24 | 25 | def _set_stdout(self, stdout): 26 | self.client.run.return_value.stdout = stdout 27 | 28 | def _set_stderr(self, stderr): 29 | self.client.run.return_value.stderr = stderr 30 | -------------------------------------------------------------------------------- /tests/paramiko_tests.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Oleg Butovich' 2 | __copyright__ = '(c) Oleg Butovich 2013-2017' 3 | __licence__ = 'MIT' 4 | 5 | import io 6 | from mock import patch 7 | from nose.tools import eq_ 8 | from proxmoxer import ProxmoxAPI 9 | from .base.base_ssh_suite import BaseSSHSuite 10 | 11 | 12 | @patch('paramiko.SSHClient') 13 | def test_paramiko_connection(_): 14 | proxmox = ProxmoxAPI('proxmox', user='root', backend='ssh_paramiko', port=123) 15 | session = proxmox._store['session'] 16 | eq_(session.ssh_client.connect.call_args[0], ('proxmox',)) 17 | eq_(session.ssh_client.connect.call_args[1], {'username': 'root', 18 | 'allow_agent': True, 19 | 'key_filename': None, 20 | 'look_for_keys': True, 21 | 'timeout': 5, 22 | 'password': None, 23 | 'port': 123}) 24 | 25 | 26 | class TestParamikoSuite(BaseSSHSuite): 27 | 28 | # noinspection PyMethodOverriding 29 | @patch('paramiko.SSHClient') 30 | def setUp(self, _): 31 | self.proxmox = ProxmoxAPI('proxmox', user='root', backend='ssh_paramiko', port=123) 32 | self.client = self.proxmox._store['session'].ssh_client 33 | self.session = self.client.get_transport().open_session() 34 | self._set_stderr('200 OK') 35 | self._set_stdout('') 36 | 37 | def _get_called_cmd(self): 38 | return self.session.exec_command.call_args[0][0] 39 | 40 | def _set_stdout(self, stdout): 41 | self.session.makefile.return_value = io.BytesIO(stdout.encode('utf-8')) 42 | 43 | def _set_stderr(self, stderr): 44 | self.session.makefile_stderr.return_value = io.BytesIO(stderr.encode('utf-8')) 45 | 46 | 47 | class TestParamikoSuiteWithSudo(BaseSSHSuite): 48 | 49 | # noinspection PyMethodOverriding 50 | @patch('paramiko.SSHClient') 51 | def setUp(self, _): 52 | super(TestParamikoSuiteWithSudo, self).__init__(sudo=True) 53 | self.proxmox = ProxmoxAPI('proxmox', user='root', backend='ssh_paramiko', port=123, sudo=True) 54 | self.client = self.proxmox._store['session'].ssh_client 55 | self.session = self.client.get_transport().open_session() 56 | self._set_stderr('200 OK') 57 | self._set_stdout('') 58 | 59 | def _get_called_cmd(self): 60 | return self.session.exec_command.call_args[0][0] 61 | 62 | def _set_stdout(self, stdout): 63 | self.session.makefile.return_value = io.BytesIO(stdout.encode('utf-8')) 64 | 65 | def _set_stderr(self, stderr): 66 | self.session.makefile_stderr.return_value = io.BytesIO(stderr.encode('utf-8')) 67 | --------------------------------------------------------------------------------