mirror of
https://github.com/dkmstr/openuds.git
synced 2025-01-21 18:03:54 +03:00
More test and fixes for proxmox
This commit is contained in:
parent
8f902b36fa
commit
57a8d26adc
@ -74,7 +74,7 @@ authLogger = logging.getLogger('authLog')
|
||||
|
||||
RT = typing.TypeVar('RT')
|
||||
|
||||
|
||||
# Local type only
|
||||
class AuthResult(typing.NamedTuple):
|
||||
user: typing.Optional[models.User] = None
|
||||
url: typing.Optional[str] = None
|
||||
|
@ -38,7 +38,7 @@ import collections.abc
|
||||
# Imports for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from .authenticator import Authenticator as AuthenticatorInstance
|
||||
from uds.models.group import Group as DBGroup
|
||||
from uds import models
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -50,22 +50,22 @@ class Group:
|
||||
|
||||
It's only constructor expect a database group as parameter.
|
||||
"""
|
||||
_db_group: 'DBGroup'
|
||||
_db_group: 'models.Group'
|
||||
|
||||
def __init__(self, db_group: 'DBGroup'):
|
||||
def __init__(self, db_group: 'models.Group'):
|
||||
"""
|
||||
Initializes internal data
|
||||
"""
|
||||
self._manager = db_group.get_manager()
|
||||
self._cached_manager = db_group.get_manager()
|
||||
self._db_group = db_group
|
||||
|
||||
def manager(self) -> 'AuthenticatorInstance':
|
||||
"""
|
||||
Returns the database authenticator associated with this group
|
||||
"""
|
||||
return self._manager
|
||||
return self._cached_manager
|
||||
|
||||
def db_obj(self) -> 'DBGroup':
|
||||
def db_obj(self) -> 'models.Group':
|
||||
"""
|
||||
Returns the database group associated with this
|
||||
"""
|
||||
|
@ -40,8 +40,7 @@ from .groups_manager import GroupsManager
|
||||
# Imports for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from .authenticator import Authenticator as AuthenticatorInstance
|
||||
from uds.models.group import Group as DBGroup
|
||||
from uds.models.user import User as DBUser
|
||||
from uds import models
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -53,27 +52,17 @@ class User:
|
||||
and its groups.
|
||||
"""
|
||||
|
||||
_manager: 'AuthenticatorInstance'
|
||||
grps_manager: typing.Optional['GroupsManager']
|
||||
_db_user: 'DBUser'
|
||||
_cached_manager: 'AuthenticatorInstance'
|
||||
_groups_manager: 'GroupsManager'
|
||||
_db_user: 'models.User'
|
||||
_groups: typing.Optional[list[Group]]
|
||||
|
||||
def __init__(self, db_user: 'DBUser') -> None:
|
||||
self._manager = db_user.get_manager()
|
||||
self.grps_manager = None
|
||||
def __init__(self, db_user: 'models.User') -> None:
|
||||
self._cached_manager = db_user.get_manager()
|
||||
self._groups_manager = GroupsManager(db_user.manager)
|
||||
self._db_user = db_user
|
||||
self._groups = None
|
||||
|
||||
def _groups_manager(self) -> 'GroupsManager':
|
||||
"""
|
||||
If the groups manager for this user already exists, it returns this.
|
||||
If it does not exists, it creates one default from authenticator and
|
||||
returns it.
|
||||
"""
|
||||
if self.grps_manager is None:
|
||||
self.grps_manager = GroupsManager(self._manager.db_obj())
|
||||
return self.grps_manager
|
||||
|
||||
def groups(self) -> list[Group]:
|
||||
"""
|
||||
Returns the valid groups for this user.
|
||||
@ -87,9 +76,9 @@ class User:
|
||||
)
|
||||
|
||||
if self._groups is None:
|
||||
if self._manager.external_source:
|
||||
self._manager.get_groups(self._db_user.name, self._groups_manager())
|
||||
self._groups = list(self._groups_manager().enumerate_valid_groups())
|
||||
if self._cached_manager.external_source:
|
||||
self._cached_manager.get_groups(self._db_user.name, self._groups_manager)
|
||||
self._groups = list(self._groups_manager.enumerate_valid_groups())
|
||||
logger.debug(self._groups)
|
||||
# This is just for updating "cached" data of this user, we only get real groups at login and at modify user operation
|
||||
usr = DBUser.objects.get(pk=self._db_user.id) # @UndefinedVariable
|
||||
@ -104,9 +93,9 @@ class User:
|
||||
"""
|
||||
Returns the authenticator instance
|
||||
"""
|
||||
return self._manager
|
||||
return self._cached_manager
|
||||
|
||||
def db_obj(self) -> 'DBUser':
|
||||
def db_obj(self) -> 'models.User':
|
||||
"""
|
||||
Returns the database user
|
||||
"""
|
||||
|
@ -91,7 +91,16 @@ class Publication(Environmentable, Serializable):
|
||||
|
||||
_db_obj: typing.Optional['models.ServicePoolPublication']
|
||||
|
||||
def __init__(self, environment: 'Environment', **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
environment: 'Environment',
|
||||
*,
|
||||
service: 'services.Service',
|
||||
os_manager: typing.Optional['osmanagers.OSManager'] = None,
|
||||
revision: int = -1,
|
||||
servicepool_name: str = 'Unknown',
|
||||
uuid: str = '',
|
||||
) -> None:
|
||||
"""
|
||||
Do not forget to invoke this in your derived class using "super(self.__class__, self).__init__(environment, values)"
|
||||
We want to use the env, cache and storage methods outside class. If not called, you must implement your own methods
|
||||
@ -100,11 +109,11 @@ class Publication(Environmentable, Serializable):
|
||||
"""
|
||||
Environmentable.__init__(self, environment)
|
||||
Serializable.__init__(self)
|
||||
self._osManager = kwargs.get('osmanager', None)
|
||||
self._service = kwargs['service'] # Raises an exception if service is not included
|
||||
self._revision = kwargs.get('revision', -1)
|
||||
self._servicepool_name = kwargs.get('servicepool_name', 'Unknown')
|
||||
self._uuid = kwargs.get('uuid', '')
|
||||
self._service = service
|
||||
self._osmanager = os_manager
|
||||
self._revision = revision
|
||||
self._servicepool_name = servicepool_name
|
||||
self._uuid = uuid
|
||||
|
||||
self.initialize()
|
||||
|
||||
@ -147,7 +156,7 @@ class Publication(Environmentable, Serializable):
|
||||
The returned value can be None if no Os manager is needed by
|
||||
the service owner of this publication.
|
||||
"""
|
||||
return self._osManager
|
||||
return self._osmanager
|
||||
|
||||
def revision(self) -> int:
|
||||
"""
|
||||
@ -211,7 +220,7 @@ class Publication(Environmentable, Serializable):
|
||||
State values RUNNING, FINISHED or ERROR.
|
||||
|
||||
This method will be invoked whenever a publication is started, but it
|
||||
do not finish in 1 step. (that is, invoked as long as the instance has not
|
||||
do not finish in 1 step. (that is, invoked as long as the instance has not
|
||||
finished or produced an error)
|
||||
|
||||
The idea behind this is simple, we can initiate an operation of publishing,
|
||||
@ -303,4 +312,4 @@ class Publication(Environmentable, Serializable):
|
||||
"""
|
||||
String method, mainly used for debugging purposes
|
||||
"""
|
||||
return 'Base Publication'
|
||||
return f'{self.__class__.__name__}({self._service.name})'
|
||||
|
@ -51,7 +51,7 @@ class ProxmoxPublication(services.Publication, autoserializable.AutoSerializable
|
||||
suggested_delay = 20
|
||||
|
||||
_name = autoserializable.StringField(default='')
|
||||
_vm = autoserializable.StringField(default='')
|
||||
_vmid = autoserializable.StringField(default='')
|
||||
_task = autoserializable.StringField(default='')
|
||||
_state = autoserializable.StringField(default='')
|
||||
_operation = autoserializable.StringField(default='')
|
||||
@ -74,7 +74,7 @@ class ProxmoxPublication(services.Publication, autoserializable.AutoSerializable
|
||||
if vals[0] == 'v1':
|
||||
(
|
||||
self._name,
|
||||
self._vm,
|
||||
self._vmid,
|
||||
self._task,
|
||||
self._state,
|
||||
self._operation,
|
||||
@ -106,7 +106,7 @@ class ProxmoxPublication(services.Publication, autoserializable.AutoSerializable
|
||||
self.servicepool_name(), str(datetime.now()).split('.')[0]
|
||||
)
|
||||
task = self.service().clone_machine(self._name, comments)
|
||||
self._vm = str(task.vmid)
|
||||
self._vmid = str(task.vmid)
|
||||
self._task = ','.join((task.upid.node, task.upid.upid))
|
||||
self._state = State.RUNNING
|
||||
self._operation = 'p' # Publishing
|
||||
@ -142,15 +142,15 @@ class ProxmoxPublication(services.Publication, autoserializable.AutoSerializable
|
||||
self._state = State.FINISHED
|
||||
if self._operation == 'p': # not Destroying
|
||||
# Disable Protection (removal)
|
||||
self.service().provider().set_protection(int(self._vm), protection=False)
|
||||
self.service().provider().set_protection(int(self._vmid), protection=False)
|
||||
time.sleep(
|
||||
0.5
|
||||
) # Give some tome to proxmox. We have observed some concurrency issues
|
||||
# And add it to HA if
|
||||
self.service().enable_machine_ha(int(self._vm))
|
||||
self.service().enable_machine_ha(int(self._vmid))
|
||||
time.sleep(0.5)
|
||||
# Mark vm as template
|
||||
self.service().provider().create_template(int(self._vm))
|
||||
self.service().provider().create_template(int(self._vmid))
|
||||
|
||||
# This seems to cause problems on Proxmox
|
||||
# makeTemplate --> setProtection (that calls "config"). Seems that the HD dissapears...
|
||||
@ -169,7 +169,7 @@ class ProxmoxPublication(services.Publication, autoserializable.AutoSerializable
|
||||
self._destroy_after = True
|
||||
return State.RUNNING
|
||||
|
||||
self.state = State.RUNNING
|
||||
self._state = State.RUNNING
|
||||
self._operation = 'd'
|
||||
self._destroy_after = False
|
||||
try:
|
||||
@ -188,4 +188,4 @@ class ProxmoxPublication(services.Publication, autoserializable.AutoSerializable
|
||||
return self._reason
|
||||
|
||||
def machine(self) -> int:
|
||||
return int(self._vm)
|
||||
return int(self._vmid)
|
||||
|
@ -76,7 +76,7 @@ SERIALIZED_PUBLICATION_DATA: typing.Final[bytes] = b'v1\tname\tvm\ttask\tstate\t
|
||||
class OpenStackPublicationSerializationTest(UDSTestCase):
|
||||
def check(self, instance: publication.ProxmoxPublication) -> None:
|
||||
self.assertEqual(instance._name, 'name')
|
||||
self.assertEqual(instance._vm, 'vm')
|
||||
self.assertEqual(instance._vmid, 'vm')
|
||||
self.assertEqual(instance._task, 'task')
|
||||
self.assertEqual(instance._state, 'state')
|
||||
self.assertEqual(instance._operation, 'operation')
|
||||
|
@ -46,7 +46,15 @@ from uds.services.OpenNebula.on import vm
|
||||
from ...utils.test import UDSTestCase
|
||||
from ...utils.autospec import autospec, AutoSpecMethodInfo
|
||||
|
||||
from uds.services.Proxmox import provider, client, service, service_fixed
|
||||
from uds.services.Proxmox import (
|
||||
provider,
|
||||
client,
|
||||
service,
|
||||
service_fixed,
|
||||
publication,
|
||||
deployment,
|
||||
deployment_fixed,
|
||||
)
|
||||
|
||||
NODES: typing.Final[list[client.types.Node]] = [
|
||||
client.types.Node(name='node0', online=True, local=True, nodeid=1, ip='0.0.0.1', level='level', id='id'),
|
||||
@ -160,7 +168,7 @@ VMS_INFO: typing.Final[list[client.types.VMInfo]] = [
|
||||
diskwrite=1,
|
||||
vgpu_type=VGPUS[i % len(VGPUS)].type,
|
||||
)
|
||||
for i in range(10)
|
||||
for i in range(1,16)
|
||||
]
|
||||
|
||||
VMS_CONFIGURATION: typing.Final[list[client.types.VMConfiguration]] = [
|
||||
@ -219,8 +227,8 @@ TASK_STATUS = client.types.TaskStatus(
|
||||
pstart=1,
|
||||
starttime=datetime.datetime.now(),
|
||||
type='type',
|
||||
status='status',
|
||||
exitstatus='exitstatus',
|
||||
status='stopped',
|
||||
exitstatus='OK',
|
||||
user='user',
|
||||
upid='upid',
|
||||
id='id',
|
||||
@ -452,3 +460,20 @@ def create_service_fixed(
|
||||
values=values,
|
||||
uuid=uuid_,
|
||||
)
|
||||
|
||||
|
||||
def create_publication(
|
||||
service: typing.Optional[service.ProxmoxServiceLinked] = None,
|
||||
**kwargs: typing.Any,
|
||||
) -> 'publication.ProxmoxPublication':
|
||||
"""
|
||||
Create a publication
|
||||
"""
|
||||
uuid_ = str(uuid.uuid4())
|
||||
return publication.ProxmoxPublication(
|
||||
environment=environment.Environment.private_environment(uuid_),
|
||||
service=service or create_service_linked(**kwargs),
|
||||
revision=1,
|
||||
servicepool_name='servicepool_name',
|
||||
uuid=uuid_,
|
||||
)
|
||||
|
127
server/tests/services/proxmox/test_publication.py
Normal file
127
server/tests/services/proxmox/test_publication.py
Normal file
@ -0,0 +1,127 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2024 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import typing
|
||||
import datetime
|
||||
import collections.abc
|
||||
import itertools
|
||||
from unittest import mock
|
||||
|
||||
from uds.core import types, ui, environment
|
||||
from uds.services.OpenNebula.on.vm import remove_machine
|
||||
from uds.services.Proxmox.publication import ProxmoxPublication
|
||||
|
||||
from . import fixtures
|
||||
|
||||
from ...utils.test import UDSTestCase
|
||||
from ...utils import MustBeOfType
|
||||
|
||||
|
||||
class TestProxmovPublication(UDSTestCase):
|
||||
|
||||
def test_publication(self) -> None:
|
||||
with fixtures.patch_provider_api() as api:
|
||||
publication = fixtures.create_publication()
|
||||
|
||||
state = publication.publish()
|
||||
self.assertEqual(state, types.states.State.RUNNING)
|
||||
api.clone_machine.assert_called_with(
|
||||
publication.service().machine.as_int(),
|
||||
MustBeOfType(int),
|
||||
MustBeOfType(str),
|
||||
MustBeOfType(str),
|
||||
False,
|
||||
None,
|
||||
publication.service().datastore.value,
|
||||
publication.service().pool.value,
|
||||
None,
|
||||
)
|
||||
running_task = fixtures.TASK_STATUS._replace(status='running')
|
||||
|
||||
api.get_task.return_value = running_task
|
||||
state = publication.check_state()
|
||||
self.assertEqual(state, types.states.State.RUNNING)
|
||||
# Now ensure task is finished
|
||||
api.get_task.return_value = fixtures.TASK_STATUS._replace(status='stopped', exitstatus='OK')
|
||||
self.assertEqual(publication.check_state(), types.states.State.FINISHED)
|
||||
# Now, error
|
||||
publication._state = types.states.State.RUNNING
|
||||
api.get_task.return_value = fixtures.TASK_STATUS._replace(
|
||||
status='stopped', exitstatus='ERROR, BOOM!'
|
||||
)
|
||||
self.assertEqual(publication.check_state(), types.states.State.ERROR)
|
||||
self.assertEqual(publication.error_reason(), 'ERROR, BOOM!')
|
||||
|
||||
publication._vmid = str(fixtures.VMS_INFO[0].vmid)
|
||||
self.assertEqual(publication.machine(), fixtures.VMS_INFO[0].vmid)
|
||||
|
||||
def test_publication_destroy(self) -> None:
|
||||
vmid = str(fixtures.VMS_INFO[0].vmid)
|
||||
with fixtures.patch_provider_api() as api:
|
||||
publication = fixtures.create_publication()
|
||||
# Destroy
|
||||
publication._state = types.states.State.RUNNING
|
||||
publication._vmid = vmid
|
||||
state = publication.destroy()
|
||||
self.assertEqual(state, types.states.State.RUNNING)
|
||||
self.assertEqual(publication._destroy_after, True)
|
||||
|
||||
# Now, destroy again
|
||||
state = publication.destroy()
|
||||
publication._vmid = vmid
|
||||
self.assertEqual(state, types.states.State.RUNNING)
|
||||
self.assertEqual(publication._destroy_after, False)
|
||||
self.assertEqual(publication._operation, 'd')
|
||||
self.assertEqual(publication._state, types.states.State.RUNNING)
|
||||
api.remove_machine.assert_called_with(publication.service().machine.as_int())
|
||||
|
||||
# Now, repeat with finished state at the very beginning
|
||||
api.remove_machine.reset_mock()
|
||||
publication._state = types.states.State.FINISHED
|
||||
publication._vmid = vmid
|
||||
self.assertEqual(publication.destroy(), types.states.State.RUNNING)
|
||||
self.assertEqual(publication._destroy_after, False)
|
||||
self.assertEqual(publication._operation, 'd')
|
||||
self.assertEqual(publication._state, types.states.State.RUNNING)
|
||||
api.remove_machine.assert_called_with(publication.service().machine.as_int())
|
||||
|
||||
# And now, with error
|
||||
api.remove_machine.side_effect = Exception('BOOM!')
|
||||
publication._state = types.states.State.FINISHED
|
||||
publication._vmid = vmid
|
||||
self.assertEqual(publication.destroy(), types.states.State.ERROR)
|
||||
self.assertEqual(publication.error_reason(), 'BOOM!')
|
||||
|
||||
# Ensure cancel calls destroy
|
||||
with mock.patch.object(publication, 'destroy') as destroy:
|
||||
publication.cancel()
|
||||
destroy.assert_called_with()
|
@ -76,7 +76,7 @@ SERIALIZED_PUBLICATION_DATA: typing.Final[bytes] = b'v1\tname\tvm\ttask\tstate\t
|
||||
class ProxmoxPublicationSerializationTest(UDSTestCase):
|
||||
def check(self, instance: Publication) -> None:
|
||||
self.assertEqual(instance._name, 'name')
|
||||
self.assertEqual(instance._vm, 'vm')
|
||||
self.assertEqual(instance._vmid, 'vm')
|
||||
self.assertEqual(instance._task, 'task')
|
||||
self.assertEqual(instance._state, 'state')
|
||||
self.assertEqual(instance._operation, 'operation')
|
||||
|
@ -136,3 +136,25 @@ def random_hostname() -> str:
|
||||
import string
|
||||
|
||||
return ''.join(random.choice(string.ascii_lowercase) for _ in range(15)) # nosec
|
||||
|
||||
|
||||
# Just compare types
|
||||
# This is a simple class that returns true if the types of the two objects are the same
|
||||
class MustBeOfType:
|
||||
_kind: type
|
||||
|
||||
def __init__(self, kind: type) -> None:
|
||||
self._kind = kind
|
||||
|
||||
def __eq__(self, other: typing.Any) -> bool:
|
||||
return isinstance(other, self._kind)
|
||||
|
||||
def __ne__(self, other: typing.Any) -> bool:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.__class__.__name__}({self._kind.__name__})'
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user