1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-02-09 09:57:36 +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) 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] cls: type[V]
for cls in classes: for cls in classes:
clsSubCls = cls.__subclasses__() clsSubCls = cls.__subclasses__()
if clsSubCls: if clsSubCls:
process(clsSubCls) # recursive add sub classes _process(clsSubCls) # recursive add sub classes
if not check_function(cls): if not check_function(cls):
logger.debug('Node is a not accepted, skipping: %s.%s', cls.__module__, cls.__name__) 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.error(' - Error registering %s.%s: %s', cls.__module__, cls.__name__, e)
logger.info('* Start registering %s', module_name) logger.info('* Start registering %s', module_name)
process(type_.__subclasses__()) _process(type_.__subclasses__())
logger.info('* Done Registering %s', module_name) 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 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_ # Will receive all classes that are subclasses of type_
return not cls.is_base return not cls.is_base
@ -195,5 +195,5 @@ def dynamically_load_and_register_modules(
factory.insert, factory.insert,
type_, type_,
module_name, module_name,
checker=checker, checker=_checker,
) )

View File

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

View File

@ -39,7 +39,7 @@ import typing
from uds.core import consts, services, types from uds.core import consts, services, types
from uds.core.util import autoserializable, log 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 # Not imported at runtime, just for type checking
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
@ -155,12 +155,14 @@ class OpenStackLiveDeployment(
try: try:
status = self.service().get_machine_state(self._vmid) 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') 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) self.service().resume_machine(self._vmid)
elif status in (openstack.STOPPED, openstack.SHUTOFF): elif power_state.is_stopped():
self.service().start_machine(self._vmid) self.service().start_machine(self._vmid)
# Right now, we suppose the machine is ready # Right now, we suppose the machine is ready
@ -207,26 +209,22 @@ class OpenStackLiveDeployment(
else: else:
self._queue = [Operation.CREATE, Operation.WAIT, Operation.SUSPEND, Operation.FINISH] 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( logger.debug(
'Checking that state of machine %s (%s) is %s', 'Checking that state of machine %s (%s) is %s',
self._vmid, self._vmid,
self._name, self._name,
chkState, check_state,
) )
status = self.service().get_machine_state(self._vmid) power_state = self.service().get_machine_power_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))
ret = types.states.TaskState.RUNNING ret = types.states.TaskState.RUNNING
chkStates = [chkState] if not isinstance(chkState, (list, tuple)) else chkState if power_state == check_state:
if status in chkStates:
ret = types.states.TaskState.FINISHED ret = types.states.TaskState.FINISHED
return ret return ret
def _get_current_op(self) -> Operation: def _get_current_op(self) -> Operation:
if not self._queue: if not self._queue:
return Operation.FINISH return Operation.FINISH
@ -252,7 +250,7 @@ class OpenStackLiveDeployment(
self.do_log(log.LogLevel.ERROR, self._reason) 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: try:
self.service().remove_machine(self._vmid) self.service().remove_machine(self._vmid)
except Exception: except Exception:
@ -333,7 +331,7 @@ class OpenStackLiveDeployment(
""" """
status = self.service().get_machine_state(self._vmid) 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)) raise Exception('Machine not found. (Status {})'.format(status))
self.service().remove_machine(self._vmid) self.service().remove_machine(self._vmid)
@ -361,7 +359,7 @@ class OpenStackLiveDeployment(
""" """
Checks the state of a deploy for an user or cache 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: if ret == types.states.TaskState.FINISHED:
# Get IP & MAC (early stage) # Get IP & MAC (early stage)
self._mac, self._ip = self.service().get_network_info(self._vmid) self._mac, self._ip = self.service().get_network_info(self._vmid)
@ -372,13 +370,13 @@ class OpenStackLiveDeployment(
""" """
Checks if machine has started 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: def _check_suspend(self) -> types.states.TaskState:
""" """
Check if the machine has suspended 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: 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()] zones = [gui.choice_item(z.id, z.name) for z in api.list_availability_zones()]
networks = [ networks = [
gui.choice_item(z['id'], z['name']) for z in api.list_networks(name_from_subnets=name_from_subnets) 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()
] ]
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 = [ data: types.ui.CallbackResultType = [
{'name': 'availabilityZone', 'choices': zones}, {'name': 'availability_zone', 'choices': zones},
{'name': 'network', 'choices': networks}, {'name': 'network', 'choices': networks},
{'name': 'flavor', 'choices': flavors}, {'name': 'flavor', 'choices': flavors},
{'name': 'securityGroups', 'choices': securityGroups}, {'name': 'security_groups', 'choices': security_groups},
{'name': 'volumeType', 'choices': volumeTypes},
] ]
logger.debug('Return data: %s', data) logger.debug('Return data: %s', data)
return data return data

View File

@ -30,12 +30,10 @@
""" """
Author: Adolfo Gómez, dkmaster at dkmon dot com Author: Adolfo Gómez, dkmaster at dkmon dot com
""" """
# pyright: reportUnusedImport=false import re
from .openstack_client import Client def sanitized_name(name: str) -> str:
"""
# Import submodules machine names with [a-zA-Z0-9_-]
from .common import * """
return re.sub("[^a-zA-Z0-9._-]", "_", name)
# Logger imported from .common, if you ask
logger = logging.getLogger(__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', 'volumev2',
] # 'volume' is also valid, but it is deprecated A LONG TYPE AGO ] # 'volume' is also valid, but it is deprecated A LONG TYPE AGO
COMPUTE_ENDPOINT_TYPES = ['compute', 'compute_legacy'] COMPUTE_ENDPOINT_TYPES = ['compute', 'compute_legacy']
NETWORKS_ENDPOINT_TYPES = ['network']
# Helpers # Helpers
@ -122,7 +123,7 @@ def auth_required(for_project: bool = False) -> collections.abc.Callable[[decora
def decorator(func: decorators.FT) -> decorators.FT: def decorator(func: decorators.FT) -> decorators.FT:
@functools.wraps(func) @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 for_project is True:
if obj._projectid is None: if obj._projectid is None:
raise Exception('Need a project for method {}'.format(func)) 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 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( return '_'.join(
[ [
obj._authurl, 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' PUBLIC = 'public'
PRIVATE = 'private' PRIVATE = 'private'
INTERNAL = 'url' INTERNAL = 'url'
@ -200,7 +201,7 @@ class Client: # pylint: disable=too-many-public-methods
self._catalog = None self._catalog = None
self._is_legacy = is_legacy 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._domain, self._username, self._password = domain, username, password
self._userid = None self._userid = None
self._projectid = projectid 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) @decorators.cached(prefix='vols', timeout=consts.cache.SHORT_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_volumes(self) -> list[openstack_types.VolumeInfo]: def list_volumes(self) -> list[openstack_types.VolumeInfo]:
return [ 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( def list_volume_snapshots(
self, volume_id: typing.Optional[dict[str, typing.Any]] = None self, volume_id: typing.Optional[dict[str, typing.Any]] = None
) -> list[openstack_types.VolumeSnapshotInfo]: ) -> 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 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) @decorators.cached(prefix='azs', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_availability_zones(self) -> list[openstack_types.AvailabilityZoneInfo]: def list_availability_zones(self) -> list[openstack_types.AvailabilityZoneInfo]:
return [ return [
@ -551,155 +520,97 @@ class Client: # pylint: disable=too-many-public-methods
if availability_zone['zoneState']['available'] is True 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) @decorators.cached(prefix='flvs', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_flavors(self) -> list[typing.Any]: def list_flavors(self) -> list[openstack_types.FlavorInfo]:
return list( return [
self._get_recurring_from_endpoint( openstack_types.FlavorInfo.from_dict(f)
for f in self._get_recurring_from_endpoint(
endpoint_types=COMPUTE_ENDPOINT_TYPES, endpoint_types=COMPUTE_ENDPOINT_TYPES,
path='/flavors', path='/flavors/detail',
error_message='List Flavors', error_message='List Flavors',
key='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) @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( nets = self._get_recurring_from_endpoint(
endpoint_types=['network'], endpoint_types=NETWORKS_ENDPOINT_TYPES,
path='/v2.0/networks', path='/v2.0/networks',
error_message='List Networks', error_message='List Networks',
key='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: if not name_from_subnets:
return list(nets) return [openstack_types.NetworkInfo.from_dict(n) for n in nets]
else: else:
# Get and cache subnets names # Get and cache subnets names
subnetNames = {s['id']: s['name'] for s in self.list_subnets()} subnets_dct = {s.id: f'{s.name} ({s.cidr})' for s in self.list_subnets()}
res: list[typing.Any] = [] res: list[openstack_types.NetworkInfo] = []
for net in nets: for net in nets:
name = ','.join(subnetNames[i] for i in net['subnets'] if i in subnetNames) name = ','.join(subnets_dct[i] for i in net['subnets'] if i in subnets_dct)
net['old_name'] = net['name']
if name: if name:
net['name'] = name net['name'] = name
res.append(net) res.append(openstack_types.NetworkInfo.from_dict(net))
return res return res
@decorators.cached(prefix='subns', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper) @decorators.cached(prefix='subns', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_subnets(self) -> collections.abc.Iterable[typing.Any]: def list_subnets(self) -> collections.abc.Iterable[openstack_types.SubnetInfo]:
return list( return [
self._get_recurring_from_endpoint( openstack_types.SubnetInfo.from_dict(s)
endpoint_types=['network'], for s in self._get_recurring_from_endpoint(
endpoint_types=NETWORKS_ENDPOINT_TYPES,
path='/v2.0/subnets', path='/v2.0/subnets',
error_message='List Subnets', error_message='List Subnets',
key='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) @decorators.cached(prefix='sgps', timeout=consts.cache.LONG_CACHE_TIMEOUT, key_helper=cache_key_helper)
def list_ports( def list_ports(
self, self,
networkId: typing.Optional[str] = None, network_id: typing.Optional[str] = None,
ownerId: typing.Optional[str] = None, owner_id: typing.Optional[str] = None,
) -> list[typing.Any]: ) -> list[openstack_types.PortInfo]:
params: dict[str, typing.Any] = {} params: dict[str, typing.Any] = {}
if networkId is not None: if network_id is not None:
params['network_id'] = networkId params['network_id'] = network_id
if ownerId is not None: if owner_id is not None:
params['device_owner'] = ownerId params['device_owner'] = owner_id
return list( return [
self._get_recurring_from_endpoint( openstack_types.PortInfo.from_dict(p)
endpoint_types=['network'], for p in self._get_recurring_from_endpoint(
endpoint_types=NETWORKS_ENDPOINT_TYPES,
path='/v2.0/ports', path='/v2.0/ports',
error_message='List ports', error_message='List ports',
key='ports', key='ports',
params=params, params=params,
) )
) ]
# TODO: Remove this @decorators.cached(prefix='sgps', timeout=consts.cache.EXTREME_CACHE_TIMEOUT, key_helper=cache_key_helper)
# return get_recurring_url_json( def list_security_groups(self) -> list[openstack_types.SecurityGroupInfo]:
# self._get_endpoint_for('network') + '/v2.0/ports', return [
# self._session, openstack_types.SecurityGroupInfo.from_dict(sg)
# headers=self._get_request_headers(), for sg in self._get_recurring_from_endpoint(
# key='ports', endpoint_types=NETWORKS_ENDPOINT_TYPES,
# 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'],
path='/v2.0/security-groups', path='/v2.0/security-groups',
error_message='List security groups', error_message='List security groups',
key='security_groups', key='security_groups',
) )
) ]
# TODO: Remove this def get_server(self, server_id: str) -> openstack_types.VMInfo:
# 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]:
r = self._request_from_endpoint( r = self._request_from_endpoint(
'get', 'get',
endpoints_types=COMPUTE_ENDPOINT_TYPES, endpoints_types=COMPUTE_ENDPOINT_TYPES,
path=f'/servers/{server_id}', path=f'/servers/{server_id}',
error_message='Get Server information', 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]: def get_volume(self, volumeId: str) -> dict[str, typing.Any]:
r = self._request_from_endpoint( r = self._request_from_endpoint(
@ -709,15 +620,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Get Volume information', 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'] return r.json()['volume']
def get_snapshot(self, snapshot_id: str) -> dict[str, typing.Any]: 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', 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'] return r.json()['snapshot']
def update_snapshot( def update_snapshot(
@ -764,16 +657,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Update Snaphost information', 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'] return r.json()['snapshot']
def create_volume_snapshot( def create_volume_snapshot(
@ -799,16 +682,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Get Volume information', 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'] return r.json()['snapshot']
def create_volume_from_snapshot( def create_volume_from_snapshot(
@ -832,16 +705,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Create Volume from Snapshot', 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'] return r.json()['volume']
def create_server_from_snapshot( def create_server_from_snapshot(
@ -889,16 +752,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Create instance from snapshot', 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'] return r.json()['server']
@auth_required(for_project=True) @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).', 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: def delete_snapshot(self, snapshot_id: str) -> None:
# This does not returns anything # This does not returns anything
self._request_from_endpoint( self._request_from_endpoint(
@ -936,17 +773,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Cannot remove snapshot.', 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: def start_server(self, server_id: str) -> None:
# this does not returns anything # this does not returns anything
self._request_from_endpoint( self._request_from_endpoint(
@ -957,16 +783,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Starting server', 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: def stop_server(self, server_id: str) -> None:
# this does not returns anything # this does not returns anything
self._request_from_endpoint( self._request_from_endpoint(
@ -977,16 +793,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Stoping server', 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) @auth_required(for_project=True)
def suspend_server(self, server_id: str) -> None: def suspend_server(self, server_id: str) -> None:
# this does not returns anything # this does not returns anything
@ -998,16 +804,6 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Suspending server', 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: def resume_server(self, server_id: str) -> None:
# This does not returns anything # This does not returns anything
self._request_from_endpoint( self._request_from_endpoint(
@ -1018,26 +814,18 @@ class Client: # pylint: disable=too-many-public-methods
error_message='Resuming server', 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: def reset_server(self, server_id: str) -> None:
# Does not need return value # Does not need return value
self._session.post( try:
self._get_compute_endpoint() + f'/servers/{server_id}/action', self._request_from_endpoint(
data='{"reboot":{"type":"HARD"}}', 'post',
headers=self._get_request_headers(), endpoints_types=COMPUTE_ENDPOINT_TYPES,
timeout=self._timeout, path=f'/servers/{server_id}/action',
) data='{"reboot":{"type":"HARD"}}',
error_message='Resetting server',
# Ignore response for this... )
# ensureResponseIsValid(r, 'Reseting server') except Exception:
pass # Ignore error for reseting server
def test_connection(self) -> bool: def test_connection(self) -> bool:
# First, ensure requested api is supported # First, ensure requested api is supported

View File

@ -36,33 +36,119 @@ import dataclasses
import enum import enum
class State(enum.StrEnum): class Status(enum.StrEnum):
ACTIVE = 'ACTIVE' ACTIVE = 'ACTIVE' # The server is active.
BUILDING = 'BUILDING' BUILD = 'BUILD' # The server has not finished the original build process.
DELETED = 'DELETED' DELETED = 'DELETED' # The server is permanently deleted.
ERROR = 'ERROR' ERROR = 'ERROR' # The server is in error.
HARD_REBOOT = 'HARD_REBOOT' 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' MIGRATING = 'MIGRATING' # The server is being migrated to a new host.
PASSWORD = 'PASSWORD' PASSWORD = 'PASSWORD' # The password is being reset on the server.
PAUSED = 'PAUSED' 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' REBOOT = (
REBUILD = 'REBUILD' 'REBOOT' # The server is in a soft reboot state. A reboot command was passed to the operating system.
RESCUED = 'RESCUED' )
RESIZED = 'RESIZED' REBUILD = 'REBUILD' # The server is currently being rebuilt from an image.
REVERT_RESIZE = 'REVERT_RESIZE' RESCUE = 'RESCUE' # The server is in rescue mode. A rescue image is running with the original server image attached.
SOFT_DELETED = 'SOFT_DELETED' RESIZE = 'RESIZE' # Server is performing the differential copy of data that changed during its initial copy. Server is down for this stage.
STOPPED = 'STOPPED' 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.
SUSPENDED = 'SUSPENDED' SHELVED = 'SHELVED' # The server is in shelved state. Depending on the shelve offload time, the server will be automatically shelved offloaded.
UNKNOWN = 'UNKNOWN' SHELVED_OFFLOADED = 'SHELVED_OFFLOADED' # The shelved server is offloaded (removed from the compute host) and it needs unshelved action to be used again.
VERIFY_RESIZE = 'VERIFY_RESIZE' SHUTOFF = 'SHUTOFF' # The server is powered off and the disk image still persists.
SHUTOFF = 'SHUTOFF' 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 @dataclasses.dataclass
class VMInfo: 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 id: str
name: 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 @staticmethod
def from_dict(d: dict[str, typing.Any]) -> 'VMInfo': def from_dict(d: dict[str, typing.Any]) -> 'VMInfo':
@ -75,10 +161,22 @@ class VMInfo:
break break
except Exception: except Exception:
pass # Just ignore any error here 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( return VMInfo(
id=d['id'], id=d['id'],
name=d['name'], name=d['name'],
href=href, 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'], name=d['name'],
) )
@dataclasses.dataclass @dataclasses.dataclass
class AvailabilityZoneInfo: class AvailabilityZoneInfo:
id: str id: str
@ -189,4 +288,124 @@ class AvailabilityZoneInfo:
return AvailabilityZoneInfo( return AvailabilityZoneInfo(
id=d['zoneName'], id=d['zoneName'],
name=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 import validators, fields
from uds.core.util.decorators import cached from uds.core.util.decorators import cached
from . import openstack from .openstack import openstack_client, sanitized_name
from .service import OpenStackLiveService from .service import OpenStackLiveService
# Not imported at runtime, just for type checking # Not imported at runtime, just for type checking
@ -188,7 +188,7 @@ class OpenStackProvider(ServiceProvider):
legacy = False legacy = False
# Own variables # Own variables
_api: typing.Optional[openstack.Client] = None _api: typing.Optional[openstack_client.OpenstackClient] = None
def initialize(self, values: 'types.core.ValuesType' = None) -> None: def initialize(self, values: 'types.core.ValuesType' = None) -> None:
""" """
@ -201,14 +201,14 @@ class OpenStackProvider(ServiceProvider):
def api( def api(
self, projectid: typing.Optional[str] = None, region: typing.Optional[str] = None self, projectid: typing.Optional[str] = None, region: typing.Optional[str] = None
) -> openstack.Client: ) -> openstack_client.OpenstackClient:
projectid = projectid or self.tenant.value or None projectid = projectid or self.tenant.value or None
region = region or self.region.value or None region = region or self.region.value or None
if self._api is None: if self._api is None:
proxies = None proxies = None
if self.https_proxy.value.strip(): if self.https_proxy.value.strip():
proxies = {'https': self.https_proxy.value} proxies = {'https': self.https_proxy.value}
self._api = openstack.Client( self._api = openstack_client.OpenstackClient(
self.endpoint.value, self.endpoint.value,
-1, -1,
self.domain.value, self.domain.value,
@ -224,7 +224,7 @@ class OpenStackProvider(ServiceProvider):
return self._api return self._api
def sanitized_name(self, name: str) -> str: def sanitized_name(self, name: str) -> str:
return openstack.sanitized_name(name) return sanitized_name(name)
def test_connection(self) -> types.core.TestResult: 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 import validators, fields
from uds.core.util.decorators import cached from uds.core.util.decorators import cached
from . import openstack from .openstack import openstack_client, sanitized_name
from .service import OpenStackLiveService from .service import OpenStackLiveService
# Not imported at runtime, just for type checking # Not imported at runtime, just for type checking
@ -179,7 +179,7 @@ class OpenStackProviderLegacy(ServiceProvider):
legacy = True legacy = True
# Own variables # Own variables
_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:
""" """
@ -190,11 +190,11 @@ class OpenStackProviderLegacy(ServiceProvider):
if values is not None: if values is not None:
self.timeout.value = validators.validate_timeout(self.timeout.value) 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 proxies: typing.Optional[dict[str, str]] = None
if self.https_proxy.value.strip(): if self.https_proxy.value.strip():
proxies = {'https': self.https_proxy.value} proxies = {'https': self.https_proxy.value}
return openstack.Client( return openstack_client.OpenstackClient(
self.host.value, self.host.value,
self.port.value, self.port.value,
self.domain.value, self.domain.value,
@ -209,7 +209,7 @@ class OpenStackProviderLegacy(ServiceProvider):
) )
def sanitized_name(self, name: str) -> str: def sanitized_name(self, name: str) -> str:
return openstack.sanitized_name(name) return sanitized_name(name)
def test_connection(self) -> types.core.TestResult: def test_connection(self) -> types.core.TestResult:
""" """

View File

@ -36,11 +36,12 @@ import typing
from django.utils.translation import gettext_noop as _ from django.utils.translation import gettext_noop as _
from uds.core import services, types 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 uds.core.ui import gui
from .publication import OpenStackLivePublication from .publication import OpenStackLivePublication
from .deployment import OpenStackLiveDeployment from .deployment import OpenStackLiveDeployment
from .openstack import types as openstack_types, openstack_client
from . import helpers from . import helpers
@ -48,7 +49,6 @@ logger = logging.getLogger(__name__)
# Not imported at runtime, just for type checking # Not imported at runtime, just for type checking
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from . import openstack
from .provider import OpenStackProvider from .provider import OpenStackProvider
from .provider_legacy import OpenStackProviderLegacy from .provider_legacy import OpenStackProviderLegacy
@ -102,8 +102,6 @@ class OpenStackLiveService(services.Service):
allowed_protocols = types.transports.Protocol.generic_vdi(types.transports.Protocol.SPICE) allowed_protocols = types.transports.Protocol.generic_vdi(types.transports.Protocol.SPICE)
services_type_provided = types.services.ServiceType.VDI services_type_provided = types.services.ServiceType.VDI
# Now the form part # Now the form part
region = gui.ChoiceField( region = gui.ChoiceField(
label=_('Region'), label=_('Region'),
@ -174,31 +172,16 @@ class OpenStackLiveService(services.Service):
old_field_name='securityGroups', old_field_name='securityGroups',
) )
baseName = gui.TextField( basename = fields.basename_field(order=9, tab=_('Machine'))
label=_('Machine Names'), lenname = fields.lenname_field(order=10, tab=_('Machine'))
readonly=False,
order=9,
tooltip=_('Base name for clones from this machine'),
required=True,
tab=_('Machine'),
)
lenName = gui.NumericField( maintain_on_error = fields.maintain_on_error_field(order=104)
length=1,
label=_('Name Length'),
default=5,
order=10,
tooltip=_('Size of numeric part for the names of these machines'),
required=True,
tab=_('Machine'),
)
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. 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. initialized by __init__ method of base class, before invoking this.
""" """
if values: 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.ov.value = self.provider().serialize()
# self.ev.value = self.provider().env.key # self.ev.value = self.provider().env.key
@ -221,55 +204,45 @@ class OpenStackLiveService(services.Service):
api = self.provider().api() api = self.provider().api()
# Checks if legacy or current openstack provider # Checks if legacy or current openstack provider
parent = ( parent = typing.cast('OpenStackProvider', self.provider()) if not self.provider().legacy else None
typing.cast('OpenStackProvider', self.provider())
if not self.provider().legacy
else None
)
if parent and parent.region.value: if parent and parent.region.value:
regions = [ regions = [gui.choice_item(parent.region.value, parent.region.value)]
gui.choice_item(parent.region.value, parent.region.value)
]
else: else:
regions = [gui.choice_item(r.id, r.name) for r in api.list_regions()] regions = [gui.choice_item(r.id, r.name) for r in api.list_regions()]
self.region.set_choices(regions) self.region.set_choices(regions)
if parent and parent.tenant.value: if parent and parent.tenant.value:
tenants = [ tenants = [gui.choice_item(parent.tenant.value, parent.tenant.value)]
gui.choice_item(parent.tenant.value, parent.tenant.value)
]
else: else:
tenants = [gui.choice_item(t.id, t.name) for t in api.list_projects()] tenants = [gui.choice_item(t.id, t.name) for t in api.list_projects()]
self.project.set_choices(tenants) self.project.set_choices(tenants)
# So we can instantiate parent to get API # So we can instantiate parent to get API
logger.debug(self.provider().serialize()) logger.debug(self.provider().serialize())
self.parent_uuid.value = self.provider().get_uuid() self.parent_uuid.value = self.provider().get_uuid()
@property @property
def api(self) -> 'openstack.Client': def api(self) -> 'openstack_client.OpenstackClient':
if not self._api: if not self._api:
self._api = self.provider().api( self._api = self.provider().api(projectid=self.project.value, region=self.region.value)
projectid=self.project.value, region=self.region.value
)
return self._api return self._api
def sanitized_name(self, name: str) -> str: def sanitized_name(self, name: str) -> str:
return self.provider().sanitized_name(name) 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 # First, ensures that volume has not any running instances
# if self.api.getVolume(self.volume.value)['status'] != 'available': # 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') # raise Exception('The Volume is in use right now. Ensure that there is no machine running before publishing')
description = description or 'UDS Template snapshot' description = description or 'UDS Template snapshot'
return self.api.create_volume_snapshot( return self.api.create_volume_snapshot(self.volume.value, template_name, description)
self.volume.value, template_name, description
)
def get_template(self, snapshot_id: str) -> dict[str, typing.Any]: def get_template(self, snapshot_id: str) -> dict[str, typing.Any]:
""" """
@ -306,43 +279,20 @@ class OpenStackLiveService(services.Service):
""" """
self.api.delete_snapshot(templateId) self.api.delete_snapshot(templateId)
def get_machine_state(self, machineId: str) -> str: def get_machine_state(self, machine_id: str) -> openstack_types.Status:
""" vminfo = self.api.get_server(machine_id)
Invokes getServer from openstack client if vminfo.status in (openstack_types.Status.ERROR, openstack_types.Status.DELETED):
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'):
logger.warning( logger.warning(
'Got server status %s for %s: %s', 'Got server status %s for %s: %s',
server['status'], vminfo.status,
machineId, machine_id,
server.get('fault'), 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: 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 Gets the mac address of first nic of the machine
""" """
net = self.api.get_server(machineid)['addresses'] vminfo = self.api.get_server(machineid)
vals = next(iter(net.values()))[ return vminfo.addresses[0].addr, vminfo.addresses[0].mac.upper()
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']
def get_basename(self) -> str: def get_basename(self) -> str:
""" """
Returns the base name Returns the base name
""" """
return self.baseName.value return self.basename.value
def get_lenname(self) -> int: def get_lenname(self) -> int:
""" """
Returns the length of numbers part Returns the length of numbers part
""" """
return int(self.lenName.value) return int(self.lenname.value)
def is_avaliable(self) -> bool: def is_avaliable(self) -> bool:
return self.provider().is_available() 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: def as_human_readable(self) -> str:
try: try:
errList = { error_list = {
XenFailure.exBadVmPowerState: 'Machine state is invalid for requested operation (needs {2} and state is {3})', 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.exVmMissingPVDrivers: 'Machine needs Xen Server Tools to allow requested operation',
XenFailure.exHostIsSlave: 'The connected host is an slave, try to connect to {1}', XenFailure.exHostIsSlave: 'The connected host is an slave, try to connect to {1}',
XenFailure.exSRError: 'Error on SR: {2}', XenFailure.exSRError: 'Error on SR: {2}',
XenFailure.exHandleInvalid: 'Invalid reference to {1}', 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)) return err.format(*typing.cast(list[typing.Any], self.details))
except Exception: except Exception: