1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-22 13:34:04 +03:00

Refactor ProxmoxProvider to support API Token authentication

This commit is contained in:
Adolfo Gómez García 2024-08-16 18:08:34 +02:00
parent 57f70b9d4f
commit 01353528dd
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
4 changed files with 135 additions and 51 deletions

View File

@ -30,7 +30,8 @@
""" """
Author: Adolfo Gómez, dkmaster at dkmon dot com Author: Adolfo Gómez, dkmaster at dkmon dot com
""" """
import random
import typing
import logging import logging
from uds.services.Proxmox.proxmox import ( from uds.services.Proxmox.proxmox import (
@ -63,7 +64,8 @@ class TestProxmoxClient(UDSTransactionTestCase):
port=int(v['port']), port=int(v['port']),
username=v['username'], username=v['username'],
password=v['password'], password=v['password'],
verify_ssl=True, use_api_token=v['use_api_token'] == 'true',
verify_ssl=False,
) )
for vm in self.pclient.list_vms(): for vm in self.pclient.list_vms():
@ -76,3 +78,45 @@ class TestProxmoxClient(UDSTransactionTestCase):
for pool in self.pclient.list_pools(): for pool in self.pclient.list_pools():
if pool.id == v['test_pool']: # id is the pool name in proxmox if pool.id == v['test_pool']: # id is the pool name in proxmox
self.pool = pool self.pool = pool
def _get_new_vmid(self) -> int:
MAX_RETRIES: typing.Final[int] = 512 # So we don't loop forever, just in case...
vmid = 0
for _ in range(MAX_RETRIES):
vmid = 100000 + random.randint(0, 899999) # Get a reasonable vmid
if self.pclient.is_vmid_available(vmid):
return vmid
# All assigned vmid will be left as unusable on UDS until released by time (3 years)
# This is not a problem at all, in the rare case that a machine id is released from uds db
# if it exists when we try to create a new one, we will simply try to get another one
self.fail(f'Could not get a new vmid!!: last tried {vmid}')
# Connect is not needed, because setUp will do the connection so if it fails, the test will throw an exception
def test_list_vms(self):
vms = self.pclient.list_vms()
# At least, the test vm should be there :)
self.assertTrue(len(vms) > 0)
self.assertTrue(self.vm.id > 0)
self.assertTrue(self.vm.status in prox_types.VMStatus)
self.assertTrue(self.vm.node)
self.assertTrue(self.vm.template in (True, False))
self.assertIsInstance(self.vm.agent, (str, type(None)))
self.assertIsInstance(self.vm.cpus, (int, type(None)))
self.assertIsInstance(self.vm.lock, (str, type(None)))
self.assertIsInstance(self.vm.disk, (int, type(None)))
self.assertIsInstance(self.vm.maxdisk, (int, type(None)))
self.assertIsInstance(self.vm.mem, (int, type(None)))
self.assertIsInstance(self.vm.maxmem, (int, type(None)))
self.assertIsInstance(self.vm.name, (str, type(None)))
self.assertIsInstance(self.vm.pid, (int, type(None)))
self.assertIsInstance(self.vm.qmpstatus, (str, type(None)))
self.assertIsInstance(self.vm.tags, (str, type(None)))
self.assertIsInstance(self.vm.uptime, (int, type(None)))
self.assertIsInstance(self.vm.netin, (int, type(None)))
self.assertIsInstance(self.vm.netout, (int, type(None)))
self.assertIsInstance(self.vm.diskread, (int, type(None)))
self.assertIsInstance(self.vm.diskwrite, (int, type(None)))
self.assertIsInstance(self.vm.vgpu_type, (str, type(None)))

View File

@ -82,10 +82,19 @@ class ProxmoxProvider(services.ServiceProvider):
default=8006, default=8006,
) )
use_api_token = gui.CheckBoxField(
label=_('Use API Token'),
order=3,
tooltip=_(
'Use API Token and secret instead of password. (username must contain the Token ID, the password will be the secret)'
),
required=False,
default=False,
)
username = gui.TextField( username = gui.TextField(
length=32, length=32,
label=_('Username'), label=_('Username'),
order=3, order=4,
tooltip=_('User with valid privileges on Proxmox, (use "user@authenticator" form)'), tooltip=_('User with valid privileges on Proxmox, (use "user@authenticator" form)'),
required=True, required=True,
default='root@pam', default='root@pam',
@ -93,7 +102,7 @@ class ProxmoxProvider(services.ServiceProvider):
password = gui.PasswordField( password = gui.PasswordField(
length=32, length=32,
label=_('Password'), label=_('Password'),
order=4, order=5,
tooltip=_('Password of the user of Proxmox'), tooltip=_('Password of the user of Proxmox'),
required=True, required=True,
) )
@ -133,6 +142,7 @@ class ProxmoxProvider(services.ServiceProvider):
self.port.as_int(), self.port.as_int(),
self.username.value, self.username.value,
self.password.value, self.password.value,
self.use_api_token.value,
self.timeout.as_int(), self.timeout.as_int(),
False, False,
self.cache, self.cache,

View File

@ -57,6 +57,7 @@ class ProxmoxClient:
_host: str _host: str
_port: int _port: int
_credentials: tuple[tuple[str, str], tuple[str, str]] _credentials: tuple[tuple[str, str], tuple[str, str]]
_use_api_token: bool
_url: str _url: str
_verify_ssl: bool _verify_ssl: bool
_timeout: int _timeout: int
@ -74,6 +75,7 @@ class ProxmoxClient:
port: int, port: int,
username: str, username: str,
password: str, password: str,
use_api_token: bool = False,
timeout: int = 5, timeout: int = 5,
verify_ssl: bool = False, verify_ssl: bool = False,
cache: typing.Optional['Cache'] = None, cache: typing.Optional['Cache'] = None,
@ -81,6 +83,7 @@ class ProxmoxClient:
self._host = host self._host = host
self._port = port self._port = port
self._credentials = (('username', username), ('password', password)) self._credentials = (('username', username), ('password', password))
self._use_api_token = use_api_token
self._verify_ssl = verify_ssl self._verify_ssl = verify_ssl
self._timeout = timeout self._timeout = timeout
self._url = 'https://{}:{}/api2/json/'.format(self._host, self._port) self._url = 'https://{}:{}/api2/json/'.format(self._host, self._port)
@ -101,6 +104,19 @@ class ProxmoxClient:
return self._session return self._session
self._session = security.secure_requests_session(verify=self._verify_ssl) self._session = security.secure_requests_session(verify=self._verify_ssl)
if self._use_api_token:
token = f'{self._credentials[0][1]}={self._credentials[1][1]}'
# Set _ticket to something, so we don't try to connect again
self._ticket = 'API_TOKEN' # Using API token, not a real ticket
self._session.headers.update(
{
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': f'PVEAPIToken={token}',
}
)
else:
self._session.headers.update( self._session.headers.update(
{ {
'Accept': 'application/json', 'Accept': 'application/json',
@ -139,10 +155,12 @@ class ProxmoxClient:
self.cache.put(self._host + 'conn', (ticket, csrf), validity=1800) # 30 minutes self.cache.put(self._host + 'conn', (ticket, csrf), validity=1800) # 30 minutes
_update_session(ticket, csrf) _update_session(ticket, csrf)
return self._session
except requests.RequestException as e: except requests.RequestException as e:
raise exceptions.ProxmoxConnectionError(str(e)) from e raise exceptions.ProxmoxConnectionError(str(e)) from e
return self._session
def ensure_correct(self, response: 'requests.Response', *, node: typing.Optional[str]) -> typing.Any: def ensure_correct(self, response: 'requests.Response', *, node: typing.Optional[str]) -> typing.Any:
if not response.ok: if not response.ok:
logger.debug('Error on request %s: %s', response.status_code, response.content) logger.debug('Error on request %s: %s', response.status_code, response.content)
@ -240,6 +258,10 @@ class ProxmoxClient:
def test(self) -> bool: def test(self) -> bool:
try: try:
self.connect() self.connect()
if self._use_api_token:
# When using api token, we need to ask for something
# Because the login has not been done, just the token has been set on headers
self.get_cluster_info()
except Exception: except Exception:
# logger.error('Error testing proxmox: %s', e) # logger.error('Error testing proxmox: %s', e)
return False return False
@ -249,6 +271,13 @@ class ProxmoxClient:
def get_cluster_info(self, **kwargs: typing.Any) -> types.ClusterInfo: def get_cluster_info(self, **kwargs: typing.Any) -> types.ClusterInfo:
return types.ClusterInfo.from_dict(self.do_get('cluster/status')) return types.ClusterInfo.from_dict(self.do_get('cluster/status'))
@cached('cluster_res', consts.CACHE_DURATION, key_helper=caching_key_helper)
def get_cluster_resources(
self, type: typing.Literal['vm', 'storage', 'node', 'sdn'], **kwargs: typing.Any
) -> list[dict[str, typing.Any]]:
# i.e.: self.do_get('cluster/resources?type=vm')
return self.do_get(f'cluster/resources?type={type}')['data']
def get_next_vmid(self) -> int: def get_next_vmid(self) -> int:
return int(self.do_get('cluster/nextid')['data']) return int(self.do_get('cluster/nextid')['data'])
@ -562,7 +591,7 @@ class ProxmoxClient:
return sorted( return sorted(
[ [
types.VMInfo.from_dict(vm_info) types.VMInfo.from_dict(vm_info)
for vm_info in self.do_get('cluster/resources?type=vm')['data'] for vm_info in self.get_cluster_resources('vm')
if vm_info['type'] == 'qemu' and vm_info['node'] in node_list if vm_info['type'] == 'qemu' and vm_info['node'] in node_list
], ],
key=lambda x: f'{x.node}{x.name}', key=lambda x: f'{x.node}{x.name}',
@ -702,17 +731,18 @@ class ProxmoxClient:
) -> list[types.StorageInfo]: ) -> list[types.StorageInfo]:
"""We use a list for storage instead of an iterator, so we can cache it...""" """We use a list for storage instead of an iterator, so we can cache it..."""
node_list: set[str] node_list: set[str]
if node is None: match node:
case None:
node_list = {n.name for n in self.get_cluster_info().nodes if n.online} node_list = {n.name for n in self.get_cluster_info().nodes if n.online}
elif isinstance(node, str): case str():
node_list = set([node]) node_list = {node}
else: case collections.abc.Iterable():
node_list = set(node) node_list = set(node)
return sorted( return sorted(
[ [
types.StorageInfo.from_dict(st_info) types.StorageInfo.from_dict(st_info)
for st_info in self.do_get('cluster/resources?type=storage')['data'] for st_info in self.get_cluster_resources('storage')
if st_info['node'] in node_list and (content is None or content in st_info['content']) if st_info['node'] in node_list and (content is None or content in st_info['content'])
], ],
key=lambda x: f'{x.node}{x.storage}', key=lambda x: f'{x.node}{x.storage}',

View File

@ -198,7 +198,7 @@ class TaskStatus:
starttime=datetime.datetime.fromtimestamp(data['starttime']), starttime=datetime.datetime.fromtimestamp(data['starttime']),
type=data['type'], type=data['type'],
status=data['status'], status=data['status'],
exitstatus=data['exitstatus'], exitstatus=data.get('exitstatus', ''),
user=data['user'], user=data['user'],
upid=data['upid'], upid=data['upid'],
id=data['id'], id=data['id'],