Skip to content

Commit 1e93bb7

Browse files
committed
Merge branch 'master' of github.com:/VirtualCable/openuds
2 parents 57cfb0d + c885101 commit 1e93bb7

16 files changed

Lines changed: 1884 additions & 154 deletions

server/src/uds/core/managers/notifications.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def notify(
9696
message = message % args
9797
except Exception:
9898
message = message + ' ' + str(args) + ' (format error)'
99-
message = message[:4096] # Max length of message
99+
message = message[:4000] # Max length of message, fixed to ensure it also supports sqlserver
100100
# Store the notification on local persistent storage
101101
# Will be processed by UDS backend
102102
try:

server/src/uds/services/OpenShift/openshift/client.py

Lines changed: 105 additions & 147 deletions
Large diffs are not rendered by default.

server/src/uds/services/OpenShift/provider.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,17 @@ def test(
126126
def sanitized_name(self, name: str) -> str:
127127
"""
128128
Sanitizes the VM name to comply with RFC 1123:
129-
- Lowercase
130-
- Alphanumeric, '-', '.'
131-
- Starts/ends with alphanumeric
132-
- Max length 63 chars
129+
- Converts to lowercase
130+
- Replaces any character not in [a-z0-9.-] with '-'
131+
- Collapses multiple '-' into one
132+
- Removes leading/trailing non-alphanumeric characters
133+
- Limits length to 63 characters
133134
"""
134-
name = re.sub(r'^[^a-z0-9]+|[^a-z0-9.-]|-{2,}|[^a-z0-9]+$', '-', name.lower())
135-
return name[:63]
135+
name = name.lower()
136+
# Replace any character not allowed with '-'
137+
name = re.sub(r'[^a-z0-9.-]', '-', name)
138+
# Collapse multiple '-' into one
139+
name = re.sub(r'-{2,}', '-', name)
140+
# Remove leading/trailing non-alphanumeric characters
141+
name = re.sub(r'^[^a-z0-9]+|[^a-z0-9]+$', '', name)
142+
return name[:63]

server/tests/services/openshift/__init__.py

Whitespace-only changes.
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Test fixtures for OpenShift service tests.
4+
Provides reusable functions and mock objects for unit testing OpenShift provider, service, deployment, publication, and user service logic.
5+
All functions are designed to be used across multiple test modules for consistency and maintainability.
6+
"""
7+
8+
#
9+
# Copyright (c) 2024 Virtual Cable S.L.U.
10+
# All rights reserved.
11+
#
12+
# Redistribution and use in source and binary forms, with or without modification,
13+
# are permitted provided that the following conditions are met:
14+
#
15+
# * Redistributions of source code must retain the above copyright notice,
16+
# this list of conditions and the following disclaimer.
17+
# * Redistributions in binary form must reproduce the above copyright notice,
18+
# this list of conditions and the following disclaimer in the documentation
19+
# and/or other materials provided with the distribution.
20+
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
21+
# may be used to endorse or promote products derived from this software
22+
# without specific prior written permission.
23+
#
24+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34+
"""
35+
Author: Adolfo Gómez, dkmaster at dkmon dot com
36+
"""
37+
import contextlib
38+
import copy
39+
import functools
40+
import random
41+
import typing
42+
43+
from unittest import mock
44+
import uuid
45+
46+
47+
from uds.core import environment
48+
from uds.core.ui.user_interface import gui
49+
from uds.models.user import User
50+
51+
from uds.services.OpenShift import service, service_fixed, provider, publication, deployment, deployment_fixed
52+
from uds.services.OpenShift.openshift import types as openshift_types, exceptions as openshift_exceptions
53+
54+
DEF_VMS: list[openshift_types.VM] = [
55+
openshift_types.VM(
56+
name=f'vm-{i}',
57+
namespace='default',
58+
uid=f'uid-{i}',
59+
status=openshift_types.VMStatus.STOPPED if i % 2 == 0 else openshift_types.VMStatus.RUNNING,
60+
volume_template=openshift_types.VolumeTemplate(name=f'volume-{i}', storage='10Gi'),
61+
disks=[openshift_types.DeviceDisk(name=f'disk-{i}', boot_order=1)],
62+
volumes=[openshift_types.Volume(name=f'volume-{i}', data_volume=f'dv-{i}')],
63+
)
64+
for i in range(1, 11)
65+
]
66+
DEF_VM_INSTANCES: list[openshift_types.VMInstance] = [
67+
openshift_types.VMInstance(
68+
name=f'vm-{i}',
69+
namespace='default',
70+
uid=f'uid-instance-{i}',
71+
interfaces=[
72+
openshift_types.Interface(
73+
name='eth0',
74+
mac_address=f'00:11:22:33:44:{i:02x}',
75+
ip_address=f'192.168.1.{i}',
76+
)
77+
],
78+
status=openshift_types.VMStatus.STOPPED if i % 2 == 0 else openshift_types.VMStatus.RUNNING,
79+
phase=openshift_types.VMStatus.STOPPED if i % 2 == 0 else openshift_types.VMStatus.RUNNING,
80+
)
81+
for i in range(1, 11)
82+
]
83+
84+
# clone values to avoid modifying the original ones
85+
VMS: list[openshift_types.VM] = copy.deepcopy(DEF_VMS)
86+
VM_INSTANCES: list[openshift_types.VMInstance] = copy.deepcopy(DEF_VM_INSTANCES)
87+
88+
89+
def clear() -> None:
90+
"""
91+
Reset all VM and VMInstance values to their default state.
92+
Use this before each test to ensure a clean environment.
93+
"""
94+
VMS[:] = copy.deepcopy(DEF_VMS)
95+
VM_INSTANCES[:] = copy.deepcopy(DEF_VM_INSTANCES)
96+
97+
98+
def replace_vm_info(vm_name: str, **kwargs: typing.Any) -> None:
99+
"""
100+
Update attributes of a VM in VMS by name.
101+
Raises OpenshiftNotFoundError if VM is not found.
102+
"""
103+
try:
104+
vm = next(vm for vm in VMS if vm.name == vm_name)
105+
for k, v in kwargs.items():
106+
setattr(vm, k, v)
107+
except Exception:
108+
raise openshift_exceptions.OpenshiftNotFoundError(f'VM {vm_name} not found')
109+
110+
111+
def replacer_vm_info(**kwargs: typing.Any) -> typing.Callable[..., None]:
112+
"""
113+
Returns a partial function to update VM info with preset kwargs.
114+
Useful for patching or repeated updates in tests.
115+
"""
116+
return functools.partial(replace_vm_info, **kwargs)
117+
118+
119+
T = typing.TypeVar('T')
120+
121+
122+
def returner(value: T, *args: typing.Any, **kwargs: typing.Any) -> typing.Callable[..., T]:
123+
"""
124+
Returns a function that always returns the given value.
125+
Useful for mocking return values in tests.
126+
"""
127+
def inner(*args: typing.Any, **kwargs: typing.Any) -> T:
128+
return value
129+
130+
return inner
131+
132+
133+
# Provider values
134+
PROVIDER_VALUES_DICT: gui.ValuesDictType = {
135+
'cluster_url': 'https://oauth-openshift.apps-crc.testing',
136+
'api_url': 'https://api.crc.testing:6443',
137+
'username': 'kubeadmin',
138+
'password': 'test-password',
139+
'namespace': 'default',
140+
'verify_ssl': False,
141+
'concurrent_creation_limit': 1,
142+
'concurrent_removal_limit': 1,
143+
'timeout': 10,
144+
}
145+
146+
# Service values
147+
SERVICE_VALUES_DICT: gui.ValuesDictType = {
148+
'template': VMS[0].name,
149+
'basename': 'base',
150+
'lenname': 4,
151+
'publication_timeout': 120,
152+
'prov_uuid': '',
153+
}
154+
155+
# Service fixed values
156+
SERVICE_FIXED_VALUES_DICT: gui.ValuesDictType = {
157+
'token': '',
158+
'machines': [VMS[2].name, VMS[3].name, VMS[4].name],
159+
'on_logout': 'no',
160+
'randomize': False,
161+
'maintain_on_error': False,
162+
'prov_uuid': '',
163+
}
164+
165+
166+
def create_client_mock() -> mock.Mock:
167+
"""
168+
Create a MagicMock for OpenshiftClient with default behaviors and side effects.
169+
Used to simulate API responses in provider/service tests.
170+
"""
171+
client = mock.MagicMock()
172+
173+
# Prepare deep copies of default data
174+
client.test.return_value = True
175+
client.list_vms.return_value = copy.deepcopy(DEF_VMS)
176+
client.start_vm_instance.return_value = True
177+
client.stop_vm_instance.return_value = True
178+
client.delete_vm_instance.return_value = True
179+
client.get_datavolume_phase.return_value = "Succeeded"
180+
client.get_vm_pvc_or_dv_name.return_value = ("test-pvc", "pvc")
181+
client.get_pvc_size.return_value = "10Gi"
182+
client.create_vm_from_pvc.return_value = True
183+
client.wait_for_datavolume_clone_progress.return_value = True
184+
185+
def get_vm_info_side_effect(vm_name: str, **kwargs: typing.Any) -> openshift_types.VM | None:
186+
for vm in VMS:
187+
if vm.name == vm_name:
188+
return vm
189+
return None
190+
191+
def get_vm_instance_info_side_effect(vm_name: str, **kwargs: typing.Any) -> openshift_types.VMInstance | None:
192+
for inst in VM_INSTANCES:
193+
if inst.name == vm_name:
194+
return inst
195+
return None
196+
197+
client.get_vm_info.side_effect = get_vm_info_side_effect
198+
client.get_vm_instance_info.side_effect = get_vm_instance_info_side_effect
199+
200+
return client
201+
202+
203+
@contextlib.contextmanager
204+
def patched_provider(**kwargs: typing.Any) -> typing.Generator[provider.OpenshiftProvider, None, None]:
205+
"""
206+
Context manager that yields a provider with a patched OpenshiftClient mock.
207+
Use this to ensure all API calls are intercepted and controlled in tests.
208+
"""
209+
client = create_client_mock()
210+
prov = create_provider(**kwargs)
211+
prov._cached_api = client
212+
yield prov
213+
214+
215+
def create_provider(**kwargs: typing.Any) -> provider.OpenshiftProvider:
216+
"""
217+
Create an OpenshiftProvider instance with default or overridden values.
218+
Used for provider-level tests and as a dependency for other fixtures.
219+
"""
220+
values = PROVIDER_VALUES_DICT.copy()
221+
values.update(kwargs)
222+
223+
uuid_ = str(uuid.uuid4())
224+
return provider.OpenshiftProvider(
225+
environment=environment.Environment.private_environment(uuid_), values=values, uuid=uuid_
226+
)
227+
228+
229+
def create_service(
230+
provider: typing.Optional[provider.OpenshiftProvider] = None, **kwargs: typing.Any
231+
) -> service.OpenshiftService:
232+
"""
233+
Create an OpenshiftService instance (dynamic service).
234+
Used for service-level tests and as a dependency for user services and publications.
235+
"""
236+
uuid_ = str(uuid.uuid4())
237+
values = SERVICE_VALUES_DICT.copy()
238+
values.update(kwargs)
239+
srvc = service.OpenshiftService(
240+
environment=environment.Environment.private_environment(uuid_),
241+
provider=provider or create_provider(),
242+
values=values,
243+
uuid=uuid_,
244+
)
245+
return srvc
246+
247+
248+
def create_service_fixed(
249+
provider: typing.Optional[provider.OpenshiftProvider] = None, **kwargs: typing.Any
250+
) -> service_fixed.OpenshiftServiceFixed:
251+
"""
252+
Create an OpenshiftServiceFixed instance (fixed service).
253+
Used for fixed service tests and as a dependency for fixed user services.
254+
"""
255+
uuid_ = str(uuid.uuid4())
256+
values = SERVICE_FIXED_VALUES_DICT.copy()
257+
values.update(kwargs)
258+
return service_fixed.OpenshiftServiceFixed(
259+
environment=environment.Environment.private_environment(uuid_),
260+
provider=provider or create_provider(),
261+
values=values,
262+
uuid=uuid_,
263+
)
264+
265+
266+
def create_publication(
267+
service: typing.Optional[service.OpenshiftService] = None,
268+
**kwargs: typing.Any,
269+
) -> publication.OpenshiftTemplatePublication:
270+
"""
271+
Create an OpenshiftTemplatePublication instance.
272+
Used for publication-level tests and as a dependency for user services.
273+
"""
274+
uuid_ = str(uuid.uuid4())
275+
pub = publication.OpenshiftTemplatePublication(
276+
environment=environment.Environment.private_environment(uuid_),
277+
service=service or create_service(**kwargs),
278+
revision=1,
279+
servicepool_name='servicepool_name',
280+
uuid=uuid_,
281+
)
282+
pub._name = f"pub-{random.randint(1000, 9999)}"
283+
return pub
284+
285+
286+
def create_userservice(
287+
service: typing.Optional[service.OpenshiftService] = None,
288+
publication: typing.Optional[publication.OpenshiftTemplatePublication] = None,
289+
) -> deployment.OpenshiftUserService:
290+
"""
291+
Create an OpenshiftUserService instance (dynamic user service).
292+
Used for user service tests that require a publication and service.
293+
"""
294+
uuid_ = str(uuid.uuid4())
295+
return deployment.OpenshiftUserService(
296+
environment=environment.Environment.private_environment(uuid_),
297+
service=service or create_service(),
298+
publication=publication or create_publication(),
299+
uuid=uuid_,
300+
)
301+
302+
303+
def create_userservice_fixed(
304+
service: typing.Optional[service_fixed.OpenshiftServiceFixed] = None,
305+
) -> deployment_fixed.OpenshiftUserServiceFixed:
306+
"""
307+
Create an OpenshiftUserServiceFixed instance (fixed user service).
308+
Used for tests of fixed user service logic and lifecycle.
309+
"""
310+
uuid_ = str(uuid.uuid4().hex)
311+
return deployment_fixed.OpenshiftUserServiceFixed(
312+
environment=environment.Environment.private_environment(uuid_),
313+
service=service or create_service_fixed(),
314+
publication=None,
315+
uuid=uuid_,
316+
)
317+
318+
319+
def create_user(
320+
name: str = "testuser",
321+
real_name: str = "Test User",
322+
is_admin: bool = False,
323+
state: str = 'A',
324+
password: str = 'password',
325+
mfa_data: str = '',
326+
staff_member: bool = False,
327+
last_access: typing.Optional[str] = None,
328+
parent: typing.Optional[User] = None,
329+
created: typing.Optional[str] = None,
330+
comments: str = '',
331+
) -> User:
332+
"""
333+
Create a mock User instance for testing.
334+
All fields can be customized for specific test scenarios.
335+
"""
336+
user = mock.Mock(spec=User)
337+
user.name = name
338+
user.real_name = real_name
339+
user.is_admin = is_admin
340+
user.state = state
341+
user.password = password
342+
user.mfa_data = mfa_data
343+
user.staff_member = staff_member
344+
user.last_access = last_access
345+
user.parent = parent
346+
user.created = created
347+
user.comments = comments
348+
return user

0 commit comments

Comments
 (0)