diff --git a/README.rst b/README.rst index bd639c7..ea25cf3 100644 --- a/README.rst +++ b/README.rst @@ -2,256 +2,4 @@ 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 `_. - -It was inspired by slumber, but it dedicated only to Proxmox. It allows to use not only REST API 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 - -For 'https' backend install requests - -.. code-block:: bash - - pip install requests - -For 'ssh_paramiko' backend install paramiko - -.. code-block:: bash - - pip install paramiko - - -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 through the 'https' backend. - -It is possible to use already prepared public/private key authentication. It is possible to use ssh-agent also. - -.. code-block:: python - - from proxmoxer import ProxmoxAPI - proxmox = ProxmoxAPI('proxmox_host', user='proxmox_admin', backend='ssh_paramiko') - -**Please note, https-backend needs 'requests' library, ssh_paramiko-backend needs 'paramiko' library, -openssh-backend needs 'openssh_wrapper' library installed.** - -Queries are exposed via the access methods **get**, **post**, **put** and **delete**. For convenience added two -synonyms: **create** for **post**, and **set** for **put**. - -.. 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.baseblack.com => running - 101. munki.london.baseblack.com => running - 102. redmine.london.baseblack.com => running - 140. dns-1.london.baseblack.com => running - 126. ns-3.london.baseblack.com => running - 113. rabbitmq.london.baseblack.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']) - - -for example next lines do the same job: - -.. 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: - -.. code-block:: python - - for vm in proxmox.cluster.resources.get(type='vm'): - print("{0}. {1} => {2}" .format(vm['vmid'], vm['name'], vm['status'])) - - -.. code-block:: python - - node = proxmox.nodes('proxmox_node') - pprint(node.storage('local').content.get()) - -or the with same results - -.. code-block:: python - - node = proxmox.nodes.proxmox_node() - pprint(node.storage.local.content.get()) - - -Example of creation of 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') - -Example of template upload: - -.. 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')))) - - -Example of rrd download: - -.. 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') - - -Roadmap -------- - -* write tests -* support other actual python versions -* add optional validation of requests -* add some shortcuts for convenience - -History -------- - -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/swayf/proxmoxer.png?branch=master - :target: https://travis-ci.org/swayf/proxmoxer - -.. |master_coverage_status| image:: https://coveralls.io/repos/swayf/proxmoxer/badge.png?branch=master - :target: https://coveralls.io/r/swayf/proxmoxer - -.. |develop_build_status| image:: https://travis-ci.org/swayf/proxmoxer.png?branch=develop - :target: https://travis-ci.org/swayf/proxmoxer - -.. |develop_coverage_status| image:: https://coveralls.io/repos/swayf/proxmoxer/badge.png?branch=develop - :target: https://coveralls.io/r/swayf/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 - +repository is moved to https://github.com/proxmoxer/proxmoxer diff --git a/proxmoxer/__init__.py b/proxmoxer/__init__.py index a7cd49c..736104b 100644 --- a/proxmoxer/__init__.py +++ b/proxmoxer/__init__.py @@ -1,6 +1,6 @@ __author__ = 'Oleg Butovich' __copyright__ = '(c) Oleg Butovich 2013-2017' -__version__ = '1.0.2' +__version__ = '1.0.3' __licence__ = 'MIT' from .core import * diff --git a/proxmoxer/backends/base_ssh.py b/proxmoxer/backends/base_ssh.py index ee8fc72..683c846 100644 --- a/proxmoxer/backends/base_ssh.py +++ b/proxmoxer/backends/base_ssh.py @@ -40,7 +40,7 @@ def request(self, method, url, data=None, params=None, headers=None): 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())]) + translated_data = ' '.join(["-{0} {1}".format(k, v if not isinstance(v, str) or " " not in v else '"{}"'.format(v)) for k, v in chain(data.items(), params.items())]) full_cmd = 'pvesh {0}'.format(' '.join(filter(None, (cmd, url, translated_data)))) stdout, stderr = self._exec(full_cmd) @@ -49,7 +49,10 @@ def request(self, method, url, data=None, params=None, headers=None): status_code = next( (int(s.split()[0]) for s in stderr.splitlines() if match(s)), 500) - return Response(stdout, status_code) + if stdout: + return Response(stdout, status_code) + else: + return Response(stderr, status_code) def upload_file_obj(self, file_obj, remote_path): raise NotImplementedError() diff --git a/proxmoxer/backends/https.py b/proxmoxer/backends/https.py index b870195..ad76a47 100644 --- a/proxmoxer/backends/https.py +++ b/proxmoxer/backends/https.py @@ -90,6 +90,10 @@ def request(self, method, url, params=None, data=None, headers=None, cookies=Non timeout=None, allow_redirects=True, proxies=None, hooks=None, stream=None, verify=None, cert=None, serializer=None): + # take set verify flag from session request does not have this parameter explicitly + if verify is None: + verify = self.verify + #filter out streams files = files or {} data = data or {} @@ -109,7 +113,12 @@ def request(self, method, url, params=None, data=None, headers=None, cookies=Non class Backend(object): def __init__(self, host, user, password, port=8006, verify_ssl=True, mode='json', timeout=5, auth_token=None, csrf_token=None): + if ':' in host: + host, host_port = host.split(':') + port = host_port if host_port.isdigit() else port + self.base_url = "https://{0}:{1}/api2/{2}".format(host, port, mode) + if auth_token is not None: self.auth = ProxmoxHTTPTokenAuth(auth_token, csrf_token) else: diff --git a/proxmoxer/backends/ssh_paramiko.py b/proxmoxer/backends/ssh_paramiko.py index c24c384..482da55 100644 --- a/proxmoxer/backends/ssh_paramiko.py +++ b/proxmoxer/backends/ssh_paramiko.py @@ -56,8 +56,8 @@ def _exec(self, cmd): cmd = 'sudo ' + cmd session = self.ssh_client.get_transport().open_session() session.exec_command(cmd) - stdout = ''.join(session.makefile('rb', -1)) - stderr = ''.join(session.makefile_stderr('rb', -1)) + 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): diff --git a/proxmoxer/core.py b/proxmoxer/core.py index 43c3a06..b2c9b49 100644 --- a/proxmoxer/core.py +++ b/proxmoxer/core.py @@ -42,7 +42,16 @@ def url_join(self, base, *args): class ResourceException(Exception): - pass + def __init__(self, status_code, status_message, content, reason): + self.status_code = status_code + self.status_message = status_message + self.content = content + self.reason = reason.strip() + super(ResourceException, self).__init__(self.__repr__()) + + def __repr__(self): + return "{0} {1}: {2}, Content:{3}".format( + self.status_code, self.status_message, self.reason, self.content) class ProxmoxResource(ProxmoxResourceBase): @@ -75,8 +84,7 @@ def _request(self, method, data=None, params=None): logger.debug('Status code: %s, output: %s', resp.status_code, resp.content) if resp.status_code >= 400: - raise ResourceException("{0} {1}: {2}".format(resp.status_code, httplib.responses[resp.status_code], - resp.content)) + raise ResourceException(resp.status_code, httplib.responses[resp.status_code], resp.content, resp.reason) elif 200 <= resp.status_code <= 299: return self._store["serializer"].loads(resp) diff --git a/tests/base/base_ssh_suite.py b/tests/base/base_ssh_suite.py index cbe6d07..943b72f 100644 --- a/tests/base/base_ssh_suite.py +++ b/tests/base/base_ssh_suite.py @@ -72,12 +72,15 @@ def test_get(self): 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')) + 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')) + 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')) 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', @@ -102,6 +105,7 @@ def test_post(self): 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', @@ -127,6 +131,7 @@ def test_post(self): 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()) @@ -136,6 +141,7 @@ def test_put(self): 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()) diff --git a/tests/https_tests.py b/tests/https_tests.py index 3efc1fc..1c1b454 100644 --- a/tests/https_tests.py +++ b/tests/https_tests.py @@ -20,6 +20,32 @@ def test_https_connection(req_session): eq_(call['verify'], False) +@patch('requests.sessions.Session') +def test_https_connection_wth_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_wth_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) + + class TestSuite(): proxmox = None serializer = None diff --git a/tests/paramiko_tests.py b/tests/paramiko_tests.py index 923c386..e69ee79 100644 --- a/tests/paramiko_tests.py +++ b/tests/paramiko_tests.py @@ -2,6 +2,7 @@ __copyright__ = '(c) Oleg Butovich 2013-2017' __licence__ = 'MIT' +import io from mock import patch from nose.tools import eq_ from proxmoxer import ProxmoxAPI @@ -37,10 +38,10 @@ def _get_called_cmd(self): return self.session.exec_command.call_args[0][0] def _set_stdout(self, stdout): - self.session.makefile.return_value = [stdout] + self.session.makefile.return_value = io.BytesIO(stdout.encode('utf-8')) def _set_stderr(self, stderr): - self.session.makefile_stderr.return_value = [stderr] + self.session.makefile_stderr.return_value = io.BytesIO(stderr.encode('utf-8')) class TestParamikoSuiteWithSudo(BaseSSHSuite): @@ -59,7 +60,7 @@ def _get_called_cmd(self): return self.session.exec_command.call_args[0][0] def _set_stdout(self, stdout): - self.session.makefile.return_value = [stdout] + self.session.makefile.return_value = io.BytesIO(stdout.encode('utf-8')) def _set_stderr(self, stderr): - self.session.makefile_stderr.return_value = [stderr] + self.session.makefile_stderr.return_value = io.BytesIO(stderr.encode('utf-8'))