1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-02-03 13:47:14 +03:00

Enhacing OpenStack provider and services

This commit is contained in:
Adolfo Gómez García 2024-03-09 20:43:00 +01:00
parent 9d00766200
commit 947b35bcf5
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
12 changed files with 382 additions and 525 deletions

View File

@ -147,13 +147,13 @@ def dynamically_load_and_register_packages(
check_function: collections.abc.Callable[[type[V]], bool] = checker or (lambda x: True)
def process(classes: collections.abc.Iterable[type[V]]) -> None:
def _process(classes: collections.abc.Iterable[type[V]]) -> None:
cls: type[V]
for cls in classes:
clsSubCls = cls.__subclasses__()
if clsSubCls:
process(clsSubCls) # recursive add sub classes
_process(clsSubCls) # recursive add sub classes
if not check_function(cls):
logger.debug('Node is a not accepted, skipping: %s.%s', cls.__module__, cls.__name__)
@ -168,7 +168,7 @@ def dynamically_load_and_register_packages(
logger.error(' - Error registering %s.%s: %s', cls.__module__, cls.__name__, e)
logger.info('* Start registering %s', module_name)
process(type_.__subclasses__())
_process(type_.__subclasses__())
logger.info('* Done Registering %s', module_name)
@ -187,7 +187,7 @@ def dynamically_load_and_register_modules(
module_name (str): Name of the package to load
'''
def checker(cls: type[T]) -> bool:
def _checker(cls: type[T]) -> bool:
# Will receive all classes that are subclasses of type_
return not cls.is_base
@ -195,5 +195,5 @@ def dynamically_load_and_register_modules(
factory.insert,
type_,
module_name,
checker=checker,
checker=_checker,
)

View File

@ -33,8 +33,6 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
# pyright: reportUnusedImport=false
import os.path
import typing
import sys
from django.utils.translation import gettext_noop as _
from uds.core import managers

View File

@ -39,7 +39,7 @@ import typing
from uds.core import consts, services, types
from uds.core.util import autoserializable, log
from . import openstack
from .openstack import types as openstack_types
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
@ -155,12 +155,14 @@ class OpenStackLiveDeployment(
try:
status = self.service().get_machine_state(self._vmid)
if openstack.status_is_lost(status):
if status.is_lost():
return self._error('Machine is not available anymore')
if status == openstack.PAUSED:
power_state = self.service().get_machine_power_state(self._vmid)
if power_state.is_paused():
self.service().resume_machine(self._vmid)
elif status in (openstack.STOPPED, openstack.SHUTOFF):
elif power_state.is_stopped():
self.service().start_machine(self._vmid)
# Right now, we suppose the machine is ready
@ -207,26 +209,22 @@ class OpenStackLiveDeployment(
else:
self._queue = [Operation.CREATE, Operation.WAIT, Operation.SUSPEND, Operation.FINISH]
def _check_machine_state(self, chkState: str) -> types.states.TaskState:
def _check_machine_power_state(self, check_state: openstack_types.PowerState) -> types.states.TaskState:
logger.debug(
'Checking that state of machine %s (%s) is %s',
self._vmid,
self._name,
chkState,
check_state,
)
status = self.service().get_machine_state(self._vmid)
# If we want to check an state and machine does not exists (except in case that we whant to check this)
if openstack.status_is_lost(status):
return self._error('Machine not available. ({})'.format(status))
power_state = self.service().get_machine_power_state(self._vmid)
ret = types.states.TaskState.RUNNING
chkStates = [chkState] if not isinstance(chkState, (list, tuple)) else chkState
if status in chkStates:
if power_state == check_state:
ret = types.states.TaskState.FINISHED
return ret
def _get_current_op(self) -> Operation:
if not self._queue:
return Operation.FINISH
@ -252,7 +250,7 @@ class OpenStackLiveDeployment(
self.do_log(log.LogLevel.ERROR, self._reason)
if self._vmid: # Powers off & delete it
if self._vmid and self.service().keep_on_error() is False: # Powers off & delete it
try:
self.service().remove_machine(self._vmid)
except Exception:
@ -333,7 +331,7 @@ class OpenStackLiveDeployment(
"""
status = self.service().get_machine_state(self._vmid)
if openstack.status_is_lost(status):
if status.is_lost():
raise Exception('Machine not found. (Status {})'.format(status))
self.service().remove_machine(self._vmid)
@ -361,7 +359,7 @@ class OpenStackLiveDeployment(
"""
Checks the state of a deploy for an user or cache
"""
ret = self._check_machine_state(openstack.ACTIVE)
ret = self._check_machine_power_state(openstack_types.PowerState.RUNNING)
if ret == types.states.TaskState.FINISHED:
# Get IP & MAC (early stage)
self._mac, self._ip = self.service().get_network_info(self._vmid)
@ -372,13 +370,13 @@ class OpenStackLiveDeployment(
"""
Checks if machine has started
"""
return self._check_machine_state(openstack.ACTIVE)
return self._check_machine_power_state(openstack_types.PowerState.RUNNING)
def _check_suspend(self) -> types.states.TaskState:
"""
Check if the machine has suspended
"""
return self._check_machine_state(openstack.SUSPENDED)
return self._check_machine_power_state(openstack_types.PowerState.SUSPENDED)
def _check_removed(self) -> types.states.TaskState:
"""

View File

@ -68,20 +68,16 @@ def get_resources(parameters: dict[str, str]) -> types.ui.CallbackResultType:
zones = [gui.choice_item(z.id, z.name) for z in api.list_availability_zones()]
networks = [
gui.choice_item(z['id'], z['name']) for z in api.list_networks(name_from_subnets=name_from_subnets)
]
flavors = [gui.choice_item(z['id'], z['name']) for z in api.list_flavors()]
securityGroups = [gui.choice_item(z['id'], z['name']) for z in api.list_security_groups()]
volumeTypes = [gui.choice_item('-', _('None'))] + [
gui.choice_item(t.id, t.name) for t in api.list_volume_types()
gui.choice_item(z.id, z.name) for z in api.list_networks(name_from_subnets=name_from_subnets)
]
flavors = [gui.choice_item(z.id, f'{z.name} ({z.vcpus} vCPUs, {z.ram} MiB)') for z in api.list_flavors() if not z.disabled]
security_groups = [gui.choice_item(z.id, z.name) for z in api.list_security_groups()]
data: types.ui.CallbackResultType = [
{'name': 'availabilityZone', 'choices': zones},
{'name': 'availability_zone', 'choices': zones},
{'name': 'network', 'choices': networks},
{'name': 'flavor', 'choices': flavors},
{'name': 'securityGroups', 'choices': securityGroups},
{'name': 'volumeType', 'choices': volumeTypes},
{'name': 'security_groups', 'choices': security_groups},
]
logger.debug('Return data: %s', data)
return data

View File

@ -30,12 +30,10 @@
"""
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
# pyright: reportUnusedImport=false
import re
from .openstack_client import Client
# Import submodules
from .common import *
# Logger imported from .common, if you ask
logger = logging.getLogger(__name__)
def sanitized_name(name: str) -> str:
"""
machine names with [a-zA-Z0-9_-]
"""
return re.sub("[^a-zA-Z0-9._-]", "_", name)

View File

@ -1,92 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# 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 re
import logging
logger = logging.getLogger(__name__)
(
ACTIVE,
BUILDING,
DELETED,
ERROR,
HARD_REBOOT,
MIGRATING,
PASSWORD,
PAUSED,
REBOOT,
REBUILD,
RESCUED,
RESIZED,
REVERT_RESIZE,
SOFT_DELETED,
STOPPED,
SUSPENDED,
UNKNOWN,
VERIFY_RESIZE,
SHUTOFF,
) = (
'ACTIVE',
'BUILDING',
'DELETED',
'ERROR',
'HARD_REBOOT',
'MIGRATING',
'PASSWORD',
'PAUSED',
'REBOOT',
'REBUILD',
'RESCUED',
'RESIZED',
'REVERT_RESIZE',
'SOFT_DELETED',
'STOPPED',
'SUSPENDED',
'UNKNOWN',
'VERIFY_RESIZE',
'SHUTOFF',
)
# Helpers to check statuses
def status_is_lost(status: str) -> bool:
return status in [DELETED, ERROR, UNKNOWN, SOFT_DELETED]
def sanitized_name(name: str) -> str:
"""
machine names with [a-zA-Z0-9_-]
"""
return re.sub("[^a-zA-Z0-9._-]", "_", name)

View File

@ -62,6 +62,7 @@ VOLUMES_ENDPOINT_TYPES = [
'volumev2',
] # 'volume' is also valid, but it is deprecated A LONG TYPE AGO
COMPUTE_ENDPOINT_TYPES = ['compute', 'compute_legacy']
NETWORKS_ENDPOINT_TYPES = ['network']
# Helpers
@ -122,7 +123,7 @@ def auth_required(for_project: bool = False) -> collections.abc.Callable[[decora
def decorator(func: decorators.FT) -> decorators.FT:
@functools.wraps(func)
def wrapper(obj: 'Client', *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
def wrapper(obj: 'OpenstackClient', *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
if for_project is True:
if obj._projectid is None:
raise Exception('Need a project for method {}'.format(func))
@ -134,7 +135,7 @@ def auth_required(for_project: bool = False) -> collections.abc.Callable[[decora
return decorator
def cache_key_helper(obj: 'Client', *args: typing.Any, **kwargs: typing.Any) -> str:
def cache_key_helper(obj: 'OpenstackClient', *args: typing.Any, **kwargs: typing.Any) -> str:
return '_'.join(
[
obj._authurl,
@ -150,7 +151,7 @@ def cache_key_helper(obj: 'Client', *args: typing.Any, **kwargs: typing.Any) ->
)
class Client: # pylint: disable=too-many-public-methods
class OpenstackClient: # pylint: disable=too-many-public-methods
PUBLIC = 'public'
PRIVATE = 'private'
INTERNAL = 'url'
@ -200,7 +201,7 @@ class Client: # pylint: disable=too-many-public-methods
self._catalog = None
self._is_legacy = is_legacy
self._access = Client.PUBLIC if access is None else access
self._access = OpenstackClient.PUBLIC if access is None else access
self._domain, self._username, self._password = domain, username, password
self._userid = None
self._projectid = projectid
@ -480,16 +481,6 @@ class Client: # pylint: disable=too-many-public-methods
)
]
# TODO: Remove this
# return get_recurring_url_json(
# self._get_endpoint_for('volumev3', 'volumev2') + '/types',
# self._session,
# headers=self._get_request_headers(),
# key='volume_types',
# error_message='List Volume Types',
# timeout=self._timeout,
# )
@decorators.cached(prefix='vols', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_volumes(self) -> list[openstack_types.VolumeInfo]:
return [
@ -502,16 +493,6 @@ class Client: # pylint: disable=too-many-public-methods
)
]
# TODO: Remove this
# return get_recurring_url_json(
# self._get_endpoint_for('volumev3', 'volumev2') + '/volumes/detail',
# self._session,
# headers=self._get_request_headers(),
# key='volumes',
# error_message='List Volumes',
# timeout=self._timeout,
# )
def list_volume_snapshots(
self, volume_id: typing.Optional[dict[str, typing.Any]] = None
) -> list[openstack_types.VolumeSnapshotInfo]:
@ -526,18 +507,6 @@ class Client: # pylint: disable=too-many-public-methods
if volume_id is None or snapshot['volume_id'] == volume_id
]
# TODO: Remove this
# for s in get_recurring_url_json(
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots',
# self._session,
# headers=self._get_request_headers(),
# key='snapshots',
# error_message='List snapshots',
# timeout=self._timeout,
# ):
# if volume_id is None or s['volume_id'] == volume_id:
# yield s
@decorators.cached(prefix='azs', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_availability_zones(self) -> list[openstack_types.AvailabilityZoneInfo]:
return [
@ -551,155 +520,97 @@ class Client: # pylint: disable=too-many-public-methods
if availability_zone['zoneState']['available'] is True
]
# TODO: Remove this
# for az in get_recurring_url_json(
# self._get_compute_endpoint() + '/os-availability-zone',
# self._session,
# headers=self._get_request_headers(),
# key='availabilityZoneInfo',
# error_message='List Availability Zones',
# timeout=self._timeout,
# ):
# if az['zoneState']['available'] is True:
# yield az['zoneName']
@decorators.cached(prefix='flvs', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_flavors(self) -> list[typing.Any]:
return list(
self._get_recurring_from_endpoint(
def list_flavors(self) -> list[openstack_types.FlavorInfo]:
return [
openstack_types.FlavorInfo.from_dict(f)
for f in self._get_recurring_from_endpoint(
endpoint_types=COMPUTE_ENDPOINT_TYPES,
path='/flavors',
path='/flavors/detail',
error_message='List Flavors',
key='flavors',
)
)
# TODO: Remove this
# return get_recurring_url_json(
# self._get_compute_endpoint() + '/flavors',
# self._session,
# headers=self._get_request_headers(),
# key='flavors',
# error_message='List Flavors',
# timeout=self._timeout,
# )
]
@decorators.cached(prefix='nets', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_networks(self, name_from_subnets: bool = False) -> list[typing.Any]:
def list_networks(self, name_from_subnets: bool = False) -> list[openstack_types.NetworkInfo]:
nets = self._get_recurring_from_endpoint(
endpoint_types=['network'],
endpoint_types=NETWORKS_ENDPOINT_TYPES,
path='/v2.0/networks',
error_message='List Networks',
key='networks',
)
# TODO: Remove this
# nets = get_recurring_url_json(
# self._get_endpoint_for('network') + '/v2.0/networks',
# self._session,
# headers=self._get_request_headers(),
# key='networks',
# error_message='List Networks',
# timeout=self._timeout,
# )
if not name_from_subnets:
return list(nets)
return [openstack_types.NetworkInfo.from_dict(n) for n in nets]
else:
# Get and cache subnets names
subnetNames = {s['id']: s['name'] for s in self.list_subnets()}
res: list[typing.Any] = []
subnets_dct = {s.id: f'{s.name} ({s.cidr})' for s in self.list_subnets()}
res: list[openstack_types.NetworkInfo] = []
for net in nets:
name = ','.join(subnetNames[i] for i in net['subnets'] if i in subnetNames)
net['old_name'] = net['name']
name = ','.join(subnets_dct[i] for i in net['subnets'] if i in subnets_dct)
if name:
net['name'] = name
res.append(net)
res.append(openstack_types.NetworkInfo.from_dict(net))
return res
@decorators.cached(prefix='subns', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_subnets(self) -> collections.abc.Iterable[typing.Any]:
return list(
self._get_recurring_from_endpoint(
endpoint_types=['network'],
def list_subnets(self) -> collections.abc.Iterable[openstack_types.SubnetInfo]:
return [
openstack_types.SubnetInfo.from_dict(s)
for s in self._get_recurring_from_endpoint(
endpoint_types=NETWORKS_ENDPOINT_TYPES,
path='/v2.0/subnets',
error_message='List Subnets',
key='subnets',
)
)
# TODO: Remove this
# return get_recurring_url_json(
# self._get_endpoint_for('network') + '/v2.0/subnets',
# self._session,
# headers=self._get_request_headers(),
# key='subnets',
# error_message='List Subnets',
# timeout=self._timeout,
# )
]
@decorators.cached(prefix='sgps', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_ports(
self,
networkId: typing.Optional[str] = None,
ownerId: typing.Optional[str] = None,
) -> list[typing.Any]:
network_id: typing.Optional[str] = None,
owner_id: typing.Optional[str] = None,
) -> list[openstack_types.PortInfo]:
params: dict[str, typing.Any] = {}
if networkId is not None:
params['network_id'] = networkId
if ownerId is not None:
params['device_owner'] = ownerId
if network_id is not None:
params['network_id'] = network_id
if owner_id is not None:
params['device_owner'] = owner_id
return list(
self._get_recurring_from_endpoint(
endpoint_types=['network'],
return [
openstack_types.PortInfo.from_dict(p)
for p in self._get_recurring_from_endpoint(
endpoint_types=NETWORKS_ENDPOINT_TYPES,
path='/v2.0/ports',
error_message='List ports',
key='ports',
params=params,
)
)
]
# TODO: Remove this
# return get_recurring_url_json(
# self._get_endpoint_for('network') + '/v2.0/ports',
# self._session,
# headers=self._get_request_headers(),
# key='ports',
# params=params,
# error_message='List ports',
# timeout=self._timeout,
# )
@decorators.cached(prefix='sgps', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_security_groups(self) -> list[typing.Any]:
return list(
self._get_recurring_from_endpoint(
endpoint_types=['network'],
@decorators.cached(prefix='sgps', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_security_groups(self) -> list[openstack_types.SecurityGroupInfo]:
return [
openstack_types.SecurityGroupInfo.from_dict(sg)
for sg in self._get_recurring_from_endpoint(
endpoint_types=NETWORKS_ENDPOINT_TYPES,
path='/v2.0/security-groups',
error_message='List security groups',
key='security_groups',
)
)
]
# TODO: Remove this
# return get_recurring_url_json(
# self._get_compute_endpoint() + '/os-security-groups',
# self._session,
# headers=self._get_request_headers(),
# key='security_groups',
# error_message='List security groups',
# timeout=self._timeout,
# )
def get_server(self, server_id: str) -> dict[str, typing.Any]:
def get_server(self, server_id: str) -> openstack_types.VMInfo:
r = self._request_from_endpoint(
'get',
endpoints_types=COMPUTE_ENDPOINT_TYPES,
path=f'/servers/{server_id}',
error_message='Get Server information',
)
return r.json()['server']
return openstack_types.VMInfo.from_dict(r.json()['server'])
def get_volume(self, volumeId: str) -> dict[str, typing.Any]:
r = self._request_from_endpoint(
@ -709,15 +620,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Get Volume information',
)
# TODO: Remove this
# r = self._session.get(
# self._get_endpoint_for('volumev3', 'volumev2') + '/volumes/{volume_id}'.format(volume_id=volumeId),
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Get Volume information')
return r.json()['volume']
def get_snapshot(self, snapshot_id: str) -> dict[str, typing.Any]:
@ -732,15 +634,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Get Snaphost information',
)
# TODO: Remove this
# r = self._session.get(
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshotId),
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Get Snaphost information')
return r.json()['snapshot']
def update_snapshot(
@ -764,16 +657,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Update Snaphost information',
)
# TODO: Remove this
# r = self._session.put(
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshot_id),
# data=json.dumps(data),
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Update Snaphost information')
return r.json()['snapshot']
def create_volume_snapshot(
@ -799,16 +682,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Get Volume information',
)
# TODO: Remove this
# r = self._session.post(
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots',
# data=json.dumps(data),
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Cannot create snapshot. Ensure volume is in state "available"')
return r.json()['snapshot']
def create_volume_from_snapshot(
@ -832,16 +705,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Create Volume from Snapshot',
)
# TODO: Remove this
# r = self._session.post(
# self._get_endpoint_for('volumev3', 'volumev2') + '/volumes',
# data=json.dumps(data),
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Cannot create volume from snapshot.')
return r.json()['volume']
def create_server_from_snapshot(
@ -889,16 +752,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Create instance from snapshot',
)
# TODO: Remove this
# r = self._session.post(
# self._get_compute_endpoint() + '/servers',
# data=json.dumps(data),
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Cannot create instance from snapshot.')
return r.json()['server']
@auth_required(for_project=True)
@ -911,22 +764,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Cannot delete server (probably server does not exists).',
)
# TODO: Remove this
# r = self._session.post(
# self._getEndpointFor('compute', , 'compute_legacy') + '/servers/{server_id}/action'.format(server_id=serverId),
# data='{"forceDelete": null}',
# headers=self._requestHeaders(),
# timeout=self._timeout
# )
# r = self._session.delete(
# self._get_compute_endpoint() + f'/servers/{server_id}',
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Cannot delete server (probably server does not exists).')
def delete_snapshot(self, snapshot_id: str) -> None:
# This does not returns anything
self._request_from_endpoint(
@ -936,17 +773,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Cannot remove snapshot.',
)
# TODO: Remove this
# r = self._session.delete(
# self._get_endpoint_for('volumev3', 'volumev2') + '/snapshots/{snapshot_id}'.format(snapshot_id=snapshotId),
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Cannot remove snapshot.')
# Does not returns a message body
def start_server(self, server_id: str) -> None:
# this does not returns anything
self._request_from_endpoint(
@ -957,16 +783,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Starting server',
)
# TODO: Remove this
# r = self._session.post(
# self._get_compute_endpoint() + f'/servers/{server_id}/action',
# data='{"os-start": null}',
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Starting server')
def stop_server(self, server_id: str) -> None:
# this does not returns anything
self._request_from_endpoint(
@ -977,16 +793,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Stoping server',
)
# TODO: Remove this
# r = self._session.post(
# self._get_compute_endpoint() + f'/servers/{server_id}/action',
# data='{"os-stop": null}',
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Stoping server')
@auth_required(for_project=True)
def suspend_server(self, server_id: str) -> None:
# this does not returns anything
@ -998,16 +804,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Suspending server',
)
# TODO: Remove this
# r = self._session.post(
# self._get_compute_endpoint() + f'/servers/{server_id}/action',
# data='{"suspend": null}',
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Suspending server')
def resume_server(self, server_id: str) -> None:
# This does not returns anything
self._request_from_endpoint(
@ -1018,26 +814,18 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Resuming server',
)
# r = self._session.post(
# self._get_compute_endpoint() + f'/servers/{server_id}/action',
# data='{"resume": null}',
# headers=self._get_request_headers(),
# timeout=self._timeout,
# )
# ensure_valid_response(r, 'Resuming server')
def reset_server(self, server_id: str) -> None:
# Does not need return value
self._session.post(
self._get_compute_endpoint() + f'/servers/{server_id}/action',
data='{"reboot":{"type":"HARD"}}',
headers=self._get_request_headers(),
timeout=self._timeout,
)
# Ignore response for this...
# ensureResponseIsValid(r, 'Reseting server')
try:
self._request_from_endpoint(
'post',
endpoints_types=COMPUTE_ENDPOINT_TYPES,
path=f'/servers/{server_id}/action',
data='{"reboot":{"type":"HARD"}}',
error_message='Resetting server',
)
except Exception:
pass # Ignore error for reseting server
def test_connection(self) -> bool:
# First, ensure requested api is supported

View File

@ -36,33 +36,119 @@ import dataclasses
import enum
class State(enum.StrEnum):
ACTIVE = 'ACTIVE'
BUILDING = 'BUILDING'
DELETED = 'DELETED'
ERROR = 'ERROR'
HARD_REBOOT = 'HARD_REBOOT'
MIGRATING = 'MIGRATING'
PASSWORD = 'PASSWORD'
PAUSED = 'PAUSED'
REBOOT = 'REBOOT'
REBUILD = 'REBUILD'
RESCUED = 'RESCUED'
RESIZED = 'RESIZED'
REVERT_RESIZE = 'REVERT_RESIZE'
SOFT_DELETED = 'SOFT_DELETED'
STOPPED = 'STOPPED'
SUSPENDED = 'SUSPENDED'
UNKNOWN = 'UNKNOWN'
VERIFY_RESIZE = 'VERIFY_RESIZE'
SHUTOFF = 'SHUTOFF'
class Status(enum.StrEnum):
ACTIVE = 'ACTIVE' # The server is active.
BUILD = 'BUILD' # The server has not finished the original build process.
DELETED = 'DELETED' # The server is permanently deleted.
ERROR = 'ERROR' # The server is in error.
HARD_REBOOT = 'HARD_REBOOT' # The server is hard rebooting. This is equivalent to pulling the power plug on a physical server, plugging it back in, and rebooting it.
MIGRATING = 'MIGRATING' # The server is being migrated to a new host.
PASSWORD = 'PASSWORD' # The password is being reset on the server.
PAUSED = 'PAUSED' # In a paused state, the state of the server is stored in RAM. A paused server continues to run in frozen state.
REBOOT = (
'REBOOT' # The server is in a soft reboot state. A reboot command was passed to the operating system.
)
REBUILD = 'REBUILD' # The server is currently being rebuilt from an image.
RESCUE = 'RESCUE' # The server is in rescue mode. A rescue image is running with the original server image attached.
RESIZE = 'RESIZE' # Server is performing the differential copy of data that changed during its initial copy. Server is down for this stage.
REVERT_RESIZE = 'REVERT_RESIZE' # The resize or migration of a server failed for some reason. The destination server is being cleaned up and the original source server is restarting.
SHELVED = 'SHELVED' # The server is in shelved state. Depending on the shelve offload time, the server will be automatically shelved offloaded.
SHELVED_OFFLOADED = 'SHELVED_OFFLOADED' # The shelved server is offloaded (removed from the compute host) and it needs unshelved action to be used again.
SHUTOFF = 'SHUTOFF' # The server is powered off and the disk image still persists.
SOFT_DELETED = (
'SOFT_DELETED' # The server is marked as deleted but the disk images are still available to restore.
)
SUSPENDED = 'SUSPENDED' # The server is suspended, either by request or necessity. When you suspend a server, its state is stored on disk, all memory is written to disk, and the server is stopped. Suspending a server is similar to placing a device in hibernation and its occupied resource will not be freed but rather kept for when the server is resumed. If a server is infrequently used and the occupied resource needs to be freed to create other servers, it should be shelved.
UNKNOWN = 'UNKNOWN' # The state of the server is unknown. Contact your cloud provider.
VERIFY_RESIZE = 'VERIFY_RESIZE' # System is awaiting confirmation that the server is operational after a move or resize.
@staticmethod
def from_str(s: str) -> 'Status':
try:
return Status(s)
except ValueError:
return Status.UNKNOWN
# Helpers to check statuses
def is_lost(self) -> bool:
return self in [Status.DELETED, Status.ERROR, Status.UNKNOWN, Status.SOFT_DELETED]
def is_paused(self) -> bool:
return self in [Status.PAUSED, Status.SUSPENDED]
def is_running(self) -> bool:
return self in [Status.ACTIVE, Status.RESCUE, Status.RESIZE, Status.VERIFY_RESIZE]
def is_stopped(self) -> bool:
return self in [Status.SHUTOFF, Status.SHELVED, Status.SHELVED_OFFLOADED, Status.SOFT_DELETED]
class PowerState(enum.IntEnum):
NOSTATE = 0
RUNNING = 1
PAUSED = 3
SHUTDOWN = 4
CRASHED = 6
SUSPENDED = 7
@staticmethod
def from_int(i: int) -> 'PowerState':
try:
return PowerState(i)
except ValueError:
return PowerState.NOSTATE
def is_paused(self) -> bool:
return self == PowerState.PAUSED
def is_running(self) -> bool:
return self == PowerState.RUNNING
def is_stopped(self) -> bool:
return self in [PowerState.SHUTDOWN, PowerState.CRASHED, PowerState.SUSPENDED]
@dataclasses.dataclass
class VMInfo:
@dataclasses.dataclass
class AddresInfo:
version: int
addr: str
mac: str
type: str
network_name: str = ''
@staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'VMInfo.AddresInfo':
return VMInfo.AddresInfo(
version=d.get('version') or 4,
addr=d.get('addr') or '',
mac=d.get('OS-EXT-IPS-MAC:mac_addr') or '',
type=d.get('OS-EXT-IPS:type') or '',
)
@staticmethod
def from_addresses(adresses: dict[str, list[dict[str, typing.Any]]]) -> list['VMInfo.AddresInfo']:
def _build() -> typing.Generator['VMInfo.AddresInfo', None, None]:
for net_name, inner_addresses in adresses.items():
for address in inner_addresses:
address_info = VMInfo.AddresInfo.from_dict(address)
address_info.network_name = net_name
yield address_info
return list(_build())
id: str
name: str
href: str = ''
href: str
flavor: str
status: Status
power_state: PowerState
addresses: list[AddresInfo] # network_name: AddresInfo
access_addr_ipv4: str
access_addr_ipv6: str
fault: typing.Optional[str]
@staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'VMInfo':
@ -75,10 +161,22 @@ class VMInfo:
break
except Exception:
pass # Just ignore any error here
# Try to get flavor, only on >= 2.47
try:
flavor = d.get('flavor', {}).get('id', '')
except Exception:
flavor = ''
return VMInfo(
id=d['id'],
name=d['name'],
href=href,
flavor=flavor,
status=Status.from_str(d.get('status', Status.UNKNOWN.value)),
power_state=PowerState.from_int(d.get('OS-EXT-STS:power_state', PowerState.NOSTATE)),
addresses=VMInfo.AddresInfo.from_addresses(d.get('addresses', {})),
access_addr_ipv4=d.get('accessIPv4', ''),
access_addr_ipv6=d.get('accessIPv6', ''),
fault=d.get('fault', None),
)
@ -179,6 +277,7 @@ class VolumeTypeInfo:
name=d['name'],
)
@dataclasses.dataclass
class AvailabilityZoneInfo:
id: str
@ -189,4 +288,124 @@ class AvailabilityZoneInfo:
return AvailabilityZoneInfo(
id=d['zoneName'],
name=d['zoneName'],
)
)
@dataclasses.dataclass
class FlavorInfo:
id: str
name: str
vcpus: int
ram: int
disk: int
swap: int
is_public: bool
disabled: bool
@staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'FlavorInfo':
return FlavorInfo(
id=d['id'],
name=d['name'],
vcpus=d['vcpus'],
ram=d['ram'],
disk=d['disk'],
swap=d['swap'],
is_public=d.get('os-flavor-access:is_public', True),
disabled=d.get('OS-FLV-DISABLED:disabled', False),
)
@dataclasses.dataclass
class NetworkInfo:
id: str
name: str
status: str
shared: bool
subnets: list[str]
availability_zones: list[str]
@staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'NetworkInfo':
return NetworkInfo(
id=d['id'],
name=d['name'],
status=d['status'],
shared=d['shared'],
subnets=d['subnets'],
availability_zones=d.get('availability_zones', []),
)
@dataclasses.dataclass
class SubnetInfo:
id: str
name: str
cidr: str
enable_dhcp: bool
gateway_ip: str
ip_version: int
network_id: str
@staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'SubnetInfo':
return SubnetInfo(
id=d['id'],
name=d['name'],
cidr=d['cidr'],
enable_dhcp=d['enable_dhcp'],
gateway_ip=d['gateway_ip'],
ip_version=d['ip_version'],
network_id=d['network_id'],
)
@dataclasses.dataclass
class PortInfo:
@dataclasses.dataclass
class FixedIp:
ip_address: str
subnet_id: str
@staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'PortInfo.FixedIp':
return PortInfo.FixedIp(
ip_address=d['ip_address'],
subnet_id=d['subnet_id'],
)
id: str
name: str
status: str
device_id: str
device_owner: str
mac_address: str
fixed_ips: list['FixedIp']
@staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'PortInfo':
return PortInfo(
id=d['id'],
name=d['name'],
status=d['status'],
device_id=d['device_id'],
device_owner=d['device_owner'],
mac_address=d['mac_address'],
fixed_ips=[PortInfo.FixedIp.from_dict(ip) for ip in d['fixed_ips']],
)
@dataclasses.dataclass
class SecurityGroupInfo:
id: str
name: str
description: str
@staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'SecurityGroupInfo':
return SecurityGroupInfo(
id=d['id'],
name=d['name'],
description=d['description'],
)

View File

@ -41,7 +41,7 @@ from uds.core.ui import gui
from uds.core.util import validators, fields
from uds.core.util.decorators import cached
from . import openstack
from .openstack import openstack_client, sanitized_name
from .service import OpenStackLiveService
# Not imported at runtime, just for type checking
@ -188,7 +188,7 @@ class OpenStackProvider(ServiceProvider):
legacy = False
# Own variables
_api: typing.Optional[openstack.Client] = None
_api: typing.Optional[openstack_client.OpenstackClient] = None
def initialize(self, values: 'types.core.ValuesType' = None) -> None:
"""
@ -201,14 +201,14 @@ class OpenStackProvider(ServiceProvider):
def api(
self, projectid: typing.Optional[str] = None, region: typing.Optional[str] = None
) -> openstack.Client:
) -> openstack_client.OpenstackClient:
projectid = projectid or self.tenant.value or None
region = region or self.region.value or None
if self._api is None:
proxies = None
if self.https_proxy.value.strip():
proxies = {'https': self.https_proxy.value}
self._api = openstack.Client(
self._api = openstack_client.OpenstackClient(
self.endpoint.value,
-1,
self.domain.value,
@ -224,7 +224,7 @@ class OpenStackProvider(ServiceProvider):
return self._api
def sanitized_name(self, name: str) -> str:
return openstack.sanitized_name(name)
return sanitized_name(name)
def test_connection(self) -> types.core.TestResult:
"""

View File

@ -43,7 +43,7 @@ from uds.core.ui import gui
from uds.core.util import validators, fields
from uds.core.util.decorators import cached
from . import openstack
from .openstack import openstack_client, sanitized_name
from .service import OpenStackLiveService
# Not imported at runtime, just for type checking
@ -179,7 +179,7 @@ class OpenStackProviderLegacy(ServiceProvider):
legacy = True
# Own variables
_api: typing.Optional[openstack.Client] = None
_api: typing.Optional[openstack_client.OpenstackClient] = None
def initialize(self, values: 'types.core.ValuesType') -> None:
"""
@ -190,11 +190,11 @@ class OpenStackProviderLegacy(ServiceProvider):
if values is not None:
self.timeout.value = validators.validate_timeout(self.timeout.value)
def api(self, projectid: typing.Optional[str]=None, region: typing.Optional[str]=None) -> openstack.Client:
def api(self, projectid: typing.Optional[str]=None, region: typing.Optional[str]=None) -> openstack_client.OpenstackClient:
proxies: typing.Optional[dict[str, str]] = None
if self.https_proxy.value.strip():
proxies = {'https': self.https_proxy.value}
return openstack.Client(
return openstack_client.OpenstackClient(
self.host.value,
self.port.value,
self.domain.value,
@ -209,7 +209,7 @@ class OpenStackProviderLegacy(ServiceProvider):
)
def sanitized_name(self, name: str) -> str:
return openstack.sanitized_name(name)
return sanitized_name(name)
def test_connection(self) -> types.core.TestResult:
"""

View File

@ -36,11 +36,12 @@ import typing
from django.utils.translation import gettext_noop as _
from uds.core import services, types
from uds.core.util import validators
from uds.core.util import fields, validators
from uds.core.ui import gui
from .publication import OpenStackLivePublication
from .deployment import OpenStackLiveDeployment
from .openstack import types as openstack_types, openstack_client
from . import helpers
@ -48,7 +49,6 @@ logger = logging.getLogger(__name__)
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from . import openstack
from .provider import OpenStackProvider
from .provider_legacy import OpenStackProviderLegacy
@ -102,8 +102,6 @@ class OpenStackLiveService(services.Service):
allowed_protocols = types.transports.Protocol.generic_vdi(types.transports.Protocol.SPICE)
services_type_provided = types.services.ServiceType.VDI
# Now the form part
region = gui.ChoiceField(
label=_('Region'),
@ -174,31 +172,16 @@ class OpenStackLiveService(services.Service):
old_field_name='securityGroups',
)
baseName = gui.TextField(
label=_('Machine Names'),
readonly=False,
order=9,
tooltip=_('Base name for clones from this machine'),
required=True,
tab=_('Machine'),
)
basename = fields.basename_field(order=9, tab=_('Machine'))
lenname = fields.lenname_field(order=10, tab=_('Machine'))
lenName = gui.NumericField(
length=1,
label=_('Name Length'),
default=5,
order=10,
tooltip=_('Size of numeric part for the names of these machines'),
required=True,
tab=_('Machine'),
)
maintain_on_error = fields.maintain_on_error_field(order=104)
parent_uuid = gui.HiddenField(
)
parent_uuid = gui.HiddenField()
_api: typing.Optional['openstack.Client'] = None
_api: typing.Optional['openstack_client.OpenstackClient'] = None
def initialize(self, values: types.core.ValuesType) -> None:
def initialize(self, values: types.core.ValuesType) -> None:
"""
We check here form values to see if they are valid.
@ -206,7 +189,7 @@ class OpenStackLiveService(services.Service):
initialized by __init__ method of base class, before invoking this.
"""
if values:
validators.validate_basename(self.baseName.value, self.lenName.as_int())
validators.validate_basename(self.basename.value, self.lenname.as_int())
# self.ov.value = self.provider().serialize()
# self.ev.value = self.provider().env.key
@ -221,55 +204,45 @@ class OpenStackLiveService(services.Service):
api = self.provider().api()
# Checks if legacy or current openstack provider
parent = (
typing.cast('OpenStackProvider', self.provider())
if not self.provider().legacy
else None
)
parent = typing.cast('OpenStackProvider', self.provider()) if not self.provider().legacy else None
if parent and parent.region.value:
regions = [
gui.choice_item(parent.region.value, parent.region.value)
]
regions = [gui.choice_item(parent.region.value, parent.region.value)]
else:
regions = [gui.choice_item(r.id, r.name) for r in api.list_regions()]
self.region.set_choices(regions)
if parent and parent.tenant.value:
tenants = [
gui.choice_item(parent.tenant.value, parent.tenant.value)
]
tenants = [gui.choice_item(parent.tenant.value, parent.tenant.value)]
else:
tenants = [gui.choice_item(t.id, t.name) for t in api.list_projects()]
self.project.set_choices(tenants)
# So we can instantiate parent to get API
logger.debug(self.provider().serialize())
self.parent_uuid.value = self.provider().get_uuid()
@property
def api(self) -> 'openstack.Client':
def api(self) -> 'openstack_client.OpenstackClient':
if not self._api:
self._api = self.provider().api(
projectid=self.project.value, region=self.region.value
)
self._api = self.provider().api(projectid=self.project.value, region=self.region.value)
return self._api
def sanitized_name(self, name: str) -> str:
return self.provider().sanitized_name(name)
def make_template(self, template_name: str, description: typing.Optional[str] = None) -> dict[str, typing.Any]:
def make_template(
self, template_name: str, description: typing.Optional[str] = None
) -> dict[str, typing.Any]:
# First, ensures that volume has not any running instances
# if self.api.getVolume(self.volume.value)['status'] != 'available':
# raise Exception('The Volume is in use right now. Ensure that there is no machine running before publishing')
description = description or 'UDS Template snapshot'
return self.api.create_volume_snapshot(
self.volume.value, template_name, description
)
return self.api.create_volume_snapshot(self.volume.value, template_name, description)
def get_template(self, snapshot_id: str) -> dict[str, typing.Any]:
"""
@ -306,43 +279,20 @@ class OpenStackLiveService(services.Service):
"""
self.api.delete_snapshot(templateId)
def get_machine_state(self, machineId: str) -> str:
"""
Invokes getServer from openstack client
Args:
machineId: If of the machine to get state
Returns:
one of this values:
ACTIVE. The server is active.
BUILDING. The server has not finished the original build process.
DELETED. The server is permanently deleted.
ERROR. The server is in error.
HARD_REBOOT. The server is hard rebooting. This is equivalent to pulling the power plug on a physical server, plugging it back in, and rebooting it.
MIGRATING. The server is being migrated to a new host.
PASSWORD. The password is being reset on the server.
PAUSED. In a paused state, the state of the server is stored in RAM. A paused server continues to run in frozen state.
REBOOT. The server is in a soft reboot state. A reboot command was passed to the operating system.
REBUILD. The server is currently being rebuilt from an image.
RESCUED. The server is in rescue mode. A rescue image is running with the original server image attached.
RESIZED. Server is performing the differential copy of data that changed during its initial copy. Server is down for this stage.
REVERT_RESIZE. The resize or migration of a server failed for some reason. The destination server is being cleaned up and the original source server is restarting.
SOFT_DELETED. The server is marked as deleted but the disk images are still available to restore.
STOPPED. The server is powered off and the disk image still persists.
SUSPENDED. The server is suspended, either by request or necessity. This status appears for only the XenServer/XCP, KVM, and ESXi hypervisors. Administrative users can suspend an instance if it is infrequently used or to perform system maintenance. When you suspend an instance, its VM state is stored on disk, all memory is written to disk, and the virtual machine is stopped. Suspending an instance is similar to placing a device in hibernation; memory and vCPUs become available to create other instances.
VERIFY_RESIZE. System is awaiting confirmation that the server is operational after a move or resize.
SHUTOFF. The server was powered down by the user, either through the OpenStack Compute API or from within the server. For example, the user issued a shutdown -h command from within the server. If the OpenStack Compute manager detects that the VM was powered down, it transitions the server to the SHUTOFF status.
"""
server = self.api.get_server(machineId)
if server['status'] in ('ERROR', 'DELETED'):
def get_machine_state(self, machine_id: str) -> openstack_types.Status:
vminfo = self.api.get_server(machine_id)
if vminfo.status in (openstack_types.Status.ERROR, openstack_types.Status.DELETED):
logger.warning(
'Got server status %s for %s: %s',
server['status'],
machineId,
server.get('fault'),
vminfo.status,
machine_id,
vminfo.fault,
)
return server['status']
return vminfo.status
def get_machine_power_state(self, machine_id: str) -> openstack_types.PowerState:
vminfo = self.api.get_server(machine_id)
return vminfo.power_state
def start_machine(self, machineId: str) -> None:
"""
@ -416,24 +366,26 @@ class OpenStackLiveService(services.Service):
"""
Gets the mac address of first nic of the machine
"""
net = self.api.get_server(machineid)['addresses']
vals = next(iter(net.values()))[
0
] # Returns "any" mac address of any interface. We just need only one interface info
# vals = six.next(six.itervalues(net))[0]
return vals['OS-EXT-IPS-MAC:mac_addr'].upper(), vals['addr']
vminfo = self.api.get_server(machineid)
return vminfo.addresses[0].addr, vminfo.addresses[0].mac.upper()
def get_basename(self) -> str:
"""
Returns the base name
"""
return self.baseName.value
return self.basename.value
def get_lenname(self) -> int:
"""
Returns the length of numbers part
"""
return int(self.lenName.value)
return int(self.lenname.value)
def is_avaliable(self) -> bool:
return self.provider().is_available()
def can_clean_errored_userservices(self) -> bool:
return not self.maintain_on_error.value
def keep_on_error(self) -> bool:
return not self.maintain_on_error.as_bool()

View File

@ -76,14 +76,14 @@ class XenFailure(XenAPI.Failure, XenFault):
def as_human_readable(self) -> str:
try:
errList = {
error_list = {
XenFailure.exBadVmPowerState: 'Machine state is invalid for requested operation (needs {2} and state is {3})',
XenFailure.exVmMissingPVDrivers: 'Machine needs Xen Server Tools to allow requested operation',
XenFailure.exHostIsSlave: 'The connected host is an slave, try to connect to {1}',
XenFailure.exSRError: 'Error on SR: {2}',
XenFailure.exHandleInvalid: 'Invalid reference to {1}',
}
err = errList.get(typing.cast(typing.Any, self.details[0]), 'Error {0}')
err = error_list.get(typing.cast(typing.Any, self.details[0]), 'Error {0}')
return err.format(*typing.cast(list[typing.Any], self.details))
except Exception: