1
0
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:
Adolfo Gómez García 2024-02-20 05:19:40 +01:00
parent 8f902b36fa
commit 57a8d26adc
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
10 changed files with 225 additions and 53 deletions

View File

@ -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

View File

@ -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
"""

View File

@ -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
"""

View File

@ -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})'

View File

@ -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)

View File

@ -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')

View File

@ -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_,
)

View 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()

View File

@ -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')

View File

@ -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__()