diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..b6cdd3f --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,35 @@ +name: Coriolis Client Unit Tests + +on: + workflow_dispatch: + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-22.04 + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + architecture: ["x64"] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.architecture }} + - name: Install python3 tox + shell: bash + run: | + python3 -m pip install tox + - name: Run unit tests with tox + shell: bash + run: | + tox -e py3,pep8 -v + diff --git a/.gitignore b/.gitignore index 29b5574..baedd35 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,11 @@ *.swo *.swp *~ -.coverage +.coverage* .idea .testrepository .tox +.stestr/ AUTHORS build ChangeLog @@ -20,6 +21,8 @@ doc/source/api/ # Development environment files .project .pydevproject +coverage.xml cover # Files created by releasenotes build releasenotes/build +.vscode/ diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..6a5b262 --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=${OS_TEST_PATH:-./coriolisclient/tests} +top_dir=./ diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..6d83b3c --- /dev/null +++ b/.testr.conf @@ -0,0 +1,7 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ + OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ + ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/coriolisclient/base.py b/coriolisclient/base.py index 774564f..22172ef 100644 --- a/coriolisclient/base.py +++ b/coriolisclient/base.py @@ -59,6 +59,7 @@ def wrapper(*args, **kwargs): return wrapper + class Resource(object): """Base class for OpenStack resources (tenant, user, etc.). diff --git a/coriolisclient/cli/migrations.py b/coriolisclient/cli/deployments.py similarity index 53% rename from coriolisclient/cli/migrations.py rename to coriolisclient/cli/deployments.py index 7c10b56..74c6139 100644 --- a/coriolisclient/cli/migrations.py +++ b/coriolisclient/cli/deployments.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cloudbase Solutions Srl +# Copyright (c) 2024 Cloudbase Solutions Srl # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ # limitations under the License. """ -Command-line interface sub-commands related to migrations. +Command-line interface sub-commands related to deployments. """ import os @@ -27,9 +27,10 @@ from coriolisclient.cli import utils as cli_utils -class MigrationFormatter(formatter.EntityFormatter): +class DeploymentFormatter(formatter.EntityFormatter): columns = ("ID", + "Transfer ID", "Status", "Instances", "Notes", @@ -41,6 +42,7 @@ def _get_sorted_list(self, obj_list): def _get_formatted_data(self, obj): data = (obj.id, + obj.transfer_id, obj.last_execution_status, "\n".join(obj.instances), obj.notes, @@ -49,7 +51,7 @@ def _get_formatted_data(self, obj): return data -class MigrationDetailFormatter(formatter.EntityFormatter): +class DeploymentDetailFormatter(formatter.EntityFormatter): def __init__(self, show_instances_data=False): self.columns = [ @@ -57,6 +59,8 @@ def __init__(self, show_instances_data=False): "status", "created", "last_updated", + "transfer_id", + "transfer_scenario_type", "reservation_id", "instances", "notes", @@ -65,7 +69,6 @@ def __init__(self, show_instances_data=False): "destination_endpoint_id", "destination_minion_pool_id", "instance_osmorphing_minion_pool_mappings", - "replication_count", "shutdown_instances", "destination_environment", "source_environment", @@ -122,6 +125,8 @@ def _get_formatted_data(self, obj): obj.last_execution_status, obj.created_at, obj.updated_at, + obj.transfer_id, + obj.transfer_scenario_type, obj.reservation_id, self._format_instances(obj), obj.notes, @@ -131,7 +136,6 @@ def _get_formatted_data(self, obj): obj.destination_minion_pool_id, cli_utils.format_json_for_object_property( obj, 'instance_osmorphing_minion_pool_mappings'), - getattr(obj, 'replication_count', None), getattr(obj, 'shutdown_instances', False), cli_utils.format_json_for_object_property( obj, prop_name="destination_environment"), @@ -153,118 +157,18 @@ def _get_formatted_data(self, obj): return data -class CreateMigration(show.ShowOne): - """Start a new migration""" +class CreateDeployment(show.ShowOne): + """Start a new deployment from an existing transfer""" def get_parser(self, prog_name): - parser = super(CreateMigration, self).get_parser(prog_name) - parser.add_argument('--origin-endpoint', required=True, - help='The origin endpoint id') - parser.add_argument('--destination-endpoint', required=True, - help='The destination endpoint id') - parser.add_argument('--instance', action='append', required=True, - dest="instances", metavar="INSTANCE_IDENTIFIER", - help='The identifier of a source instance to be ' - 'migrated. Can be specified multiple times') - parser.add_argument('--notes', dest='notes', - help='Notes about the migration') - parser.add_argument('--user-script-global', action='append', - required=False, - dest="global_scripts", - help='A script that will run for a particular ' - 'os_type. This option can be used multiple ' - 'times. Use: linux=/path/to/script.sh or ' - 'windows=/path/to/script.ps1') - parser.add_argument('--user-script-instance', action='append', - required=False, - dest="instance_scripts", - help='A script that will run for a particular ' - 'instance specified by the --instance option. ' - 'This option can be used multiple times. ' - 'Use: "instance_name"=/path/to/script.sh.' - ' This option overwrites any OS specific script ' - 'specified in --user-script-global for this ' - 'instance') - parser.add_argument('--skip-os-morphing', - help='Skip the OS morphing process', - action='store_true', - default=False) - parser.add_argument('--replication-count', - type=int, - help='Number of times to perform a replica sync ' - 'before deploying the migrated instance.') - parser.add_argument('--shutdown-instances', - action='store_true', - help='Whether or not to shut down the instance on ' - 'the source platform before performing the ' - 'final Replica sync') - - cli_utils.add_args_for_json_option_to_parser( - parser, 'destination-environment') - cli_utils.add_args_for_json_option_to_parser(parser, 'network-map') - cli_utils.add_args_for_json_option_to_parser( - parser, 'source-environment') - cli_utils.add_storage_mappings_arguments_to_parser(parser) - cli_utils.add_minion_pool_args_to_parser( - parser, include_origin_pool_arg=True, - include_destination_pool_arg=True, - include_osmorphing_pool_mappings_arg=True) - - return parser - - def take_action(self, args): - destination_environment = cli_utils.get_option_value_from_args( - args, 'destination-environment') - source_environment = cli_utils.get_option_value_from_args( - args, 'source-environment') - network_map = cli_utils.get_option_value_from_args( - args, 'network-map') - storage_mappings = cli_utils.get_storage_mappings_dict_from_args(args) - endpoints = self.app.client_manager.coriolis.endpoints - origin_endpoint_id = endpoints.get_endpoint_id_for_name( - args.origin_endpoint) - destination_endpoint_id = endpoints.get_endpoint_id_for_name( - args.destination_endpoint) - user_scripts = cli_utils.compose_user_scripts( - args.global_scripts, args.instance_scripts) - instance_osmorphing_minion_pool_mappings = None - if args.instance_osmorphing_minion_pool_mappings: - instance_osmorphing_minion_pool_mappings = { - mp['instance_id']: mp['pool_id'] - for mp in args.instance_osmorphing_minion_pool_mappings} - - migration = self.app.client_manager.coriolis.migrations.create( - origin_endpoint_id, - destination_endpoint_id, - source_environment, - destination_environment, - args.instances, - network_map=network_map, - notes=args.notes, - storage_mappings=storage_mappings, - skip_os_morphing=args.skip_os_morphing, - replication_count=args.replication_count, - shutdown_instances=args.shutdown_instances, - origin_minion_pool_id=args.origin_minion_pool_id, - destination_minion_pool_id=args.destination_minion_pool_id, - instance_osmorphing_minion_pool_mappings=( - instance_osmorphing_minion_pool_mappings), - user_scripts=user_scripts) - - return MigrationDetailFormatter().get_formatted_entity(migration) - - -class CreateMigrationFromReplica(show.ShowOne): - """Start a new migration from an existing replica""" - def get_parser(self, prog_name): - parser = super(CreateMigrationFromReplica, self).get_parser(prog_name) - parser.add_argument('replica', - help='The ID of the replica to migrate') + parser = super(CreateDeployment, self).get_parser(prog_name) + parser.add_argument('transfer', + help='The ID of the transfer to migrate') parser.add_argument('--force', - help='Force the migration in case of a replica ' + help='Force the deployment in case of a transfer ' 'with failed executions', action='store_true', default=False) parser.add_argument('--dont-clone-disks', - help='Retain the replica disks by cloning them', + help='Retain the transfer disks by cloning them', action='store_false', dest="clone_disks", default=True) parser.add_argument('--skip-os-morphing', @@ -296,7 +200,7 @@ def get_parser(self, prog_name): return parser def take_action(self, args): - m = self.app.client_manager.coriolis.migrations + d = self.app.client_manager.coriolis.deployments user_scripts = cli_utils.compose_user_scripts( args.global_scripts, args.instance_scripts) instance_osmorphing_minion_pool_mappings = None @@ -305,8 +209,8 @@ def take_action(self, args): mp['instance_id']: mp['pool_id'] for mp in args.instance_osmorphing_minion_pool_mappings} - migration = m.create_from_replica( - args.replica, + deployment = d.create_from_transfer( + args.transfer, args.clone_disks, args.force, args.skip_os_morphing, @@ -314,15 +218,15 @@ def take_action(self, args): instance_osmorphing_minion_pool_mappings=( instance_osmorphing_minion_pool_mappings)) - return MigrationDetailFormatter().get_formatted_entity(migration) + return DeploymentDetailFormatter().get_formatted_entity(deployment) -class ShowMigration(show.ShowOne): - """Show a migration""" +class ShowDeployment(show.ShowOne): + """Show a deployment""" def get_parser(self, prog_name): - parser = super(ShowMigration, self).get_parser(prog_name) - parser.add_argument('id', help='The migration\'s id') + parser = super(ShowDeployment, self).get_parser(prog_name) + parser.add_argument('id', help='The deployment\'s id') parser.add_argument('--show-instances-data', action='store_true', help='Includes the instances data used for tasks ' 'execution, this is useful for troubleshooting', @@ -330,17 +234,17 @@ def get_parser(self, prog_name): return parser def take_action(self, args): - migration = self.app.client_manager.coriolis.migrations.get(args.id) - return MigrationDetailFormatter( - args.show_instances_data).get_formatted_entity(migration) + deployment = self.app.client_manager.coriolis.deployments.get(args.id) + return DeploymentDetailFormatter( + args.show_instances_data).get_formatted_entity(deployment) -class CancelMigration(command.Command): - """Cancel a migration""" +class CancelDeployment(command.Command): + """Cancel a deployment""" def get_parser(self, prog_name): - parser = super(CancelMigration, self).get_parser(prog_name) - parser.add_argument('id', help='The migration\'s id') + parser = super(CancelDeployment, self).get_parser(prog_name) + parser.add_argument('id', help='The deployment\'s id') parser.add_argument('--force', help='Perform a forced termination of running ' 'tasks', action='store_true', @@ -348,28 +252,29 @@ def get_parser(self, prog_name): return parser def take_action(self, args): - self.app.client_manager.coriolis.migrations.cancel(args.id, args.force) + self.app.client_manager.coriolis.deployments.cancel( + args.id, args.force) -class DeleteMigration(command.Command): - """Delete a migration""" +class DeleteDeployment(command.Command): + """Delete a deployment""" def get_parser(self, prog_name): - parser = super(DeleteMigration, self).get_parser(prog_name) - parser.add_argument('id', help='The migration\'s id') + parser = super(DeleteDeployment, self).get_parser(prog_name) + parser.add_argument('id', help='The deployment\'s id') return parser def take_action(self, args): - self.app.client_manager.coriolis.migrations.delete(args.id) + self.app.client_manager.coriolis.deployments.delete(args.id) -class ListMigration(lister.Lister): - """List migrations""" +class ListDeployment(lister.Lister): + """List deployments""" def get_parser(self, prog_name): - parser = super(ListMigration, self).get_parser(prog_name) + parser = super(ListDeployment, self).get_parser(prog_name) return parser def take_action(self, args): - obj_list = self.app.client_manager.coriolis.migrations.list() - return MigrationFormatter().list_objects(obj_list) + obj_list = self.app.client_manager.coriolis.deployments.list() + return DeploymentFormatter().list_objects(obj_list) diff --git a/coriolisclient/cli/diagnostics.py b/coriolisclient/cli/diagnostics.py index d17cc2e..02da10b 100644 --- a/coriolisclient/cli/diagnostics.py +++ b/coriolisclient/cli/diagnostics.py @@ -15,27 +15,22 @@ """ Command-line interface sub-commands related to diagnostics. """ -import argparse - -from cliff import command from cliff import lister -from cliff import show - -from oslo_utils import timeutils from coriolisclient.cli import formatter -from coriolisclient import exceptions class DiagnosticsFormatter(formatter.EntityFormatter): - columns = ("Application", "Hostname", "IP addresses", "Packages", "OS info") + columns = ("Application", "Hostname", "IP addresses", "Packages", + "OS info") def _get_sorted_list(self, obj_list): return sorted(obj_list, key=lambda o: o.application) def _get_formatted_data(self, obj): - data = (obj.application, obj.hostname, obj.ip_addresses, obj.packages, obj.os_info) + data = (obj.application, obj.hostname, obj.ip_addresses, obj.packages, + obj.os_info) return data @@ -49,4 +44,3 @@ def get_parser(self, prog_name): def take_action(self, args): diag_details = self.app.client_manager.coriolis.diagnostics.get() return DiagnosticsFormatter().list_objects(diag_details) - diff --git a/coriolisclient/cli/endpoint_destination_minion_pool_options.py b/coriolisclient/cli/endpoint_destination_minion_pool_options.py index e1973be..d744be5 100644 --- a/coriolisclient/cli/endpoint_destination_minion_pool_options.py +++ b/coriolisclient/cli/endpoint_destination_minion_pool_options.py @@ -61,8 +61,8 @@ def take_action(self, args): endpoints = self.app.client_manager.coriolis.endpoints endpoint_id = endpoints.get_endpoint_id_for_name(args.endpoint) - empdo = ( - self.app.client_manager.coriolis.endpoint_destination_minion_pool_options) + empdo = (self.app.client_manager.coriolis. + endpoint_destination_minion_pool_options) obj_list = empdo.list( endpoint_id, environment=environment, option_names=options) return EndpointDestinationMinionPoolOptionsFormatter().list_objects( diff --git a/coriolisclient/cli/endpoint_instances.py b/coriolisclient/cli/endpoint_instances.py index 749a94b..5d2ac5b 100644 --- a/coriolisclient/cli/endpoint_instances.py +++ b/coriolisclient/cli/endpoint_instances.py @@ -103,6 +103,11 @@ def get_parser(self, prog_name): parser.add_argument( '--name', help='Filter results based on regular expression search') + parser.add_argument( + '--refresh', + action='store_true', + default=False, + help='Force refresh of cached instance data') cli_utils.add_args_for_json_option_to_parser(parser, 'environment') @@ -116,7 +121,8 @@ def take_action(self, args): args, 'environment', error_on_no_value=False) obj_list = ei.list( - endpoint_id, env, args.marker, args.limit, args.name) + endpoint_id, env, args.marker, args.limit, args.name, + refresh=args.refresh) return EndpointInstanceFormatter().list_objects(obj_list) diff --git a/coriolisclient/cli/endpoint_networks.py b/coriolisclient/cli/endpoint_networks.py index 750d2bf..03879e0 100644 --- a/coriolisclient/cli/endpoint_networks.py +++ b/coriolisclient/cli/endpoint_networks.py @@ -15,8 +15,6 @@ """ Command-line interface sub-commands related to endpoints. """ -import json - from cliff import lister from coriolisclient.cli import formatter diff --git a/coriolisclient/cli/endpoint_source_minion_pool_options.py b/coriolisclient/cli/endpoint_source_minion_pool_options.py index 9713f5f..da7a83b 100644 --- a/coriolisclient/cli/endpoint_source_minion_pool_options.py +++ b/coriolisclient/cli/endpoint_source_minion_pool_options.py @@ -59,8 +59,9 @@ def take_action(self, args): endpoints = self.app.client_manager.coriolis.endpoints endpoint_id = endpoints.get_endpoint_id_for_name(args.endpoint) - empso = ( - self.app.client_manager.coriolis.endpoint_source_minion_pool_options) + empso = (self.app.client_manager.coriolis. + endpoint_source_minion_pool_options) obj_list = empso.list( endpoint_id, environment=environment, option_names=options) - return EndpointSourceMinionPoolOptionsFormatter().list_objects(obj_list) + return EndpointSourceMinionPoolOptionsFormatter().list_objects( + obj_list) diff --git a/coriolisclient/cli/endpoint_storage.py b/coriolisclient/cli/endpoint_storage.py index 3d71034..05c058f 100644 --- a/coriolisclient/cli/endpoint_storage.py +++ b/coriolisclient/cli/endpoint_storage.py @@ -15,8 +15,6 @@ """ Command-line interface sub-commands related to endpoints. """ -import json - from cliff import lister from coriolisclient.cli import formatter diff --git a/coriolisclient/cli/endpoints.py b/coriolisclient/cli/endpoints.py index 8585b9e..813f543 100644 --- a/coriolisclient/cli/endpoints.py +++ b/coriolisclient/cli/endpoints.py @@ -15,7 +15,6 @@ """ Command-line interface sub-commands related to endpoints. """ - import argparse import json @@ -23,8 +22,8 @@ from cliff import lister from cliff import show -from coriolisclient import exceptions from coriolisclient.cli import formatter +from coriolisclient import exceptions def add_connection_info_args_to_parser(parser): @@ -44,7 +43,7 @@ def add_connection_info_args_to_parser(parser): return parser -def get_connnection_info_from_args(args, raise_if_none=True): +def get_connection_info_from_args(args, raise_if_none=True): """ Returns a dict with the connection info from the arguments. """ conn_info = None raw_conn_info = None @@ -149,7 +148,7 @@ def take_action(self, args): "Please specify either --connection or " "--connection-secret, but not both") - conn_info = get_connnection_info_from_args(args) + conn_info = get_connection_info_from_args(args) endpoint = self.app.client_manager.coriolis.endpoints.create( args.name, args.provider, @@ -192,7 +191,7 @@ def take_action(self, args): "Please specify either --connection or " "--connection-secret, but not both") - conn_info = get_connnection_info_from_args(args, raise_if_none=False) + conn_info = get_connection_info_from_args(args, raise_if_none=False) updated_values = {} if args.name is not None: updated_values["name"] = args.name diff --git a/coriolisclient/cli/licensing_server.py b/coriolisclient/cli/licensing_server.py index efdac9d..d321afd 100644 --- a/coriolisclient/cli/licensing_server.py +++ b/coriolisclient/cli/licensing_server.py @@ -13,7 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from cliff import lister from cliff import show from coriolisclient.cli import formatter diff --git a/coriolisclient/cli/logging.py b/coriolisclient/cli/logging.py index 0ef4208..a4bb216 100644 --- a/coriolisclient/cli/logging.py +++ b/coriolisclient/cli/logging.py @@ -15,16 +15,10 @@ """ Command-line interface sub-commands related to logging. """ -import argparse - from cliff import command from cliff import lister -from cliff import show - -from oslo_utils import timeutils from coriolisclient.cli import formatter -from coriolisclient import exceptions _READIBLE_LOG_LEVELS = [ diff --git a/coriolisclient/cli/services.py b/coriolisclient/cli/services.py index 260aa2a..48a7b58 100644 --- a/coriolisclient/cli/services.py +++ b/coriolisclient/cli/services.py @@ -73,7 +73,7 @@ def get_parser(self, prog_name): help='The messaging topic for the new service.') parser.add_argument('--coriolis-region', action='append', dest='regions', default=[], - help="ID of a region the service should be " + help="ID of a region the service should be " "associated with. Can be supplied multiple times.") _add_service_enablement_args_to_parser(parser) @@ -95,7 +95,7 @@ def get_parser(self, prog_name): parser.add_argument('id', help='The service\'s ID.') parser.add_argument('--coriolis-region', action='append', dest='regions', default=[], - help="ID of a region the service should be " + help="ID of a region the service should be " "associated with. Can be supplied multiple " "times. Update will override all existing " "region associations with the one(s) provided" diff --git a/coriolisclient/cli/shell.py b/coriolisclient/cli/shell.py index 9178924..c30c1ef 100644 --- a/coriolisclient/cli/shell.py +++ b/coriolisclient/cli/shell.py @@ -17,24 +17,25 @@ Command-line interface to the Coriolis API. """ +from collections import namedtuple import logging import os import sys -from collections import namedtuple from cliff import app from cliff import command from cliff import commandmanager from cliff import complete from cliff import help -from keystoneauth1 import loading -from keystoneauth1 import session from keystoneauth1.identity import v2 from keystoneauth1.identity import v3 +from keystoneauth1 import loading +from keystoneauth1 import session import six from coriolisclient import client +from coriolisclient import exceptions from coriolisclient import version @@ -100,7 +101,7 @@ def check_auth_arguments(self, args, api_version=None, raise_exc=False): successful = False if not successful and raise_exc: - raise Exception(msg) + raise exceptions.CoriolisException(msg) return successful @@ -124,7 +125,7 @@ def build_kwargs_based_on_version(self, args, api_version=None): return dict((k, v) for (k, v) in six.iteritems(kwargs) if v) def create_keystone_session( - self, args, api_version, kwargs_dict, auth_type + self, args, api_version, kwargs_dict, auth_type, verify=True, ): # Make sure we have the correct arguments to function self.check_auth_arguments(args, api_version, raise_exc=True) @@ -148,15 +149,16 @@ def create_keystone_session( auth = method(**kwargs) - return session.Session(auth=auth, verify=not args.insecure) + return session.Session(auth=auth, verify=verify) def create_client(self, args): created_client = None endpoint_filter_kwargs = self._get_endpoint_filter_kwargs(args) api_version = args.os_identity_api_version + verify = args.os_cacert or not args.insecure if args.no_auth and args.os_auth_url: - raise Exception( + raise exceptions.CoriolisException( 'ERROR: argument --os-auth-url/-A: not allowed ' 'with argument --no-auth/-N' ) @@ -164,29 +166,31 @@ def create_client(self, args): if args.no_auth: if not all([args.endpoint, args.os_tenant_id or args.os_project_id]): - raise Exception( + raise exceptions.CoriolisException( 'ERROR: please specify --endpoint and ' '--os-project-id (or --os-tenant-id)') created_client = client.Client( endpoint=args.endpoint, project_id=args.os_tenant_id or args.os_project_id, - verify=not args.insecure, + verify=verify, **endpoint_filter_kwargs ) # Token-based authentication elif args.os_auth_token: if not args.os_auth_url: - raise Exception('ERROR: please specify --os-auth-url') + raise exceptions.CoriolisException( + 'ERROR: please specify --os-auth-url') token_kwargs = { 'auth_url': args.os_auth_url, 'token': args.os_auth_token } session = self.create_keystone_session( - args, api_version, token_kwargs, auth_type='token' - ) + args, api_version, token_kwargs, auth_type='token', + verify=verify) created_client = client.Client( session=session, endpoint=args.endpoint, + verify=verify, **endpoint_filter_kwargs ) @@ -199,15 +203,17 @@ def create_client(self, args): 'username': args.os_username, } session = self.create_keystone_session( - args, api_version, password_kwargs, auth_type='password' - ) + args, api_version, password_kwargs, auth_type='password', + verify=verify) created_client = client.Client( session=session, endpoint=args.endpoint, + verify=verify, **endpoint_filter_kwargs ) else: - raise Exception('ERROR: please specify authentication credentials') + raise exceptions.CoriolisException( + 'ERROR: please specify authentication credentials') return created_client @@ -348,6 +354,7 @@ def _setup_logging(): logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("keystoneclient").setLevel(logging.ERROR) + def main(argv=sys.argv[1:]): _setup_logging() coriolis_app = Coriolis() diff --git a/coriolisclient/cli/replica_executions.py b/coriolisclient/cli/transfer_executions.py similarity index 57% rename from coriolisclient/cli/replica_executions.py rename to coriolisclient/cli/transfer_executions.py index f102284..0b1e199 100644 --- a/coriolisclient/cli/replica_executions.py +++ b/coriolisclient/cli/transfer_executions.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Command-line interface sub-commands related to replicas. +Command-line interface sub-commands related to transfers. """ import os @@ -24,9 +24,9 @@ from coriolisclient.cli import formatter -class ReplicaExecutionFormatter(formatter.EntityFormatter): +class TransferExecutionFormatter(formatter.EntityFormatter): - columns = ("Replica ID", + columns = ("Transfer ID", "ID", "Status", "Created", @@ -44,10 +44,10 @@ def _get_formatted_data(self, obj): return data -class ReplicaExecutionDetailFormatter(formatter.EntityFormatter): +class TransferExecutionDetailFormatter(formatter.EntityFormatter): columns = ("id", - "replica_id", + "transfer_id", "status", "created", "last_updated", @@ -100,49 +100,55 @@ def _get_formatted_data(self, obj): return data -class CreateReplicaExecution(show.ShowOne): - """Start a replica execution""" +class CreateTransferExecution(show.ShowOne): + """Start a transfer execution""" def get_parser(self, prog_name): - parser = super(CreateReplicaExecution, self).get_parser(prog_name) - parser.add_argument('replica', - help='The ID of the replica to execute') + parser = super(CreateTransferExecution, self).get_parser(prog_name) + parser.add_argument('transfer', + help='The ID of the transfer to execute') parser.add_argument('--shutdown-instances', help='Shutdown instances before executing the ' - 'replica', action='store_true', + 'transfer', action='store_true', + default=False) + parser.add_argument('--auto-deploy', + help="Automatically execute deployment after the " + "transfer execution finishes", + action='store_true', default=False) return parser def take_action(self, args): - execution = self.app.client_manager.coriolis.replica_executions.create( - args.replica, args.shutdown_instances) + execution = ( + self.app.client_manager.coriolis.transfer_executions.create( + args.transfer, args.shutdown_instances, args.auto_deploy)) - return ReplicaExecutionDetailFormatter().get_formatted_entity( + return TransferExecutionDetailFormatter().get_formatted_entity( execution) -class ShowReplicaExecution(show.ShowOne): - """Show a replica execution""" +class ShowTransferExecution(show.ShowOne): + """Show a transfer execution""" def get_parser(self, prog_name): - parser = super(ShowReplicaExecution, self).get_parser(prog_name) - parser.add_argument('replica', help='The replica\'s id') - parser.add_argument('id', help='The replica execution\'s id') + parser = super(ShowTransferExecution, self).get_parser(prog_name) + parser.add_argument('transfer', help='The transfer\'s id') + parser.add_argument('id', help='The transfer execution\'s id') return parser def take_action(self, args): - execution = self.app.client_manager.coriolis.replica_executions.get( - args.replica, args.id) - return ReplicaExecutionDetailFormatter().get_formatted_entity( + execution = self.app.client_manager.coriolis.transfer_executions.get( + args.transfer, args.id) + return TransferExecutionDetailFormatter().get_formatted_entity( execution) -class CancelReplicaExecution(command.Command): - """Cancel a replica execution""" +class CancelTransferExecution(command.Command): + """Cancel a transfer execution""" def get_parser(self, prog_name): - parser = super(CancelReplicaExecution, self).get_parser(prog_name) - parser.add_argument('replica', help='The replica\'s id') - parser.add_argument('id', help='The replica execution\'s id') + parser = super(CancelTransferExecution, self).get_parser(prog_name) + parser.add_argument('transfer', help='The transfer\'s id') + parser.add_argument('id', help='The transfer execution\'s id') parser.add_argument('--force', help='Perform a forced termination of running ' 'tasks', action='store_true', @@ -150,33 +156,33 @@ def get_parser(self, prog_name): return parser def take_action(self, args): - self.app.client_manager.coriolis.replica_executions.cancel( - args.replica, args.id, args.force) + self.app.client_manager.coriolis.transfer_executions.cancel( + args.transfer, args.id, args.force) -class DeleteReplicaExecution(command.Command): - """Delete a replica execution""" +class DeleteTransferExecution(command.Command): + """Delete a transfer execution""" def get_parser(self, prog_name): - parser = super(DeleteReplicaExecution, self).get_parser(prog_name) - parser.add_argument('replica', help='The replica\'s id') - parser.add_argument('id', help='The replica execution\'s id') + parser = super(DeleteTransferExecution, self).get_parser(prog_name) + parser.add_argument('transfer', help='The transfer\'s id') + parser.add_argument('id', help='The transfer execution\'s id') return parser def take_action(self, args): - self.app.client_manager.coriolis.replica_executions.delete( - args.replica, args.id) + self.app.client_manager.coriolis.transfer_executions.delete( + args.transfer, args.id) -class ListReplicaExecution(lister.Lister): - """List replica executions""" +class ListTransferExecution(lister.Lister): + """List transfer executions""" def get_parser(self, prog_name): - parser = super(ListReplicaExecution, self).get_parser(prog_name) - parser.add_argument('replica', help='The replica\'s id') + parser = super(ListTransferExecution, self).get_parser(prog_name) + parser.add_argument('transfer', help='The transfer\'s id') return parser def take_action(self, args): - obj_list = self.app.client_manager.coriolis.replica_executions.list( - args.replica) - return ReplicaExecutionFormatter().list_objects(obj_list) + obj_list = self.app.client_manager.coriolis.transfer_executions.list( + args.transfer) + return TransferExecutionFormatter().list_objects(obj_list) diff --git a/coriolisclient/cli/replica_schedules.py b/coriolisclient/cli/transfer_schedules.py similarity index 68% rename from coriolisclient/cli/replica_schedules.py rename to coriolisclient/cli/transfer_schedules.py index 7287071..c4d758a 100644 --- a/coriolisclient/cli/replica_schedules.py +++ b/coriolisclient/cli/transfer_schedules.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Command-line interface sub-commands related to replicas. +Command-line interface sub-commands related to transfers. """ import argparse @@ -42,9 +42,9 @@ def __call__(self, parser, namespace, value, option_string=None): setattr(namespace, self.dest, value) -class ReplicaScheduleFormatter(formatter.EntityFormatter): +class TransferScheduleFormatter(formatter.EntityFormatter): - columns = ("Replica ID", + columns = ("Transfer ID", "ID", "Schedule", "Created", @@ -55,7 +55,7 @@ def _get_sorted_list(self, obj_list): return sorted(obj_list, key=lambda o: o.created_at) def _get_formatted_data(self, obj): - data = (obj.replica_id, + data = (obj.transfer_id, obj.id, obj.schedule, obj.created_at, @@ -63,35 +63,37 @@ def _get_formatted_data(self, obj): return data -class ReplicaScheduleDetailFormatter(formatter.EntityFormatter): +class TransferScheduleDetailFormatter(formatter.EntityFormatter): columns = ("id", - "replica_id", + "transfer_id", "schedule", "created", "last_updated", "enabled", "expires", - "shutdown_instance") + "shutdown_instance", + "auto_deploy") def _get_formatted_data(self, obj): data = (obj.id, - obj.replica_id, + obj.transfer_id, obj.schedule, obj.created_at, obj.updated_at, obj.enabled, obj.expiration_date, - obj.shutdown_instance) + obj.shutdown_instance, + obj.auto_deploy) return data -class CreateReplicaSchedule(show.ShowOne): - """Start a replica schedule""" +class CreateTransferSchedule(show.ShowOne): + """Start a transfer schedule""" def get_parser(self, prog_name): - parser = super(CreateReplicaSchedule, self).get_parser(prog_name) - parser.add_argument('replica', - help='The ID of the replica') + parser = super(CreateTransferSchedule, self).get_parser(prog_name) + parser.add_argument('transfer', + help='The ID of the transfer') _add_schedule_group(parser) parser.add_argument('--expires-at', help='ISO8601 formatted date', @@ -104,6 +106,11 @@ def get_parser(self, prog_name): help='Shutdown instance', action='store_true', default=False) + parser.add_argument('--auto-deploy', + help="Auto Deploy transfer after scheduled " + "execution completes.", + action="store_true", + default=False) return parser def take_action(self, args): @@ -113,35 +120,36 @@ def take_action(self, args): "Please provide at least one value in the Schedule group") exp = _parse_expiration_date(args.expires_at) - schedule = self.app.client_manager.coriolis.replica_schedules.create( - args.replica, parsed_schedule, - args.disabled is False, exp, args.shutdown_instance) - return ReplicaScheduleDetailFormatter().get_formatted_entity( + schedule = self.app.client_manager.coriolis.transfer_schedules.create( + args.transfer, parsed_schedule, + args.disabled is False, exp, args.shutdown_instance, + args.auto_deploy) + return TransferScheduleDetailFormatter().get_formatted_entity( schedule) -class ShowReplicaSchedule(show.ShowOne): - """Show a replica schedule""" +class ShowTransferSchedule(show.ShowOne): + """Show a transfer schedule""" def get_parser(self, prog_name): - parser = super(ShowReplicaSchedule, self).get_parser(prog_name) - parser.add_argument('replica', help='The replica\'s id') - parser.add_argument('id', help='The replica schedule\'s id') + parser = super(ShowTransferSchedule, self).get_parser(prog_name) + parser.add_argument('transfer', help='The transfer\'s id') + parser.add_argument('id', help='The transfer schedule\'s id') return parser def take_action(self, args): - schedule = self.app.client_manager.coriolis.replica_schedules.get( - args.replica, args.id) - return ReplicaScheduleDetailFormatter().get_formatted_entity( + schedule = self.app.client_manager.coriolis.transfer_schedules.get( + args.transfer, args.id) + return TransferScheduleDetailFormatter().get_formatted_entity( schedule) -class UpdateReplicaSchedule(show.ShowOne): - """Updates a replica schedule""" +class UpdateTransferSchedule(show.ShowOne): + """Updates a transfer schedule""" def get_parser(self, prog_name): - parser = super(UpdateReplicaSchedule, self).get_parser(prog_name) - parser.add_argument('replica', help='The replica\'s id') - parser.add_argument('id', help='The replica schedule\'s id') + parser = super(UpdateTransferSchedule, self).get_parser(prog_name) + parser.add_argument('transfer', help='The transfer\'s id') + parser.add_argument('id', help='The transfer schedule\'s id') _add_schedule_group(parser) expires_parser = parser.add_mutually_exclusive_group(required=False) expires_parser.add_argument( @@ -179,6 +187,18 @@ def get_parser(self, prog_name): help="Don't shutdown instance", dest="shutdown", action='store_false') + auto_deploy_parser = parser.add_mutually_exclusive_group( + required=False) + auto_deploy_parser.add_argument( + "--auto-deploy", + help="Auto Deploy transfer after scheduled execution completes.", + dest="auto_deploy", + action="store_true") + auto_deploy_parser.add_argument( + "--dont-auto-deploy", + help="Stops auto deployment when starting scheduled execution", + dest="auto_deploy", + action="store_false") return parser def take_action(self, args): @@ -198,34 +218,36 @@ def take_action(self, args): updated_values["shutdown_instance"] = args.shutdown if args.enabled is not None: updated_values["enabled"] = args.enabled + if args.auto_deploy is not None: + updated_values['auto_deploy'] = args.auto_deploy - schedule = self.app.client_manager.coriolis.replica_schedules.update( - args.replica, args.id, updated_values) + schedule = self.app.client_manager.coriolis.transfer_schedules.update( + args.transfer, args.id, updated_values) - return ReplicaScheduleDetailFormatter().get_formatted_entity( + return TransferScheduleDetailFormatter().get_formatted_entity( schedule) -class DeleteReplicaSchedule(command.Command): - """Delete a replica schedule""" +class DeleteTransferSchedule(command.Command): + """Delete a transfer schedule""" def get_parser(self, prog_name): - parser = super(DeleteReplicaSchedule, self).get_parser(prog_name) - parser.add_argument('replica', help='The replica\'s id') - parser.add_argument('id', help='The replica schedule\'s id') + parser = super(DeleteTransferSchedule, self).get_parser(prog_name) + parser.add_argument('transfer', help='The transfer\'s id') + parser.add_argument('id', help='The transfer schedule\'s id') return parser def take_action(self, args): - self.app.client_manager.coriolis.replica_schedules.delete( - args.replica, args.id) + self.app.client_manager.coriolis.transfer_schedules.delete( + args.transfer, args.id) -class ListReplicaSchedule(lister.Lister): - """List replica schedules""" +class ListTransferSchedule(lister.Lister): + """List transfer schedules""" def get_parser(self, prog_name): - parser = super(ListReplicaSchedule, self).get_parser(prog_name) - parser.add_argument('replica', help='The replica\'s id') + parser = super(ListTransferSchedule, self).get_parser(prog_name) + parser.add_argument('transfer', help='The transfer\'s id') parser.add_argument('--hide-expired', help='Hide expired schedules', action='store_true', @@ -233,9 +255,9 @@ def get_parser(self, prog_name): return parser def take_action(self, args): - obj_list = self.app.client_manager.coriolis.replica_schedules.list( - args.replica, hide_expired=args.hide_expired) - return ReplicaScheduleFormatter().list_objects(obj_list) + obj_list = self.app.client_manager.coriolis.transfer_schedules.list( + args.transfer, hide_expired=args.hide_expired) + return TransferScheduleFormatter().list_objects(obj_list) def _add_schedule_group(parser): diff --git a/coriolisclient/cli/replicas.py b/coriolisclient/cli/transfers.py similarity index 70% rename from coriolisclient/cli/replicas.py rename to coriolisclient/cli/transfers.py index b4ab7be..37e1bbb 100644 --- a/coriolisclient/cli/replicas.py +++ b/coriolisclient/cli/transfers.py @@ -14,7 +14,7 @@ # limitations under the License. """ -Command-line interface sub-commands related to replicas. +Command-line interface sub-commands related to transfers. """ import os @@ -24,13 +24,43 @@ from cliff import show from coriolisclient.cli import formatter -from coriolisclient.cli import replica_executions +from coriolisclient.cli import transfer_executions from coriolisclient.cli import utils as cli_utils - -class ReplicaFormatter(formatter.EntityFormatter): +TRANSFER_SCENARIO_REPLICA = "replica" +TRANSFER_SCENARIO_LIVE_MIGRATION = "live_migration" + + +def _add_default_deployment_args_to_parser(parser): + cd_group = parser.add_mutually_exclusive_group() + cd_group.add_argument('--clone-disks', + help='Retain the transfer disks by cloning them ' + 'when launching deployment', + action='store_true', dest="clone_disks", + default=None) + cd_group.add_argument('--dont-clone-disks', + help="Deploy directly on transfer disks, without " + "cloning them.", + action="store_false", dest="clone_disks", + default=None) + + osm_group = parser.add_mutually_exclusive_group() + osm_group.add_argument('--os-morphing', + help="Include the OSMorphing process on the " + "deployments of this transfer.", + action="store_false", dest="skip_os_morphing", + default=None) + osm_group.add_argument('--skip-os-morphing', + help='Skip the OS morphing process on the ' + 'deployments of this transfer', + action='store_true', default=None, + dest="skip_os_morphing") + + +class TransferFormatter(formatter.EntityFormatter): columns = ("ID", + "Scenario", "Instances", "Notes", "Last Execution Status", @@ -48,6 +78,7 @@ def _format_last_execution(self, obj): def _get_formatted_data(self, obj): data = (obj.id, + getattr(obj, "scenario", "replica"), "\n".join(obj.instances), obj.notes, obj.last_execution_status, @@ -56,13 +87,14 @@ def _get_formatted_data(self, obj): return data -class ReplicaDetailFormatter(formatter.EntityFormatter): +class TransferDetailFormatter(formatter.EntityFormatter): def __init__(self, show_instances_data=False): self.columns = [ "id", "created", "last_updated", + "scenario_type", "reservation_id", "instances", "notes", @@ -78,6 +110,8 @@ def __init__(self, show_instances_data=False): "storage_backend_mappings", "default_storage_backend", "user_scripts", + "clone_disks", + "skip_os_morphing", "executions", ] @@ -102,6 +136,7 @@ def _get_formatted_data(self, obj): data = [obj.id, obj.created_at, obj.updated_at, + getattr(obj, "scenario", ""), obj.reservation_id, self._format_instances(obj), obj.notes, @@ -121,6 +156,8 @@ def _get_formatted_data(self, obj): cli_utils.format_mapping(backend_mappings), default_storage, cli_utils.format_json_for_object_property(obj, 'user_scripts'), + obj.clone_disks, + obj.skip_os_morphing, self._format_executions(obj.executions)] if "instances-data" in self.columns: @@ -129,10 +166,10 @@ def _get_formatted_data(self, obj): return data -class CreateReplica(show.ShowOne): - """Create a new replica""" +class CreateTransfer(show.ShowOne): + """Create a new transfer""" def get_parser(self, prog_name): - parser = super(CreateReplica, self).get_parser(prog_name) + parser = super(CreateTransfer, self).get_parser(prog_name) parser.add_argument('--origin-endpoint', required=True, help='The origin endpoint id') parser.add_argument('--destination-endpoint', required=True, @@ -140,9 +177,24 @@ def get_parser(self, prog_name): parser.add_argument('--instance', action='append', required=True, dest="instances", metavar="INSTANCE_IDENTIFIER", help='The identifier of a source instance to be ' - 'replicated. Can be specified multiple times') + 'transferred. Can be specified multiple ' + 'times') + parser.add_argument('--scenario', + dest="scenario", metavar="SCENARIO", + choices=[ + TRANSFER_SCENARIO_REPLICA, + TRANSFER_SCENARIO_LIVE_MIGRATION], + default=TRANSFER_SCENARIO_REPLICA, + help='The type of scenario to use when creating ' + 'the Transfer. "replica" will create a ' + 'monthly-billed Replica which can be ' + 'executed and deployed as many times as ' + 'desired, while "live_migration" will ' + 'create a Transfer which can be synced ' + 'as many times as needed but only ' + 'deployed once.') parser.add_argument('--notes', dest='notes', - help='Notes about the replica') + help='Notes about the transfer') parser.add_argument('--user-script-global', action='append', required=False, dest="global_scripts", @@ -172,6 +224,7 @@ def get_parser(self, prog_name): include_osmorphing_pool_mappings_arg=True) cli_utils.add_storage_mappings_arguments_to_parser(parser) + _add_default_deployment_args_to_parser(parser) return parser @@ -196,12 +249,13 @@ def take_action(self, args): user_scripts = cli_utils.compose_user_scripts( args.global_scripts, args.instance_scripts) - replica = self.app.client_manager.coriolis.replicas.create( + transfer = self.app.client_manager.coriolis.transfers.create( origin_endpoint_id, destination_endpoint_id, source_environment, destination_environment, args.instances, + args.scenario, network_map=network_map, notes=args.notes, storage_mappings=storage_mappings, @@ -209,17 +263,19 @@ def take_action(self, args): destination_minion_pool_id=args.destination_minion_pool_id, instance_osmorphing_minion_pool_mappings=( instance_osmorphing_minion_pool_mappings), - user_scripts=user_scripts) + user_scripts=user_scripts, + clone_disks=args.clone_disks, + skip_os_morphing=args.skip_os_morphing) - return ReplicaDetailFormatter().get_formatted_entity(replica) + return TransferDetailFormatter().get_formatted_entity(transfer) -class ShowReplica(show.ShowOne): - """Show a replica""" +class ShowTransfer(show.ShowOne): + """Show a transfer""" def get_parser(self, prog_name): - parser = super(ShowReplica, self).get_parser(prog_name) - parser.add_argument('id', help='The replica\'s id') + parser = super(ShowTransfer, self).get_parser(prog_name) + parser.add_argument('id', help='The transfer\'s id') parser.add_argument('--show-instances-data', action='store_true', help='Includes the instances data used for tasks ' 'execution, this is useful for troubleshooting', @@ -227,57 +283,57 @@ def get_parser(self, prog_name): return parser def take_action(self, args): - replica = self.app.client_manager.coriolis.replicas.get(args.id) - return ReplicaDetailFormatter( - args.show_instances_data).get_formatted_entity(replica) + transfer = self.app.client_manager.coriolis.transfers.get(args.id) + return TransferDetailFormatter( + args.show_instances_data).get_formatted_entity(transfer) -class DeleteReplica(command.Command): - """Delete a replica""" +class DeleteTransfer(command.Command): + """Delete a transfer""" def get_parser(self, prog_name): - parser = super(DeleteReplica, self).get_parser(prog_name) - parser.add_argument('id', help='The replica\'s id') + parser = super(DeleteTransfer, self).get_parser(prog_name) + parser.add_argument('id', help='The transfer\'s id') return parser def take_action(self, args): - self.app.client_manager.coriolis.replicas.delete(args.id) + self.app.client_manager.coriolis.transfers.delete(args.id) -class DeleteReplicaDisks(show.ShowOne): - """Delete replica target disks""" +class DeleteTransferDisks(show.ShowOne): + """Delete transfer target disks""" def get_parser(self, prog_name): - parser = super(DeleteReplicaDisks, self).get_parser(prog_name) - parser.add_argument('id', help='The replica\'s id') + parser = super(DeleteTransferDisks, self).get_parser(prog_name) + parser.add_argument('id', help='The transfer\'s id') return parser def take_action(self, args): - execution = self.app.client_manager.coriolis.replicas.delete_disks( + execution = self.app.client_manager.coriolis.transfers.delete_disks( args.id) - return replica_executions.ReplicaExecutionDetailFormatter( + return transfer_executions.TransferExecutionDetailFormatter( ).get_formatted_entity(execution) -class ListReplica(lister.Lister): - """List replicas""" +class ListTransfer(lister.Lister): + """List transfers""" def get_parser(self, prog_name): - parser = super(ListReplica, self).get_parser(prog_name) + parser = super(ListTransfer, self).get_parser(prog_name) return parser def take_action(self, args): - obj_list = self.app.client_manager.coriolis.replicas.list() - return ReplicaFormatter().list_objects(obj_list) + obj_list = self.app.client_manager.coriolis.transfers.list() + return TransferFormatter().list_objects(obj_list) -class UpdateReplica(show.ShowOne): - """Create a new replica""" +class UpdateTransfer(show.ShowOne): + """Create a new transfer""" def get_parser(self, prog_name): - parser = super(UpdateReplica, self).get_parser(prog_name) - parser.add_argument('id', help='The replica\'s id') + parser = super(UpdateTransfer, self).get_parser(prog_name) + parser.add_argument('id', help='The transfer\'s id') parser.add_argument('--notes', dest='notes', - help='Notes about the replica.') + help='Notes about the transfer.') parser.add_argument('--user-script-global', action='append', required=False, dest="global_scripts", @@ -306,6 +362,7 @@ def get_parser(self, prog_name): parser, include_origin_pool_arg=True, include_destination_pool_arg=True, include_osmorphing_pool_mappings_arg=True) + _add_default_deployment_args_to_parser(parser) return parser @@ -348,14 +405,18 @@ def take_action(self, args): instance_osmorphing_minion_pool_mappings) if user_scripts: updated_properties['user_scripts'] = user_scripts + if args.clone_disks is not None: + updated_properties['clone_disks'] = args.clone_disks + if args.skip_os_morphing is not None: + updated_properties['skip_os_morphing'] = args.skip_os_morphing if not updated_properties: raise ValueError( "No options provided for update. Please run `coriolis help " - "replica update` for details on accepted parameters.") + "transfer update` for details on accepted parameters.") - execution = self.app.client_manager.coriolis.replicas.update( + execution = self.app.client_manager.coriolis.transfers.update( args.id, updated_properties) - return replica_executions.ReplicaExecutionDetailFormatter( + return transfer_executions.TransferExecutionDetailFormatter( ).get_formatted_entity(execution) diff --git a/coriolisclient/cli/utils.py b/coriolisclient/cli/utils.py index b61ee28..730363f 100644 --- a/coriolisclient/cli/utils.py +++ b/coriolisclient/cli/utils.py @@ -175,7 +175,7 @@ def get_option_value_from_args(args, option_name, error_on_no_value=True): with file_arg as fin: raw_value = fin.read() - if not value and raw_value: + if raw_value: try: value = json.loads(raw_value) except ValueError as ex: diff --git a/coriolisclient/client.py b/coriolisclient/client.py index a45104c..60ef840 100644 --- a/coriolisclient/client.py +++ b/coriolisclient/client.py @@ -17,6 +17,7 @@ from keystoneauth1 import adapter +from coriolisclient.v1 import deployments from coriolisclient.v1 import diagnostics from coriolisclient.v1 import endpoint_destination_minion_pool_options from coriolisclient.v1 import endpoint_destination_options @@ -31,14 +32,13 @@ from coriolisclient.v1 import licensing_reservations from coriolisclient.v1 import licensing_server from coriolisclient.v1 import logging as coriolis_logging -from coriolisclient.v1 import migrations from coriolisclient.v1 import minion_pools from coriolisclient.v1 import providers from coriolisclient.v1 import regions -from coriolisclient.v1 import replica_executions -from coriolisclient.v1 import replica_schedules -from coriolisclient.v1 import replicas from coriolisclient.v1 import services +from coriolisclient.v1 import transfer_executions +from coriolisclient.v1 import transfer_schedules +from coriolisclient.v1 import transfers LOG = logging.getLogger(__name__) @@ -49,11 +49,12 @@ class _HTTPClient(adapter.Adapter): - def __init__(self, session, project_id=None, **kwargs): + def __init__(self, session, project_id=None, verify=True, **kwargs): kwargs.setdefault('interface', _DEFAULT_SERVICE_INTERFACE) kwargs.setdefault('service_type', _DEFAULT_SERVICE_TYPE) kwargs.setdefault('version', _DEFAULT_API_VERSION) endpoint = kwargs.pop('endpoint', None) + self.verify = verify super(_HTTPClient, self).__init__(session, **kwargs) @@ -74,23 +75,23 @@ def __init__(self, session=None, *args, **kwargs): endpoint_destination_options.EndpointDestinationOptionsManager( httpclient)) self.endpoint_source_minion_pool_options = ( - endpoint_source_minion_pool_options.EndpointSourceMinionPoolOptionsManager( - httpclient)) + endpoint_source_minion_pool_options. + EndpointSourceMinionPoolOptionsManager(httpclient)) self.endpoint_destination_minion_pool_options = ( - endpoint_destination_minion_pool_options.EndpointDestinationMinionPoolOptionsManager( - httpclient)) + endpoint_destination_minion_pool_options. + EndpointDestinationMinionPoolOptionsManager(httpclient)) self.endpoint_source_options = ( endpoint_source_options.EndpointSourceOptionsManager(httpclient)) self.endpoint_storage = endpoint_storage.EndpointStorageManager( httpclient) - self.migrations = migrations.MigrationManager(httpclient) + self.deployments = deployments.DeploymentManager(httpclient) self.minion_pools = minion_pools.MinionPoolManager(httpclient) self.providers = providers.ProvidersManager(httpclient) - self.replicas = replicas.ReplicaManager(httpclient) - self.replica_schedules = replica_schedules.ReplicaScheduleManager( - httpclient) - self.replica_executions = replica_executions.ReplicaExecutionManager( + self.transfers = transfers.TransferManager(httpclient) + self.transfer_schedules = transfer_schedules.TransferScheduleManager( httpclient) + self.transfer_executions = ( + transfer_executions.TransferExecutionManager(httpclient)) self.regions = regions.RegionManager(httpclient) self.services = services.ServiceManager(httpclient) self.logging = coriolis_logging.CoriolisLogDownloadManager(httpclient) diff --git a/coriolisclient/constants.py b/coriolisclient/constants.py index 18f98cd..8ba47d2 100644 --- a/coriolisclient/constants.py +++ b/coriolisclient/constants.py @@ -47,4 +47,4 @@ OS_TYPE_WINDOWS, OS_TYPE_OTHER, OS_TYPE_UNKNOWN, -] \ No newline at end of file +] diff --git a/coriolisclient/tests/__init__.py b/coriolisclient/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coriolisclient/tests/cli/__init__.py b/coriolisclient/tests/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coriolisclient/tests/cli/data/shell_check_auth_arguments.yml b/coriolisclient/tests/cli/data/shell_check_auth_arguments.yml new file mode 100644 index 0000000..79118cf --- /dev/null +++ b/coriolisclient/tests/cli/data/shell_check_auth_arguments.yml @@ -0,0 +1,129 @@ + +- config: + args: + os_project_id: "mock_os_project_id" + os_project_name: "mock_os_project_name" + os_project_domain_name: "mock_os_project_domain_name" + os_project_domain_id: "mock_os_project_domain_id" + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + api_version: '3' + raise_exc: False + expected_result: True + +- config: + args: + os_project_id: "mock_os_project_id" + os_project_name: "mock_os_project_name" + os_project_domain_name: "mock_os_project_domain_name" + os_project_domain_id: "mock_os_project_domain_id" + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + api_version: + raise_exc: False + expected_result: True + +- config: + args: + api_version: '3' + raise_exc: False + expected_result: False + +- config: + args: + os_project_id: "mock_os_project_id" + api_version: '3' + raise_exc: False + expected_result: True + +- config: + args: + os_project_name: "mock_os_project_name" + api_version: '3' + raise_exc: False + expected_result: False + +- config: + args: + os_project_name: "mock_os_project_name" + os_project_domain_name: "mock_os_project_domain_name" + api_version: '3' + raise_exc: False + expected_result: True + + +- config: + args: + os_tenant_id: "mock_os_tenant_id" + api_version: '3' + raise_exc: False + expected_result: False + +- config: + args: + os_project_name: "mock_os_project_name" + os_project_domain_name: "mock_os_project_domain_name" + api_version: + raise_exc: False + expected_result: True + +- config: + args: + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + api_version: + raise_exc: False + expected_result: False + +- config: + args: + api_version: + raise_exc: False + expected_result: False + +- config: + args: + api_version: + raise_exc: True + expected_result: + +- config: + args: + os_project_id: "mock_os_project_id" + os_project_name: "mock_os_project_name" + os_project_domain_name: "mock_os_project_domain_name" + os_project_domain_id: "mock_os_project_domain_id" + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + api_version: '2' + raise_exc: False + expected_result: True + +- config: + args: + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + api_version: '2' + raise_exc: False + expected_result: True + +- config: + args: + api_version: '2' + raise_exc: False + expected_result: False + +- config: + args: + os_tenant_id: "mock_os_tenant_id" + api_version: '2' + raise_exc: False + expected_result: True + +- config: + args: + os_project_name: "mock_os_project_name" + os_project_domain_name: "mock_os_project_domain_name" + api_version: '2' + raise_exc: False + expected_result: False diff --git a/coriolisclient/tests/cli/data/shell_create_keystone_auth.yml b/coriolisclient/tests/cli/data/shell_create_keystone_auth.yml new file mode 100644 index 0000000..b5503cf --- /dev/null +++ b/coriolisclient/tests/cli/data/shell_create_keystone_auth.yml @@ -0,0 +1,137 @@ + +- config: + args: + os_project_id: "mock_os_project_id" + os_project_name: "mock_os_project_name" + os_project_domain_name: "mock_os_project_domain_name" + os_project_domain_id: "mock_os_project_domain_id" + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + insecure: True + kwargs: + auth_url: "mock_auth_url" + token: "mock_token" + expected_kwargs: + auth_url: "mock_auth_url" + token: "mock_token" + project_id: "mock_os_project_id" + project_name: "mock_os_project_name" + project_domain_name: "mock_os_project_domain_name" + project_domain_id: "mock_os_project_domain_id" + verify: False + api_version: '3' + auth_type: 'token' + auth_fun: 'keystoneauth1.identity.v3.Token' + +- config: + args: + os_project_id: "mock_os_project_id" + os_tenant_id: "mock_os_tenant_id" + kwargs: + auth_url: "mock_auth_url" + token: "mock_token" + expected_kwargs: + auth_url: "mock_auth_url" + token: "mock_token" + project_id: "mock_os_project_id" + api_version: '3' + auth_type: 'token' + auth_fun: 'keystoneauth1.identity.v3.Token' + +- config: + args: + os_project_id: "mock_os_project_id" + os_tenant_id: "mock_os_tenant_id" + kwargs: + auth_url: "mock_auth_url" + password: "mock_password" + expected_kwargs: + auth_url: "mock_auth_url" + project_id: "mock_os_project_id" + password: "mock_password" + api_version: '3' + auth_type: 'other' + auth_fun: 'keystoneauth1.identity.v3.Password' + + +- config: + args: + os_project_id: "mock_os_project_id" + os_tenant_id: "mock_os_tenant_id" + kwargs: + auth_url: "mock_auth_url" + password: "mock_password" + expected_kwargs: + auth_url: "mock_auth_url" + project_id: "mock_os_project_id" + password: "mock_password" + api_version: '3' + auth_type: + auth_fun: 'keystoneauth1.identity.v3.Password' + +- config: + args: + os_project_id: "mock_os_project_id" + os_project_name: "mock_os_project_name" + os_project_domain_name: "mock_os_project_domain_name" + os_project_domain_id: "mock_os_project_domain_id" + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + kwargs: + auth_url: "mock_auth_url" + token: "mock_token" + expected_kwargs: + auth_url: "mock_auth_url" + token: "mock_token" + tenant_id: "mock_os_tenant_id" + tenant_name: "mock_os_tenant_name" + api_version: '2' + auth_type: 'token' + auth_fun: 'keystoneauth1.identity.v2.Token' + + +- config: + args: + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + kwargs: + auth_url: "mock_auth_url" + token: "mock_token" + expected_kwargs: + auth_url: "mock_auth_url" + token: "mock_token" + tenant_id: "mock_os_tenant_id" + tenant_name: "mock_os_tenant_name" + api_version: '2' + auth_type: 'token' + auth_fun: 'keystoneauth1.identity.v2.Token' + + +- config: + args: + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + kwargs: + auth_url: "mock_auth_url" + expected_kwargs: + auth_url: "mock_auth_url" + tenant_id: "mock_os_tenant_id" + tenant_name: "mock_os_tenant_name" + api_version: '2' + auth_type: 'other' + auth_fun: 'keystoneauth1.identity.v2.Password' + + +- config: + args: + os_tenant_id: "mock_os_tenant_id" + os_tenant_name: "mock_os_tenant_name" + kwargs: + auth_url: "mock_auth_url" + expected_kwargs: + auth_url: "mock_auth_url" + tenant_id: "mock_os_tenant_id" + tenant_name: "mock_os_tenant_name" + api_version: '2' + auth_type: + auth_fun: 'keystoneauth1.identity.v2.Password' diff --git a/coriolisclient/tests/cli/data/user_scripts.yml b/coriolisclient/tests/cli/data/user_scripts.yml new file mode 100644 index 0000000..7510465 --- /dev/null +++ b/coriolisclient/tests/cli/data/user_scripts.yml @@ -0,0 +1,2 @@ +"mock_script1" +"mock_script2" diff --git a/coriolisclient/tests/cli/test_deployments.py b/coriolisclient/tests/cli/test_deployments.py new file mode 100644 index 0000000..f00065f --- /dev/null +++ b/coriolisclient/tests/cli/test_deployments.py @@ -0,0 +1,340 @@ +# Copyright (c) 2024 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import copy +import os +from unittest import mock + +from coriolisclient.cli import deployments +from coriolisclient.tests import test_base +from coriolisclient.v1 import common +from coriolisclient.v1 import deployments as v1_deployments + +DEPLOYMENT_ID = "depl_id" +DEPLOYMENT_DATA = { + "storage_mappings": None, + "id": DEPLOYMENT_ID, + "last_execution_status": "COMPLETED", + "created_at": None, + "updated_at": None, + "transfer_id": "transfer_1", + "transfer_scenario_type": "replica", + "reservation_id": "res1", + "instances": ["instance1"], + "notes": "", + "origin_endpoint_id": "source1", + "origin_minion_pool_id": None, + "destination_endpoint_id": "dest1", + "destination_minion_pool_id": None, + "instance_osmorphing_minion_pool_mappings": None, + "shutdown_instances": False, + "destination_environment": {"opt1": "env1"}, + "source_environment": None, + "network_map": {"net_source": "net_dest"}, + "user_scripts": None, + "tasks": [], + "transfer_result": None, +} +DEPLOYMENT_FORMATTED_DATA = [ + "depl_id", "COMPLETED", None, None, "transfer_1", "replica", "res1", + "instance1", "", "source1", None, "dest1", None, '{}', False, + '{\n "opt1": "env1"\n}', '{}', '{\n "net_source": "net_dest"\n}', "", "", + None, '{}', "", '{}'] + +DEPLOYMENT_LIST_DATA = { + "id": "1", + "transfer_id": "2", + "last_execution_status": "RUNNING", + "instances": ["instance1", "instance2"], + "notes": "test_notes", + "created_at": "2024-11-28T15:18:25.000000", +} +DEPLOYMENT_LIST_FORMATTED_DATA = ( + "1", "2", "RUNNING", "instance1\ninstance2", "test_notes", + "2024-11-28T15:18:25.000000") +APP_MOCK = mock.MagicMock() + + +class DeploymentFormatterTestCase(test_base.CoriolisBaseTestCase): + + def setUp(self): + super(DeploymentFormatterTestCase, self).setUp() + self.formatter = deployments.DeploymentFormatter() + + def test__get_sorted_list(self): + obj1 = mock.Mock(created_at="2024-10-24T18:51:32.000000") + obj2 = mock.Mock(created_at="2024-11-18T15:18:25.000000") + obj3 = mock.Mock(created_at="2024-11-28T15:18:25.000000") + obj_list = [obj1, obj3, obj2] + self.assertEqual( + [obj1, obj2, obj3], self.formatter._get_sorted_list(obj_list)) + + def test__get_formatted_data(self): + obj = mock.Mock(**DEPLOYMENT_LIST_DATA) + self.assertEqual( + DEPLOYMENT_LIST_FORMATTED_DATA, + self.formatter._get_formatted_data(obj)) + + +class DeploymentDetailFormatterTestCase(test_base.CoriolisBaseTestCase): + + def setUp(self): + super(DeploymentDetailFormatterTestCase, self).setUp() + self.formatter = deployments.DeploymentDetailFormatter( + show_instances_data=True) + self.progress_updates = [ + {"created_at": "2024-10-24T18:51:32.000000", + "index": 0, + "message": "message 0"}, + {"created_at": "2024-11-18T15:18:25.000000", + "index": 2, + "message": "message 2"}, + {"created_at": "2024-11-28T15:18:25.000000", + "index": 1, + "message": "message 1"}, + ] + self.formatted_progress_updates = ( + f"2024-10-24T18:51:32.000000 message 0{os.linesep}" + f"2024-11-28T15:18:25.000000 message 1{os.linesep}" + f"2024-11-18T15:18:25.000000 message 2") + self.tasks_data = [ + { + "id": "1", + "task_type": "type1", + "instance": "instance1", + "status": "COMPLETED", + "depends_on": ["task0"], + "exception_details": None, + "progress_updates": self.progress_updates, + }, + { + "id": "2", + "task_type": "type2", + "instance": "instance1", + "status": "COMPLETED", + "depends_on": ["task0"], + "exception_details": None, + "progress_updates": self.progress_updates, + } + ] + self.formatted_tasks = [ + f'id: 1{os.linesep}' + f'task_type: type1{os.linesep}' + f'instance: instance1{os.linesep}' + f'status: COMPLETED{os.linesep}' + f'depends_on: task0{os.linesep}' + f'exception_details: {os.linesep}' + f'progress_updates:{os.linesep}' + f'{self.formatted_progress_updates}', + + f'id: 2{os.linesep}' + f'task_type: type2{os.linesep}' + f'instance: instance1{os.linesep}' + f'status: COMPLETED{os.linesep}' + f'depends_on: task0{os.linesep}' + f'exception_details: {os.linesep}' + f'progress_updates:{os.linesep}' + f'{self.formatted_progress_updates}' + ] + self.manager_mock = mock.MagicMock() + + def test_init_no_instances_data(self): + formatter = deployments.DeploymentDetailFormatter( + show_instances_data=False) + self.assertNotIn('instances_data', formatter.columns) + + def test__format_instances(self): + obj = mock.Mock(instances=["instance2", "instance3", "instance1"]) + self.assertEqual( + f"instance1{os.linesep}instance2{os.linesep}instance3", + self.formatter._format_instances(obj)) + + def test__format_progress_updates(self): + task_dict = { + "progress_updates": self.progress_updates} + self.assertEqual( + self.formatted_progress_updates, + self.formatter._format_progress_updates(task_dict)) + + def test__format_task(self): + task_data = self.tasks_data[0] + + task = common.Task(self.manager_mock, task_data) + + self.assertEqual( + self.formatted_tasks[0], self.formatter._format_task(task)) + + def test__format_tasks(self): + obj = mock.Mock( + tasks=[common.Task(self.manager_mock, task_data) + for task_data in self.tasks_data]) + expected_result = (f'{self.formatted_tasks[0]}{os.linesep}{os.linesep}' + f'{self.formatted_tasks[1]}') + self.assertEqual(expected_result, self.formatter._format_tasks(obj)) + + def test__get_formatted_data(self): + obj_data = { + **DEPLOYMENT_DATA, + "info": {"depl": "info"}, + } + obj = mock.Mock(**obj_data) + obj.to_dict.return_value = obj_data + expected_result = copy.copy(DEPLOYMENT_FORMATTED_DATA) + expected_result.append(obj_data['info']) + + self.assertEqual( + expected_result, self.formatter._get_formatted_data(obj)) + + +class CreateDeploymentTestCase(test_base.CoriolisBaseTestCase): + + def setUp(self): + super(CreateDeploymentTestCase, self).setUp() + self.mock_app = APP_MOCK + self.cli = deployments.CreateDeployment(self.mock_app, 'app_arg') + + def test_get_parser(self): + parser = self.cli.get_parser('coriolis') + global_script = "linux=/linux/path" + instance_script = "instance1=/instance1/path" + args = parser.parse_args([ + 'transfer_id', '--force', '--dont-clone-disks', + '--skip-os-morphing', '--user-script-global', global_script, + '--user-script-instance', instance_script]) + self.assertEqual( + ('transfer_id', True, False, True, [global_script], + [instance_script]), + (args.transfer, args.force, args.clone_disks, + args.skip_os_morphing, args.global_scripts, + args.instance_scripts)) + + def test_take_action(self): + args = mock.Mock(global_scripts=None, instance_scripts=None) + args.instance_osmorphing_minion_pool_mappings = [ + {"instance_id": "instance1", "pool_id": "pool1"}] + mock_fun = (self.mock_app.client_manager.coriolis.deployments. + create_from_transfer) + mock_fun.return_value = ( + v1_deployments.Deployment(mock.MagicMock(), DEPLOYMENT_DATA)) + expected_pool_mappings = {"instance1": "pool1"} + expected_user_scripts = {"global": {}, "instances": {}} + expected_data = copy.copy(DEPLOYMENT_FORMATTED_DATA) + + columns, data = self.cli.take_action(args) + + self.assertEqual( + deployments.DeploymentDetailFormatter().columns, columns) + self.assertEqual(expected_data, data) + mock_fun.assert_called_once_with( + args.transfer, args.clone_disks, args.force, args.skip_os_morphing, + user_scripts=expected_user_scripts, + instance_osmorphing_minion_pool_mappings=expected_pool_mappings) + + +class ShowDeploymentTestCase(test_base.CoriolisBaseTestCase): + + def setUp(self): + super(ShowDeploymentTestCase, self).setUp() + self.mock_app = APP_MOCK + self.cli = deployments.ShowDeployment(self.mock_app, 'app_args') + + def test_get_parser(self): + parser = self.cli.get_parser('coriolis') + args = parser.parse_args([DEPLOYMENT_ID, '--show-instances-data']) + self.assertEqual( + (DEPLOYMENT_ID, True), + (args.id, args.show_instances_data)) + + def test_take_action(self): + show_instances_data = False + args = mock.Mock( + id=DEPLOYMENT_ID, show_instances_data=show_instances_data) + mock_fun = self.mock_app.client_manager.coriolis.deployments.get + mock_fun.return_value = v1_deployments.Deployment( + mock.MagicMock(), DEPLOYMENT_DATA) + expected_data = DEPLOYMENT_FORMATTED_DATA + + columns, data = self.cli.take_action(args) + + self.assertEqual( + deployments.DeploymentDetailFormatter( + show_instances_data=show_instances_data).columns, columns) + self.assertEqual(expected_data, data) + mock_fun.assert_called_once_with(DEPLOYMENT_ID) + + +class CancelDeploymentTestCase(test_base.CoriolisBaseTestCase): + + def setUp(self): + super(CancelDeploymentTestCase, self).setUp() + self.mock_app = APP_MOCK + self.cli = deployments.CancelDeployment(self.mock_app, 'app_args') + + def test_get_parser(self): + parser = self.cli.get_parser('coriolis') + args = parser.parse_args([DEPLOYMENT_ID, '--force']) + self.assertEqual((DEPLOYMENT_ID, True), (args.id, args.force)) + + def test_take_action(self): + force = True + args = mock.Mock(id=DEPLOYMENT_ID, force=force) + mock_fun = self.mock_app.client_manager.coriolis.deployments.cancel + + self.cli.take_action(args) + + mock_fun.assert_called_once_with(DEPLOYMENT_ID, force) + + +class DeleteDeploymentTestCase(test_base.CoriolisBaseTestCase): + + def setUp(self): + super(DeleteDeploymentTestCase, self).setUp() + self.mock_app = APP_MOCK + self.cli = deployments.DeleteDeployment(self.mock_app, 'app_args') + + def test_get_parser(self): + parser = self.cli.get_parser('coriolis') + args = parser.parse_args([DEPLOYMENT_ID]) + self.assertEqual(DEPLOYMENT_ID, args.id) + + def test_take_action(self): + args = mock.Mock(id=DEPLOYMENT_ID) + mock_fun = self.mock_app.client_manager.coriolis.deployments.delete + + self.cli.take_action(args) + mock_fun.assert_called_once_with(DEPLOYMENT_ID) + + +class ListDeploymentTestCase(test_base.CoriolisBaseTestCase): + + def setUp(self): + super(ListDeploymentTestCase, self).setUp() + self.mock_app = APP_MOCK + self.cli = deployments.ListDeployment(self.mock_app, 'app_args') + + def test_get_parser(self): + parser = self.cli.get_parser('coriolis') + self.assertIsInstance(parser, argparse.ArgumentParser) + + def test_take_action(self): + mock_fun = self.mock_app.client_manager.coriolis.deployments.list + mock_fun.return_value = [ + v1_deployments.Deployment(mock.MagicMock(), DEPLOYMENT_LIST_DATA)] + + columns, data = self.cli.take_action(mock.ANY) + + self.assertEqual(deployments.DeploymentFormatter().columns, columns) + self.assertEqual([DEPLOYMENT_LIST_FORMATTED_DATA], list(data)) diff --git a/coriolisclient/tests/cli/test_diagnostics.py b/coriolisclient/tests/cli/test_diagnostics.py new file mode 100644 index 0000000..ba79c12 --- /dev/null +++ b/coriolisclient/tests/cli/test_diagnostics.py @@ -0,0 +1,90 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from cliff import lister +from unittest import mock + +from coriolisclient.cli import diagnostics +from coriolisclient.tests import test_base + + +class DiagnosticsFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Diagnostics.""" + + def setUp(self): + super(DiagnosticsFormatterTestCase, self).setUp() + self.diag = diagnostics.DiagnosticsFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.application = "app1" + obj2.application = "app2" + obj3.application = "app3" + obj_list = [obj2, obj1, obj3] + + result = self.diag._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.application = mock.sentinel.application + obj.hostname = mock.sentinel.hostname + obj.ip_addresses = mock.sentinel.ip_addresses + obj.packages = mock.sentinel.packages + obj.os_info = mock.sentinel.os_info + + result = self.diag._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.application, + mock.sentinel.hostname, + mock.sentinel.ip_addresses, + mock.sentinel.packages, + mock.sentinel.os_info + ), + result + ) + + +class GetCoriolisDiagnosticsTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Get Coriolis Diagnostics.""" + + @mock.patch.object(lister.Lister, '__init__') + def setUp(self, mock__init__): + mock__init__.return_value = None + super(GetCoriolisDiagnosticsTestCase, self).setUp() + self.diag = diagnostics.GetCoriolisDiagnostics( + mock.sentinel.app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser(self, mock_get_parser): + mock_get_parser.return_value = None + result = self.diag.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(diagnostics.DiagnosticsFormatter, 'list_objects') + def test_take_action(self, mock_list_objects): + mock_app = mock.Mock() + self.diag.app = mock_app + result = self.diag.take_action(mock.sentinel.prog_name) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + (mock_app.client_manager.coriolis.diagnostics.get. + assert_called_once)() + mock_list_objects.assert_called_once_with( + mock_app.client_manager.coriolis.diagnostics.get.return_value) diff --git a/coriolisclient/tests/cli/test_endpoint_destination_minion_pool_options.py b/coriolisclient/tests/cli/test_endpoint_destination_minion_pool_options.py new file mode 100644 index 0000000..a061032 --- /dev/null +++ b/coriolisclient/tests/cli/test_endpoint_destination_minion_pool_options.py @@ -0,0 +1,122 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from cliff import lister +from unittest import mock + +from coriolisclient.cli import endpoint_destination_minion_pool_options +from coriolisclient.cli import utils as cli_utils +from coriolisclient.tests import test_base + + +class EndpointDestinationMinionPoolOptionsFormatterTestCase( + test_base.CoriolisBaseTestCase): + """ + Test suite for the Coriolis Client Endpoint Destination Minion Pool + Options Formatter. + """ + + def setUp(self): + super(EndpointDestinationMinionPoolOptionsFormatterTestCase, self + ).setUp() + self.endpoint = (endpoint_destination_minion_pool_options. + EndpointDestinationMinionPoolOptionsFormatter)() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.name = "app1" + obj2.name = "app2" + obj3.name = "app3" + obj_list = [obj2, obj1, obj3] + + result = self.endpoint._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + @mock.patch.object(cli_utils, 'format_json_for_object_property') + def test_get_formatted_data(self, mock_format_json_for_object_property): + obj = mock.Mock() + obj.name = mock.sentinel.name + obj.to_dict = mock.Mock( + return_value={"config_default": mock.sentinel.config_default} + ) + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.name, + mock_format_json_for_object_property.return_value, + mock.sentinel.config_default + ), + result + ) + + +class ListEndpointDestinationMinionPoolOptionsTestCase( + test_base.CoriolisBaseTestCase): + """ + Test suite for the Coriolis Client List Endpoint Destination Minion Pool + Options. + """ + + def setUp(self): + self.mock_app = mock.Mock() + super(ListEndpointDestinationMinionPoolOptionsTestCase, self).setUp() + self.endpoint = (endpoint_destination_minion_pool_options. + ListEndpointDestinationMinionPoolOptions)( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, 'environment') + + @mock.patch.object(endpoint_destination_minion_pool_options. + EndpointDestinationMinionPoolOptionsFormatter, + 'list_objects') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_list_objects + ): + args = mock.Mock() + args.options = mock.sentinel.options + mock_endpoints = mock.Mock() + mock_empdo = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_empdo = (self.mock_app.client_manager.coriolis. + endpoint_destination_minion_pool_options) + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment', error_on_no_value=False) + mock_empdo.list.assert_called_once_with( + mock_endpoints.get_endpoint_id_for_name(args.endpoint), + environment=mock_get_option_value_from_args.return_value, + option_names=mock.sentinel.options + ) + mock_list_objects.assert_called_once_with(mock_empdo.list.return_value) diff --git a/coriolisclient/tests/cli/test_endpoint_destination_options.py b/coriolisclient/tests/cli/test_endpoint_destination_options.py new file mode 100644 index 0000000..36ac421 --- /dev/null +++ b/coriolisclient/tests/cli/test_endpoint_destination_options.py @@ -0,0 +1,120 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from cliff import lister +from unittest import mock + +from coriolisclient.cli import endpoint_destination_options +from coriolisclient.cli import utils as cli_utils +from coriolisclient.tests import test_base + + +class EndpointDestinationOptionFormatterTestCase( + test_base.CoriolisBaseTestCase): + """ + Test suite for the Coriolis Client Endpoint Destination Options Formatter. + """ + + def setUp(self): + super(EndpointDestinationOptionFormatterTestCase, self + ).setUp() + self.endpoint = (endpoint_destination_options. + EndpointDestinationOptionFormatter)() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.name = "app1" + obj2.name = "app2" + obj3.name = "app3" + obj_list = [obj2, obj1, obj3] + + result = self.endpoint._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + @mock.patch.object(cli_utils, 'format_json_for_object_property') + def test_get_formatted_data(self, mock_format_json_for_object_property): + obj = mock.Mock() + obj.name = mock.sentinel.name + obj.to_dict = mock.Mock( + return_value={"config_default": mock.sentinel.config_default} + ) + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.name, + mock_format_json_for_object_property.return_value, + mock.sentinel.config_default + ), + result + ) + + +class ListEndpointDestinationOptionsTestCase( + test_base.CoriolisBaseTestCase): + """ + Test suite for the Coriolis Client List Endpoint Destination Options. + """ + + def setUp(self): + self.mock_app = mock.Mock() + super(ListEndpointDestinationOptionsTestCase, self).setUp() + self.endpoint = ( + endpoint_destination_options.ListEndpointDestinationOptions)( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, 'environment') + + @mock.patch.object(endpoint_destination_options. + EndpointDestinationOptionFormatter, + 'list_objects') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_list_objects + ): + args = mock.Mock() + args.options = mock.sentinel.options + mock_endpoints = mock.Mock() + mock_edo = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_edo = (self.mock_app.client_manager.coriolis. + endpoint_destination_options) + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment', error_on_no_value=False) + mock_edo.list.assert_called_once_with( + mock_endpoints.get_endpoint_id_for_name(args.endpoint), + environment=mock_get_option_value_from_args.return_value, + option_names=mock.sentinel.options + ) + mock_list_objects.assert_called_once_with(mock_edo.list.return_value) diff --git a/coriolisclient/tests/cli/test_endpoint_instances.py b/coriolisclient/tests/cli/test_endpoint_instances.py new file mode 100644 index 0000000..4a0ce2c --- /dev/null +++ b/coriolisclient/tests/cli/test_endpoint_instances.py @@ -0,0 +1,218 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from cliff import lister +from cliff import show +from unittest import mock + +from coriolisclient.cli import endpoint_instances +from coriolisclient.cli import utils as cli_utils +from coriolisclient.tests import test_base + + +class EndpointInstanceFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Endpoint Instance Formatter.""" + + def setUp(self): + super(EndpointInstanceFormatterTestCase, self).setUp() + self.endpoint = endpoint_instances.EndpointInstanceFormatter() + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.flavor_name = mock.sentinel.flavor_name + obj.memory_mb = mock.sentinel.memory_mb + obj.num_cpu = mock.sentinel.num_cpu + obj.os_type = mock.sentinel.os_type + obj.to_dict = mock.Mock( + return_value={"instance_name": mock.sentinel.instance_name} + ) + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.instance_name, + mock.sentinel.flavor_name, + mock.sentinel.memory_mb, + mock.sentinel.num_cpu, + mock.sentinel.os_type + ), + result + ) + + +class InstancesDetailFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Instances Detail Formatter.""" + + def setUp(self): + super(InstancesDetailFormatterTestCase, self).setUp() + self.endpoint = endpoint_instances.InstancesDetailFormatter() + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.name = mock.sentinel.name + obj.flavor_name = mock.sentinel.flavor_name + obj.memory_mb = mock.sentinel.memory_mb + obj.num_cpu = mock.sentinel.num_cpu + obj.os_type = mock.sentinel.os_type + obj.to_dict = mock.Mock( + return_value={"instance_name": mock.sentinel.instance_name, + "firmware_type": mock.sentinel.firmware_type} + ) + devices = { + "controllers": ["controller1", "controller2"], + "disks": ["disk1", "disk2"], + "nics": ["nic1", "nic2"], + "cdroms": ["cdrom1", "cdrom2"], + "floppies": ["floppy1", "floppy2"], + "serial_ports": ["serial_port1", "serial_port2"] + } + obj.devices = devices + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + [ + mock.sentinel.id, + mock.sentinel.name, + mock.sentinel.instance_name, + mock.sentinel.flavor_name, + mock.sentinel.memory_mb, + mock.sentinel.num_cpu, + mock.sentinel.os_type, + mock.sentinel.firmware_type, + '[\n "controller1",\n "controller2"\n]', + '[\n "disk1",\n "disk2"\n]', + '[\n "nic1",\n "nic2"\n]', + '[\n "cdrom1",\n "cdrom2"\n]', + '[\n "floppy1",\n "floppy2"\n]', + '[\n "serial_port1",\n "serial_port2"\n]' + ], + result + ) + + +class ListEndpointInstanceTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Endpoint Instance.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListEndpointInstanceTestCase, self).setUp() + self.endpoint = endpoint_instances.ListEndpointInstance( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, 'environment') + + @mock.patch.object(endpoint_instances.EndpointInstanceFormatter, + 'list_objects') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_list_objects + ): + args = mock.Mock() + args.marker = mock.sentinel.marker + args.limit = mock.sentinel.limit + args.name = mock.sentinel.name + args.refresh = mock.sentinel.refresh + mock_endpoints = mock.Mock() + mock_ei = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_ei = self.mock_app.client_manager.coriolis.endpoint_instances + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment', error_on_no_value=False) + mock_ei.list.assert_called_once_with( + mock_endpoints.get_endpoint_id_for_name(args.endpoint), + mock_get_option_value_from_args.return_value, + mock.sentinel.marker, + mock.sentinel.limit, + mock.sentinel.name, + refresh=mock.sentinel.refresh, + ) + mock_list_objects.assert_called_once_with(mock_ei.list.return_value) + + +class ShowEndpointInstanceTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Show Endpoint Instance""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ShowEndpointInstanceTestCase, self).setUp() + self.endpoint = endpoint_instances.ShowEndpointInstance( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, 'environment') + + @mock.patch.object(endpoint_instances.InstancesDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_get_formatted_entity + ): + args = mock.Mock() + args.instance = mock.sentinel.instance + mock_endpoints = mock.Mock() + mock_ei = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_ei = self.mock_app.client_manager.coriolis.endpoint_instances + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment', error_on_no_value=False) + mock_ei.get.assert_called_once_with( + mock_endpoints.get_endpoint_id_for_name(args.endpoint), + mock.sentinel.instance, + mock_get_option_value_from_args.return_value, + ) + mock_get_formatted_entity.assert_called_once_with( + mock_ei.get.return_value) diff --git a/coriolisclient/tests/cli/test_endpoint_networks.py b/coriolisclient/tests/cli/test_endpoint_networks.py new file mode 100644 index 0000000..561b285 --- /dev/null +++ b/coriolisclient/tests/cli/test_endpoint_networks.py @@ -0,0 +1,104 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from cliff import lister +from unittest import mock + +from coriolisclient.cli import endpoint_networks +from coriolisclient.cli import utils as cli_utils +from coriolisclient.tests import test_base + + +class EndpointNetworkFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Endpoint Networks Formatter.""" + + def setUp(self): + super(EndpointNetworkFormatterTestCase, self).setUp() + self.endpoint = endpoint_networks.EndpointNetworkFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.name = "app1" + obj2.name = "app2" + obj3.name = "app3" + obj_list = [obj2, obj1, obj3] + + result = self.endpoint._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.name = mock.sentinel.name + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.name, + ), + result + ) + + +class ListEndpointNetworkTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Endpoint Networks.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListEndpointNetworkTestCase, self).setUp() + self.endpoint = endpoint_networks.ListEndpointNetwork( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, 'environment') + + @mock.patch.object(endpoint_networks.EndpointNetworkFormatter, + 'list_objects') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_list_objects + ): + args = mock.Mock() + args.options = mock.sentinel.options + mock_endpoints = mock.Mock() + mock_en = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_en = self.mock_app.client_manager.coriolis.endpoint_networks + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment', error_on_no_value=False) + mock_en.list.assert_called_once_with( + mock_endpoints.get_endpoint_id_for_name(args.endpoint), + mock_get_option_value_from_args.return_value + ) + mock_list_objects.assert_called_once_with(mock_en.list.return_value) diff --git a/coriolisclient/tests/cli/test_endpoint_source_minion_pool_options.py b/coriolisclient/tests/cli/test_endpoint_source_minion_pool_options.py new file mode 100644 index 0000000..07b7155 --- /dev/null +++ b/coriolisclient/tests/cli/test_endpoint_source_minion_pool_options.py @@ -0,0 +1,121 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from cliff import lister +from unittest import mock + +from coriolisclient.cli import endpoint_source_minion_pool_options +from coriolisclient.cli import utils as cli_utils +from coriolisclient.tests import test_base + + +class EndpointSourceMinionPoolOptionsFormatterTestCase( + test_base.CoriolisBaseTestCase): + """ + Test suite for the Coriolis Client Endpoint Source Minion Pool Options + Formatter. + """ + + def setUp(self): + super(EndpointSourceMinionPoolOptionsFormatterTestCase, self).setUp() + self.endpoint = (endpoint_source_minion_pool_options. + EndpointSourceMinionPoolOptionsFormatter)() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.name = "app1" + obj2.name = "app2" + obj3.name = "app3" + obj_list = [obj2, obj1, obj3] + + result = self.endpoint._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + @mock.patch.object(cli_utils, 'format_json_for_object_property') + def test_get_formatted_data(self, mock_format_json_for_object_property): + obj = mock.Mock() + obj.name = mock.sentinel.name + obj.to_dict = mock.Mock( + return_value={"config_default": mock.sentinel.config_default} + ) + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.name, + mock_format_json_for_object_property.return_value, + mock.sentinel.config_default + ), + result + ) + + +class ListEndpointSourceMinionPoolOptionsTestCase( + test_base.CoriolisBaseTestCase): + """ + Test suite for the Coriolis Client List Endpoint Source Minion Pool + Options. + """ + + def setUp(self): + self.mock_app = mock.Mock() + super(ListEndpointSourceMinionPoolOptionsTestCase, self).setUp() + self.endpoint = (endpoint_source_minion_pool_options. + ListEndpointSourceMinionPoolOptions)( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, 'environment') + + @mock.patch.object(endpoint_source_minion_pool_options. + EndpointSourceMinionPoolOptionsFormatter, + 'list_objects') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_list_objects + ): + args = mock.Mock() + args.options = mock.sentinel.options + mock_endpoints = mock.Mock() + mock_empso = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_empso = (self.mock_app.client_manager.coriolis. + endpoint_source_minion_pool_options) + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment', error_on_no_value=False) + mock_empso.list.assert_called_once_with( + mock_endpoints.get_endpoint_id_for_name(args.endpoint), + environment=mock_get_option_value_from_args.return_value, + option_names=mock.sentinel.options + ) + mock_list_objects.assert_called_once_with(mock_empso.list.return_value) diff --git a/coriolisclient/tests/cli/test_endpoint_source_options.py b/coriolisclient/tests/cli/test_endpoint_source_options.py new file mode 100644 index 0000000..cdcfd59 --- /dev/null +++ b/coriolisclient/tests/cli/test_endpoint_source_options.py @@ -0,0 +1,110 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from cliff import lister +from unittest import mock + +from coriolisclient.cli import endpoint_source_options +from coriolisclient.cli import utils as cli_utils +from coriolisclient.tests import test_base + + +class EndpointSourceOptionFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Endpoint Source Option Formatter.""" + + def setUp(self): + super(EndpointSourceOptionFormatterTestCase, self).setUp() + self.endpoint = endpoint_source_options.EndpointSourceOptionFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.name = "app1" + obj2.name = "app2" + obj3.name = "app3" + obj_list = [obj2, obj1, obj3] + + result = self.endpoint._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + @mock.patch.object(cli_utils, 'format_json_for_object_property') + def test_get_formatted_data(self, mock_format_json_for_object_property): + obj = mock.Mock() + obj.name = mock.sentinel.name + obj.to_dict = mock.Mock( + return_value={"config_default": mock.sentinel.config_default} + ) + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.name, + mock_format_json_for_object_property.return_value, + mock.sentinel.config_default + ), + result + ) + + +class ListEndpointSourceOptionsTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Endpoint Source Options.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListEndpointSourceOptionsTestCase, self).setUp() + self.endpoint = endpoint_source_options.ListEndpointSourceOptions( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, 'environment') + + @mock.patch.object(endpoint_source_options.EndpointSourceOptionFormatter, + 'list_objects') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_list_objects + ): + args = mock.Mock() + args.options = mock.sentinel.options + mock_endpoints = mock.Mock() + mock_edo = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_edo = (self.mock_app. + client_manager.coriolis.endpoint_source_options) + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment', error_on_no_value=False) + mock_edo.list.assert_called_once_with( + mock_endpoints.get_endpoint_id_for_name(args.endpoint), + environment=mock_get_option_value_from_args.return_value, + option_names=mock.sentinel.options + ) + mock_list_objects.assert_called_once_with(mock_edo.list.return_value) diff --git a/coriolisclient/tests/cli/test_endpoint_storage.py b/coriolisclient/tests/cli/test_endpoint_storage.py new file mode 100644 index 0000000..0ff65c3 --- /dev/null +++ b/coriolisclient/tests/cli/test_endpoint_storage.py @@ -0,0 +1,107 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from cliff import lister +from unittest import mock + +from coriolisclient.cli import endpoint_storage +from coriolisclient.cli import utils as cli_utils +from coriolisclient.tests import test_base + + +class EndpointStorageFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Endpoint Storage Formatter.""" + + def setUp(self): + super(EndpointStorageFormatterTestCase, self).setUp() + self.endpoint = endpoint_storage.EndpointStorageFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.name = "app1" + obj2.name = "app2" + obj3.name = "app3" + obj_list = [obj2, obj1, obj3] + + result = self.endpoint._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.name = mock.sentinel.name + obj.additional_provider_properties = \ + mock.sentinel.additional_provider_properties + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.name, + mock.sentinel.additional_provider_properties + ), + result + ) + + +class ListEndpointStorageTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Endpoint Storage.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListEndpointStorageTestCase, self).setUp() + self.endpoint = endpoint_storage.ListEndpointStorage( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, 'environment') + + @mock.patch.object(endpoint_storage.EndpointStorageFormatter, + 'list_objects') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_list_objects + ): + args = mock.Mock() + args.options = mock.sentinel.options + mock_endpoints = mock.Mock() + mock_es = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_es = self.mock_app.client_manager.coriolis.endpoint_storage + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment', error_on_no_value=False) + mock_es.list.assert_called_once_with( + mock_endpoints.get_endpoint_id_for_name(args.endpoint), + environment=mock_get_option_value_from_args.return_value + ) + mock_list_objects.assert_called_once_with(mock_es.list.return_value) diff --git a/coriolisclient/tests/cli/test_endpoints.py b/coriolisclient/tests/cli/test_endpoints.py new file mode 100644 index 0000000..09b8acd --- /dev/null +++ b/coriolisclient/tests/cli/test_endpoints.py @@ -0,0 +1,535 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from cliff import command +from cliff import lister +from cliff import show +import ddt +from unittest import mock + +from coriolisclient.cli import endpoints +from coriolisclient import exceptions +from coriolisclient.tests import test_base + + +@ddt.ddt +class EndpointTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Endpoints.""" + + def test_add_connection_info_args_to_parser(self): + mock_parser = mock.Mock() + mock_add_argument = mock.Mock() + mock_parser.add_mutually_exclusive_group.return_value.add_argument = \ + mock_add_argument + + result = endpoints.add_connection_info_args_to_parser(mock_parser) + + self.assertEqual( + mock_parser, + result + ) + mock_parser.add_mutually_exclusive_group.assert_called_once() + + @ddt.data( + {"conn": (None, None, None), "expected_result": None, + "raise_if_none": False}, + {"conn": (None, None, None), "expected_result": "exception"}, + { + "conn": ('{"conn_info": "mock_conn_info"}', None, None), + "expected_result": {"conn_info": "mock_conn_info"} + }, + { + "conn": (None, '{"conn_info": "mock_conn_info"}', None), + "expected_result": {"conn_info": "mock_conn_info"} + }, + { + "conn": (None, 'invalid', None), + "expected_result": "exception" + }, + { + "conn": (None, None, "mock_secret"), + "expected_result": {"secret_ref": "mock_secret"} + }, + ) + def test_get_connection_info_from_args(self, data): + mock_args = mock.MagicMock() + (mock_args.connection, + mock_args.connection_file.__enter__.return_value.read.return_value, + mock_args.connection_secret) = data["conn"] + if data["conn"][1] is None: + mock_args.connection_file = None + + if data["expected_result"] == "exception": + self.assertRaises( + ValueError, + endpoints.get_connection_info_from_args, + mock_args + ) + else: + result = endpoints.get_connection_info_from_args( + mock_args, raise_if_none=data.get("raise_if_none", True)) + + self.assertEqual( + data["expected_result"], + result + ) + + +class EndpointFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Endpoint Formatter.""" + + def setUp(self): + super(EndpointFormatterTestCase, self).setUp() + self.endpoint = endpoints.EndpointFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.created_at = "date1" + obj2.created_at = "date2" + obj3.created_at = "date3" + obj_list = [obj2, obj1, obj3] + + result = self.endpoint._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.name = mock.sentinel.name + obj.type = mock.sentinel.type + obj.description = mock.sentinel.description + obj.mapped_regions = mock.sentinel.mapped_regions + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.name, + mock.sentinel.type, + mock.sentinel.description, + mock.sentinel.mapped_regions + ), + result + ) + + +class EndpointDetailFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Endpoint Detail Formatter.""" + + def setUp(self): + super(EndpointDetailFormatterTestCase, self).setUp() + self.endpoint = endpoints.EndpointDetailFormatter() + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.name = mock.sentinel.name + obj.type = mock.sentinel.type + obj.description = mock.sentinel.description + obj.mapped_regions = mock.sentinel.mapped_regions + obj.created_at = mock.sentinel.created_at + obj.updated_at = mock.sentinel.updated_at + obj.connection_info.to_dict = mock.Mock( + return_value=mock.sentinel.connection_info + ) + + result = self.endpoint._get_formatted_data(obj) + + self.assertEqual( + [ + mock.sentinel.id, + mock.sentinel.name, + mock.sentinel.type, + mock.sentinel.description, + mock.sentinel.connection_info, + mock.sentinel.mapped_regions, + mock.sentinel.created_at, + mock.sentinel.updated_at + ], + result + ) + + +class CreateEndpointTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Create Endpoint.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(CreateEndpointTestCase, self).setUp() + self.endpoint = endpoints.CreateEndpoint( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(endpoints, 'add_connection_info_args_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_connection_info_args_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_connection_info_args_to_parser.assert_called_once_with( + mock_get_parser.return_value) + + @mock.patch.object(endpoints.EndpointDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(endpoints, 'get_connection_info_from_args') + def test_take_action( + self, + mock_get_connection_info_from_args, + mock_get_formatted_entity + ): + args = mock.Mock() + args.connection_secret = mock.sentinel.connection_secret + args.connection = None + args.name = mock.sentinel.name + args.provider = mock.sentinel.provider + args.description = mock.sentinel.description + args.regions = mock.sentinel.regions + args.skip_validation = False + mock_create = mock.Mock() + mock_validate_connection = mock.Mock() + mock_validate_connection.return_value = (True, "mock_message") + self.mock_app.client_manager.coriolis.endpoints.create = mock_create + self.mock_app.client_manager.coriolis.endpoints.validate_connection = \ + mock_validate_connection + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_get_connection_info_from_args.assert_called_once_with(args) + mock_create.assert_called_once_with( + mock.sentinel.name, + mock.sentinel.provider, + mock_get_connection_info_from_args.return_value, + mock.sentinel.description, + regions=mock.sentinel.regions, + ) + mock_validate_connection.assert_called_once_with( + mock_create.return_value.id) + mock_get_formatted_entity.assert_called_once_with( + mock_create.return_value) + + def test_take_action_raises_connection(self): + args = mock.Mock() + args.connection_secret = mock.sentinel.connection_secret + args.connection = mock.sentinel.connection + + self.assertRaises( + exceptions.CoriolisException, + self.endpoint.take_action, + args + ) + + @mock.patch.object(endpoints.EndpointDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(endpoints, 'get_connection_info_from_args') + def test_take_action_validation_failed( + self, + mock_get_connection_info_from_args, + mock_get_formatted_entity + ): + args = mock.Mock() + args.connection_secret = mock.sentinel.connection_secret + args.connection = None + args.name = mock.sentinel.name + args.provider = mock.sentinel.provider + args.description = mock.sentinel.description + args.regions = mock.sentinel.regions + args.skip_validation = False + mock_create = mock.Mock() + mock_validate_connection = mock.Mock() + mock_validate_connection.return_value = (False, "mock_message") + self.mock_app.client_manager.coriolis.endpoints.create = mock_create + self.mock_app.client_manager.coriolis.endpoints.validate_connection = \ + mock_validate_connection + + self.assertRaises( + exceptions.EndpointConnectionValidationFailed, + self.endpoint.take_action, + args + ) + + mock_get_connection_info_from_args.assert_called_once_with(args) + mock_create.assert_called_once_with( + mock.sentinel.name, + mock.sentinel.provider, + mock_get_connection_info_from_args.return_value, + mock.sentinel.description, + regions=mock.sentinel.regions, + ) + mock_validate_connection.assert_called_once_with( + mock_create.return_value.id) + mock_get_formatted_entity.assert_not_called() + + +class UpdateEndpointTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Update Endpoint.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(UpdateEndpointTestCase, self).setUp() + self.endpoint = endpoints.UpdateEndpoint( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(endpoints, 'add_connection_info_args_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_connection_info_args_to_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_connection_info_args_to_parser.assert_called_once_with( + mock_get_parser.return_value) + + @mock.patch.object(endpoints.EndpointDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(endpoints, 'get_connection_info_from_args') + def test_take_action( + self, + mock_get_connection_info_from_args, + mock_get_formatted_entity + ): + args = mock.Mock() + args.connection_secret = mock.sentinel.connection_secret + args.connection = None + args.id = mock.sentinel.id + args.name = mock.sentinel.name + args.description = mock.sentinel.description + args.regions = mock.sentinel.regions + args.skip_validation = False + mock_update = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints.update = mock_update + expected_updated_values = { + "name": mock.sentinel.name, + "description": mock.sentinel.description, + "connection_info": (mock_get_connection_info_from_args. + return_value), + "mapped_regions": mock.sentinel.regions, + } + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_get_connection_info_from_args.assert_called_once_with( + args, raise_if_none=False) + mock_update.assert_called_once_with( + mock.sentinel.id, + expected_updated_values, + ) + mock_get_formatted_entity.assert_called_once_with( + mock_update.return_value) + + def test_take_action_raises_connection(self): + args = mock.Mock() + args.connection_secret = mock.sentinel.connection_secret + args.connection = mock.sentinel.connection + + self.assertRaises( + exceptions.CoriolisException, + self.endpoint.take_action, + args + ) + + +class ShowEndpointTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Show Endpoint.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ShowEndpointTestCase, self).setUp() + self.endpoint = endpoints.ShowEndpoint( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(endpoints.EndpointDetailFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + mock_client = mock.Mock() + args.id = mock.sentinel.id + self.mock_app.client_manager.coriolis.endpoints = mock_client + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_client.get_endpoint_id_for_name.assert_called_once_with( + mock.sentinel.id) + mock_client.get.assert_called_once_with( + mock_client.get_endpoint_id_for_name.return_value) + mock_get_formatted_entity.assert_called_once_with( + mock_client.get.return_value) + + +class DeleteEndpointTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Delete Endpoint.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DeleteEndpointTestCase, self).setUp() + self.endpoint = endpoints.DeleteEndpoint( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser(self, mock_get_parser): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + mock_client = mock.Mock() + args.id = mock.sentinel.id + self.mock_app.client_manager.coriolis.endpoints = mock_client + + self.endpoint.take_action(args) + + mock_client.get_endpoint_id_for_name.assert_called_once_with( + mock.sentinel.id) + mock_client.delete.assert_called_once_with( + mock_client.get_endpoint_id_for_name.return_value) + + +class ListEndpointTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Endpoint.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListEndpointTestCase, self).setUp() + self.endpoint = endpoints.ListEndpoint( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser(self, mock_get_parser): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(endpoints.EndpointFormatter, 'list_objects') + def test_take_action(self, mock_list_objects): + args = mock.Mock() + mock_client = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_client + + result = self.endpoint.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + + mock_client.list.assert_called_once() + mock_list_objects.assert_called_once_with( + mock_client.list.return_value) + + +class EndpointValidateConnectionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Endpoint.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(EndpointValidateConnectionTestCase, self).setUp() + self.endpoint = endpoints.EndpointValidateConnection( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser(self, mock_get_parser): + result = self.endpoint.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(endpoints.EndpointFormatter, 'list_objects') + def test_take_action(self, mock_list_objects): + args = mock.Mock() + mock_endpoint = mock.Mock() + args.id = mock.sentinel.id + self.mock_app.client_manager.coriolis.endpoints = mock_endpoint + mock_validate_connection = mock.Mock() + mock_validate_connection.return_value = (True, "mock_message") + self.mock_app.client_manager.coriolis.endpoints.validate_connection = \ + mock_validate_connection + + self.endpoint.take_action(args) + + mock_endpoint.get_endpoint_id_for_name.assert_called_once_with( + mock.sentinel.id) + mock_validate_connection.assert_called_once_with( + mock_endpoint.get_endpoint_id_for_name.return_value) + + @mock.patch.object(endpoints.EndpointFormatter, 'list_objects') + def test_take_action_not_valid(self, mock_list_objects): + args = mock.Mock() + mock_endpoint = mock.Mock() + args.id = mock.sentinel.id + self.mock_app.client_manager.coriolis.endpoints = mock_endpoint + mock_validate_connection = mock.Mock() + mock_validate_connection.return_value = (False, "mock_message") + self.mock_app.client_manager.coriolis.endpoints.validate_connection = \ + mock_validate_connection + + self.assertRaisesRegex( + exceptions.EndpointConnectionValidationFailed, + "mock_message", + self.endpoint.take_action, + args + ) + + mock_endpoint.get_endpoint_id_for_name.assert_called_once_with( + mock.sentinel.id) + mock_validate_connection.assert_called_once_with( + mock_endpoint.get_endpoint_id_for_name.return_value) diff --git a/coriolisclient/tests/cli/test_formatter.py b/coriolisclient/tests/cli/test_formatter.py new file mode 100644 index 0000000..6b9830d --- /dev/null +++ b/coriolisclient/tests/cli/test_formatter.py @@ -0,0 +1,158 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import ddt +from unittest import mock + +from coriolisclient.cli import formatter +from coriolisclient.tests import test_base + + +class TestEntityFormattter(formatter.EntityFormatter): + + def __init__(self): + self.columns = [ + "column_1", + "column_2" + ] + + def _get_formatted_data(self, obj): + return obj + + +@ddt.ddt +class TestEntityFormattterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Entity Formatter.""" + + def setUp(self): + super(TestEntityFormattterTestCase, self).setUp() + self.format = TestEntityFormattter() + + def test_get_sorted_list(self): + obj_list = ["obj2", "obj1"] + + result = self.format._get_sorted_list(obj_list) + + self.assertEqual( + obj_list, + result + ) + + def test_list_objects(self): + obj_list = ["obj2", "obj1"] + + (columns, data) = self.format.list_objects(obj_list) + + self.assertEqual( + ( + self.format.columns, + ["obj2", "obj1"] + ), + ( + columns, + list(data) + ) + ) + + def test_get_generic_data(self): + obj = mock.Mock() + + result = self.format._get_generic_data(obj) + + self.assertEqual( + obj, + result + ) + + def test_get_generic_columns(self): + result = self.format._get_generic_columns() + + self.assertEqual( + self.format.columns, + result + ) + + def test_get_formatted_entity(self): + obj = mock.Mock() + + (columns, data) = self.format.get_formatted_entity(obj) + + self.assertEqual( + (self.format.columns, obj), + (columns, data) + ) + + @ddt.data( + { + "current_value": 3, + "max_value": 9, + "percent_format": "{:.0f}%", + "expected_result": "33%" + }, + { + "current_value": 3, + "max_value": 9, + "percent_format": "{:.2f}%", + "expected_result": "33.33%" + }, + { + "current_value": 0, + "max_value": 9, + "percent_format": "{:.0f}%", + "expected_result": None + }, + { + "current_value": 3, + "max_value": None, + "percent_format": "{:.0f}%", + "expected_result": None + } + ) + def test_get_percent_string(self, data): + result = self.format._get_percent_string( + data["current_value"], + data["max_value"], + percent_format=data["percent_format"] + ) + + self.assertEqual( + data["expected_result"], + result + ) + + @mock.patch.object(formatter.EntityFormatter, '_get_percent_string') + def test_format_progress_update(self, mock_get_percent_string): + progress_update = { + "current_step": "mock_current_step", + "total_steps": "mock_total_steps", + "created_at": "mock_created_at", + "message": "mock_message", + } + mock_get_percent_string.return_value = "mock_percent_string" + + result = self.format._format_progress_update(progress_update) + + self.assertEqual( + "mock_created_at [mock_percent_string] mock_message", + result + ) + + @mock.patch.object(formatter.EntityFormatter, '_get_percent_string') + def test_format_progress_update_no_percent_string( + self, + mock_get_percent_string + ): + progress_update = { + "current_step": "mock_current_step", + "total_steps": "mock_total_steps", + "created_at": "mock_created_at", + "message": "mock_message", + } + mock_get_percent_string.return_value = None + + result = self.format._format_progress_update(progress_update) + + self.assertEqual( + "mock_created_at mock_message", + result + ) diff --git a/coriolisclient/tests/cli/test_licensing.py b/coriolisclient/tests/cli/test_licensing.py new file mode 100644 index 0000000..39c4dab --- /dev/null +++ b/coriolisclient/tests/cli/test_licensing.py @@ -0,0 +1,362 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from cliff import command +from cliff import lister +from cliff import show + +from coriolisclient.cli import licensing +from coriolisclient.tests import test_base + + +class LicensingStatusFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Licensing Status Formatter.""" + + def setUp(self): + super(LicensingStatusFormatterTestCase, self).setUp() + self.licence = licensing.LicensingStatusFormatter() + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.appliance_id = mock.sentinel.appliance_id + obj.earliest_licence_expiry_time = \ + mock.sentinel.earliest_licence_expiry_time + obj.latest_licence_expiry_time = \ + mock.sentinel.latest_licence_expiry_time + obj.current_performed_migrations = \ + mock.sentinel.current_performed_migrations + obj.current_performed_replicas = \ + mock.sentinel.current_performed_replicas + obj.current_available_migrations = \ + mock.sentinel.current_available_migrations + obj.current_available_replicas = \ + mock.sentinel.current_available_replicas + obj.lifetime_performed_migrations = \ + mock.sentinel.lifetime_performed_migrations + obj.lifetime_performed_replicas = \ + mock.sentinel.lifetime_performed_replicas + obj.lifetime_available_migrations = \ + mock.sentinel.lifetime_available_migrations + obj.lifetime_available_replicas = \ + mock.sentinel.lifetime_available_replicas + + result = self.licence._get_formatted_data(obj) + + self.assertEqual( + [ + mock.sentinel.appliance_id, + mock.sentinel.earliest_licence_expiry_time, + mock.sentinel.latest_licence_expiry_time, + mock.sentinel.current_performed_migrations, + mock.sentinel.current_performed_replicas, + mock.sentinel.current_available_migrations, + mock.sentinel.current_available_replicas, + mock.sentinel.lifetime_performed_migrations, + mock.sentinel.lifetime_performed_replicas, + mock.sentinel.lifetime_available_migrations, + mock.sentinel.lifetime_available_replicas + ], + result + ) + + +class LicenceFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Licence Formatter.""" + + def setUp(self): + super(LicenceFormatterTestCase, self).setUp() + self.licence = licensing.LicenceFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.period_end = "period_end1" + obj2.period_end = "period_end2" + obj3.period_end = "period_end3" + obj_list = [obj2, obj1, obj3] + + result = self.licence._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.issue_date = mock.sentinel.issue_date + obj.migrations = mock.sentinel.migrations + obj.replicas = mock.sentinel.replicas + obj.period_start = mock.sentinel.period_start + obj.period_end = mock.sentinel.period_end + obj.period_duration = mock.sentinel.period_duration + obj.licence_version = mock.sentinel.licence_version + + result = self.licence._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.issue_date, + mock.sentinel.migrations, + mock.sentinel.replicas, + mock.sentinel.period_start, + mock.sentinel.period_end, + mock.sentinel.period_duration, + mock.sentinel.licence_version + ), + result + ) + + +class LicensingApplianceStatusTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Licensing Appliance Status.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(LicensingApplianceStatusTestCase, self).setUp() + self.licence = licensing.LicensingApplianceStatus( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.licence.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing.LicensingStatusFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.appliance_id = mock.sentinel.appliance_id + mock_lic_status = mock.Mock() + self.mock_app.client_manager.coriolis.licensing.status = \ + mock_lic_status + + result = self.licence.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_lic_status.assert_called_once_with(mock.sentinel.appliance_id) + mock_get_formatted_entity.assert_called_once_with( + mock_lic_status.return_value) + + +class LicenceRegisterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Licence Register.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(LicenceRegisterTestCase, self).setUp() + self.licence = licensing.LicenceRegister( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.licence.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing.LicenceFormatter, 'get_formatted_entity') + def test_take_action_raw_arg( + self, + mock_get_formatted_entity + ): + args = mock.MagicMock() + args.licence_pem = mock.sentinel.licence_pem + args.licence_pem_file.__enter__.return_value.read.return_value = \ + mock.sentinel.licence_pem_file + mock_licence = mock.Mock() + self.mock_app.client_manager.coriolis.licensing.register = mock_licence + + result = self.licence.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_licence.assert_called_once_with( + args.appliance_id, mock.sentinel.licence_pem) + mock_get_formatted_entity.assert_called_once_with( + mock_licence.return_value) + + @mock.patch.object(licensing.LicenceFormatter, 'get_formatted_entity') + def test_take_action_file_arg( + self, + mock_get_formatted_entity + ): + args = mock.MagicMock() + args.licence_pem = None + args.licence_pem_file.__enter__.return_value.read.return_value = \ + mock.sentinel.licence_pem_file + mock_licence = mock.Mock() + self.mock_app.client_manager.coriolis.licensing.register = mock_licence + + result = self.licence.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_licence.assert_called_once_with( + args.appliance_id, mock.sentinel.licence_pem_file) + mock_get_formatted_entity.assert_called_once_with( + mock_licence.return_value) + + def test_take_action_value_error(self): + args = mock.MagicMock() + args.licence_pem = None + args.licence_pem_file = None + mock_licence = mock.Mock() + self.mock_app.client_manager.coriolis.licensing.register = mock_licence + + self.assertRaises( + ValueError, + self.licence.take_action, + args + ) + + mock_licence.assert_not_called() + + +class LicenceListTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Licence List.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(LicenceListTestCase, self).setUp() + self.licence = licensing.LicenceList( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.licence.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing.LicenceFormatter, 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + args.appliance_id = mock.sentinel.appliance_id + mock_lic_list = mock.Mock() + self.mock_app.client_manager.coriolis.licensing.list = \ + mock_lic_list + + result = self.licence.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_lic_list.assert_called_once_with(mock.sentinel.appliance_id) + mock_list_objects.assert_called_once_with(mock_lic_list.return_value) + + +class LicenceShowTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Licence Show.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(LicenceShowTestCase, self).setUp() + self.licence = licensing.LicenceShow( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.licence.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing.LicenceFormatter, 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.appliance_id = mock.sentinel.appliance_id + args.licence_id = mock.sentinel.licence_id + licence = mock.Mock() + self.mock_app.client_manager.coriolis.licensing.show = licence + + result = self.licence.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + licence.assert_called_once_with(args.appliance_id, args.licence_id) + mock_get_formatted_entity.assert_called_once_with(licence.return_value) + + +class LicenceDeleteTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Licence Delete.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(LicenceDeleteTestCase, self).setUp() + self.licence = licensing.LicenceDelete( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.licence.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.appliance_id = mock.sentinel.appliance_id + args.licence_id = mock.sentinel.licence_id + licence = mock.Mock() + self.mock_app.client_manager.coriolis.licensing.delete = licence + + self.licence.take_action(args) + + licence.assert_called_once_with(args.appliance_id, args.licence_id) diff --git a/coriolisclient/tests/cli/test_licensing_appliances.py b/coriolisclient/tests/cli/test_licensing_appliances.py new file mode 100644 index 0000000..4d3490f --- /dev/null +++ b/coriolisclient/tests/cli/test_licensing_appliances.py @@ -0,0 +1,177 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from cliff import lister +from cliff import show + +from coriolisclient.cli import licensing_appliances +from coriolisclient.tests import test_base + + +class ApplianceFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Appliance Formatter.""" + + def setUp(self): + super(ApplianceFormatterTestCase, self).setUp() + self.appliance = licensing_appliances.ApplianceFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.id = "app1" + obj2.id = "app2" + obj3.id = "app3" + obj_list = [obj2, obj1, obj3] + + result = self.appliance._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + + result = self.appliance._get_formatted_data(obj) + + self.assertEqual( + [mock.sentinel.id], + result + ) + + +class ApplianceListTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Appliance List.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ApplianceListTestCase, self).setUp() + self.appliance = licensing_appliances.ApplianceList( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.appliance.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing_appliances.ApplianceFormatter, + 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + mock_licensing_appliances = mock.Mock() + self.mock_app.client_manager.coriolis.licensing_appliances = \ + mock_licensing_appliances + + result = self.appliance.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_list_objects.assert_called_once_with( + mock_licensing_appliances.list.return_value) + + +class ApplianceShowTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Appliance Show.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ApplianceShowTestCase, self).setUp() + self.appliance = licensing_appliances.ApplianceShow( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.appliance.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing_appliances.ApplianceFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.appliance_id = mock.sentinel.appliance_id + mock_licensing_appliances = mock.Mock() + self.mock_app.client_manager.coriolis.licensing_appliances = \ + mock_licensing_appliances + + result = self.appliance.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_licensing_appliances.show.assert_called_once_with( + mock.sentinel.appliance_id) + mock_get_formatted_entity.assert_called_once_with( + mock_licensing_appliances.show.return_value) + + +class ApplianceCreateTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Appliance Create.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ApplianceCreateTestCase, self).setUp() + self.appliance = licensing_appliances.ApplianceCreate( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.appliance.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing_appliances.ApplianceFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + mock_licensing_appliances = mock.Mock() + self.mock_app.client_manager.coriolis.licensing_appliances = \ + mock_licensing_appliances + + result = self.appliance.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_get_formatted_entity.assert_called_once_with( + mock_licensing_appliances.create.return_value) diff --git a/coriolisclient/tests/cli/test_licensing_reservations.py b/coriolisclient/tests/cli/test_licensing_reservations.py new file mode 100644 index 0000000..705c79f --- /dev/null +++ b/coriolisclient/tests/cli/test_licensing_reservations.py @@ -0,0 +1,190 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from cliff import lister +from cliff import show + +from coriolisclient.cli import licensing_reservations +from coriolisclient.tests import test_base + + +class ReservationFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Reservation Formatter.""" + + def setUp(self): + super(ReservationFormatterTestCase, self).setUp() + self.reservation = licensing_reservations.ReservationFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.created_at = "date1" + obj2.created_at = "date2" + obj3.created_at = "date3" + obj_list = [obj2, obj1, obj3] + + result = self.reservation._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.appliance_id = mock.sentinel.appliance_id + obj.licence_id = mock.sentinel.licence_id + obj.type = mock.sentinel.type + obj.count = mock.sentinel.count + obj.created_at = mock.sentinel.created_at + + result = self.reservation._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.appliance_id, + mock.sentinel.licence_id, + mock.sentinel.type, + mock.sentinel.count, + mock.sentinel.created_at + ), + result + ) + + +class ReservationListTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Reservation List.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ReservationListTestCase, self).setUp() + self.reservation = licensing_reservations.ReservationList( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.reservation.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing_reservations.ReservationFormatter, + 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + mock_licensing_reservations = mock.Mock() + self.mock_app.client_manager.coriolis.licensing_reservations = \ + mock_licensing_reservations + + result = self.reservation.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_list_objects.assert_called_once_with( + mock_licensing_reservations.list.return_value) + + +class ReservationShowTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Reservation Show.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ReservationShowTestCase, self).setUp() + self.reservation = licensing_reservations.ReservationShow( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.reservation.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing_reservations.ReservationFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.appliance_id = mock.sentinel.appliance_id + args.reservation_id = mock.sentinel.reservation_id + mock_licensing_reservations = mock.Mock() + self.mock_app.client_manager.coriolis.licensing_reservations = \ + mock_licensing_reservations + + result = self.reservation.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_licensing_reservations.show.assert_called_once_with( + args.appliance_id, args.reservation_id) + mock_get_formatted_entity.assert_called_once_with( + mock_licensing_reservations.show.return_value) + + +class ReservationRefreshTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Reservation Refresh.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ReservationRefreshTestCase, self).setUp() + self.reservation = licensing_reservations.ReservationRefresh( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.reservation.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing_reservations.ReservationFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + mock_licensing_reservations = mock.Mock() + self.mock_app.client_manager.coriolis.licensing_reservations = \ + mock_licensing_reservations + + result = self.reservation.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_get_formatted_entity.assert_called_once_with( + mock_licensing_reservations.refresh.return_value) diff --git a/coriolisclient/tests/cli/test_licensing_server.py b/coriolisclient/tests/cli/test_licensing_server.py new file mode 100644 index 0000000..f2f177b --- /dev/null +++ b/coriolisclient/tests/cli/test_licensing_server.py @@ -0,0 +1,96 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from cliff import show + +from coriolisclient.cli import licensing_server +from coriolisclient.tests import test_base + + +class ServerStatusFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Server Formatter.""" + + def setUp(self): + super(ServerStatusFormatterTestCase, self).setUp() + self.server = licensing_server.ServerStatusFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.hostname = "host1" + obj2.hostname = "host2" + obj3.hostname = "host3" + obj_list = [obj2, obj1, obj3] + + result = self.server._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.hostname = mock.sentinel.hostname + obj.multi_appliance = mock.sentinel.multi_appliance + obj.supported_licence_versions = \ + mock.sentinel.supported_licence_versions + obj.server_local_time = mock.sentinel.server_local_time + + result = self.server._get_formatted_data(obj) + + self.assertEqual( + [ + mock.sentinel.hostname, + mock.sentinel.multi_appliance, + mock.sentinel.supported_licence_versions, + mock.sentinel.server_local_time, + ], + result + ) + + +class ServerStatusTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Server Status.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ServerStatusTestCase, self).setUp() + self.server = licensing_server.ServerStatus( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.server.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(licensing_server.ServerStatusFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + mock_licensing_server = mock.Mock() + self.mock_app.client_manager.coriolis.licensing_server = \ + mock_licensing_server + + result = self.server.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_get_formatted_entity.assert_called_once_with( + mock_licensing_server.status.return_value) diff --git a/coriolisclient/tests/cli/test_logging.py b/coriolisclient/tests/cli/test_logging.py new file mode 100644 index 0000000..68d2546 --- /dev/null +++ b/coriolisclient/tests/cli/test_logging.py @@ -0,0 +1,163 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from cliff import command +from cliff import lister + +from coriolisclient.cli import logging +from coriolisclient.tests import test_base + + +class LogsFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Logs Formatter.""" + + def setUp(self): + super(LogsFormatterTestCase, self).setUp() + self.logger = logging.LogsFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.log_name = "log1" + obj2.log_name = "log2" + obj3.log_name = "log3" + obj_list = [obj2, obj1, obj3] + + result = self.logger._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.log_name = mock.sentinel.log_name + + result = self.logger._get_formatted_data(obj) + + self.assertEqual( + (mock.sentinel.log_name,), + result + ) + + +class ListCoriolisLogsTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Coriolis Logs.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListCoriolisLogsTestCase, self).setUp() + self.logger = logging.ListCoriolisLogs( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.logger.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(logging.LogsFormatter, 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + mock_logging = mock.Mock() + self.mock_app.client_manager.coriolis.logging.list = mock_logging + + result = self.logger.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_list_objects.assert_called_once_with(mock_logging.return_value) + + +class DownloadCoriolisLogTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Download Coriolis Log.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DownloadCoriolisLogTestCase, self).setUp() + self.logger = logging.DownloadCoriolisLog( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.logger.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.log_name = mock.sentinel.log_name + args.out_file = mock.sentinel.out_file + args.start_time = mock.sentinel.start_time + args.end_time = mock.sentinel.end_time + mock_logging = mock.Mock() + self.mock_app.client_manager.coriolis.logging.get = mock_logging + + self.logger.take_action(args) + + mock_logging.assert_called_once_with( + mock.sentinel.log_name, + mock.sentinel.out_file, + start_time=mock.sentinel.start_time, + end_time=mock.sentinel.end_time + ) + + +class StreamCoriolisLogTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Stream Coriolis Log.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(StreamCoriolisLogTestCase, self).setUp() + self.logger = logging.StreamCoriolisLog( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.logger.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.log_name = mock.sentinel.log_name + args.severity = "INFO" + mock_logging = mock.Mock() + self.mock_app.client_manager.coriolis.logging.stream = mock_logging + + self.logger.take_action(args) + + mock_logging.assert_called_once_with( + app_name=mock.sentinel.log_name, + severity=6 + ) diff --git a/coriolisclient/tests/cli/test_minion_pools.py b/coriolisclient/tests/cli/test_minion_pools.py new file mode 100644 index 0000000..f302dbe --- /dev/null +++ b/coriolisclient/tests/cli/test_minion_pools.py @@ -0,0 +1,653 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import os +from unittest import mock + +from cliff import command +from cliff import lister +from cliff import show + +from coriolisclient.cli import minion_pools +from coriolisclient.cli import utils as cli_utils +from coriolisclient.tests import test_base + + +class MinionPoolFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Minion Pool Formatter.""" + + def setUp(self): + super(MinionPoolFormatterTestCase, self).setUp() + self.minion = minion_pools.MinionPoolFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.created_at = "date1" + obj2.created_at = "date2" + obj3.created_at = "date3" + obj_list = [obj2, obj1, obj3] + + result = self.minion._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.name = mock.sentinel.name + obj.endpoint_id = mock.sentinel.endpoint_id + obj.platform = mock.sentinel.platform + obj.os_type = mock.sentinel.os_type + obj.notes = mock.sentinel.notes + obj.status = mock.sentinel.status + obj.minimum_minions = 1 + obj.maximum_minions = 2 + + result = self.minion._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.name, + mock.sentinel.endpoint_id, + mock.sentinel.platform, + mock.sentinel.os_type, + mock.sentinel.notes, + mock.sentinel.status, + "1 - 2", + ), + result + ) + + +class MinionPoolDetailFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Minion Pool Detail Formatter.""" + + def setUp(self): + super(MinionPoolDetailFormatterTestCase, self).setUp() + self.minion = minion_pools.MinionPoolDetailFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.created_at = "date1" + obj2.created_at = "date2" + obj3.created_at = "date3" + obj_list = [obj2, obj1, obj3] + + result = self.minion._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_format_pool_event(self): + event = { + "level": "mock_level", + "created_at": "mock_date", + "message": "mock_message", + } + expected_result = "mock_level mock_date mock_message" + + result = self.minion._format_pool_event(event) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + '_format_pool_event') + def test_format_pool_events(self, mock_format_pool_event): + event1 = { + "level": "mock_level1", + "created_at": "mock_date1", + "message": "mock_message1", + "index": 1 + } + event2 = { + "level": "mock_level2", + "created_at": "mock_date2", + "message": "mock_message2", + "index": 2 + } + event3 = { + "level": "mock_level3", + "created_at": "mock_date3", + "message": "mock_message3", + "index": 3 + } + ret_event1 = "mock_level1 mock_date1 mock_message1" + ret_event2 = "mock_level2 mock_date2 mock_message2" + ret_event3 = "mock_level3 mock_date3 mock_message3" + events = [event3, event1, event2] + mock_format_pool_event.side_effect = [ + ret_event1, ret_event2, ret_event3] + expected_result = ( + "mock_level1 mock_date1 mock_message1%(ls)s" + "mock_level2 mock_date2 mock_message2%(ls)s" + "mock_level3 mock_date3 mock_message3" % {"ls": os.linesep}) + + result = self.minion._format_pool_events(events) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + '_format_progress_update') + def test__format_progress_updates(self, mock_format_progress_update): + update1 = {"created_at": "date2", "message2": "message2"} + update2 = {"created_at": "date3", "message3": "message3"} + update3 = {"index": 2, "created_at": "date1"} + ret_update1 = "date2 [10] message2" + ret_update2 = "date3 [20] message3" + ret_update3 = "date1 [30] None" + progress_updates = [update3, update1, update2] + mock_format_progress_update.side_effect = [ + ret_update1, ret_update2, ret_update3] + expected_result = ( + 'date2 [10] message2%(ls)sdate3 [20] message3%(ls)s' + 'date1 [30] None' % {"ls": os.linesep} + ) + + result = self.minion._format_progress_updates(progress_updates) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + '_format_pool_events') + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + '_format_progress_updates') + @mock.patch.object(cli_utils, 'format_json_for_object_property') + def test_get_formatted_data( + self, + mock_format_json_for_object_property, + mock_format_progress_updates, + mock_format_pool_events + ): + mock_obj = mock.Mock() + mock_obj.id = mock.sentinel.id + mock_obj.name = mock.sentinel.name + mock_obj.endpoint_id = mock.sentinel.endpoint_id + mock_obj.platform = mock.sentinel.platform + mock_obj.os_type = mock.sentinel.os_type + mock_obj.status = mock.sentinel.status + mock_obj.notes = mock.sentinel.notes + mock_obj.created_at = mock.sentinel.created_at + mock_obj.updated_at = mock.sentinel.updated_at + mock_obj.maintenance_trust_id = mock.sentinel.maintenance_trust_id + mock_obj.minimum_minions = mock.sentinel.minimum_minions + mock_obj.maximum_minions = mock.sentinel.maximum_minions + mock_obj.minion_max_idle_time = mock.sentinel.minion_max_idle_time + mock_obj.minion_retention_strategy = \ + mock.sentinel.minion_retention_strategy + mock_format_json_for_object_property.side_effect = [ + mock.sentinel.environment_options, + mock.sentinel.shared_resources, + mock.sentinel.minion_machines, + ] + mock_format_pool_events.return_value = \ + mock.sentinel.formatted_pool_events + mock_format_progress_updates.return_value = \ + mock.sentinel.formatted_progress_updates + mock_obj.info = mock.sentinel.info + expected_result = ( + mock.sentinel.id, + mock.sentinel.name, + mock.sentinel.endpoint_id, + mock.sentinel.platform, + mock.sentinel.os_type, + mock.sentinel.status, + mock.sentinel.notes, + mock.sentinel.created_at, + mock.sentinel.updated_at, + mock.sentinel.maintenance_trust_id, + mock.sentinel.minimum_minions, + mock.sentinel.maximum_minions, + mock.sentinel.minion_max_idle_time, + mock.sentinel.minion_retention_strategy, + mock.sentinel.environment_options, + mock.sentinel.shared_resources, + mock.sentinel.formatted_pool_events, + mock.sentinel.formatted_progress_updates, + mock.sentinel.minion_machines, + ) + + result = self.minion._get_formatted_data(mock_obj) + + self.assertEqual( + expected_result, + result + ) + + +class CreateMinionPoolTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Create Minion Pool.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(CreateMinionPoolTestCase, self).setUp() + self.minion = minion_pools.CreateMinionPool( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.minion.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, "environment-options") + + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_get_formatted_entity + ): + args = mock.Mock() + args.name = mock.sentinel.name + args.platform = mock.sentinel.platform + args.os_type = mock.sentinel.os_type + args.minimum_minions = mock.sentinel.minimum_minions + args.maximum_minions = mock.sentinel.maximum_minions + args.minion_max_idle_time = mock.sentinel.minion_max_idle_time + args.minion_retention_strategy = \ + mock.sentinel.minion_retention_strategy + args.notes = mock.sentinel.notes + args.skip_allocation = mock.sentinel.skip_allocation + args.endpoint_id = mock.sentinel.endpoint_id + mock_endpoints = mock.Mock() + mock_endpoints.get_endpoint_id_for_name.return_value = \ + mock.sentinel.endpoint_id + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_minion_pool = mock.Mock() + self.mock_app.client_manager.coriolis.minion_pools = \ + mock_minion_pool + + result = self.minion.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment-options', error_on_no_value=True) + mock_endpoints.get_endpoint_id_for_name.assert_called_once_with( + mock.sentinel.endpoint_id) + mock_minion_pool.create.assert_called_once_with( + mock.sentinel.name, + mock.sentinel.endpoint_id, + mock.sentinel.platform, + mock.sentinel.os_type, + mock_get_option_value_from_args.return_value, + minimum_minions=mock.sentinel.minimum_minions, + maximum_minions=mock.sentinel.maximum_minions, + minion_max_idle_time=mock.sentinel.minion_max_idle_time, + minion_retention_strategy=mock.sentinel.minion_retention_strategy, + notes=mock.sentinel.notes, + skip_allocation=mock.sentinel.skip_allocation + ) + mock_get_formatted_entity.assert_called_once_with( + mock_minion_pool.create.return_value) + + +class UpdateMinionPoolTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Update Minion Pool.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(UpdateMinionPoolTestCase, self).setUp() + self.minion = minion_pools.UpdateMinionPool( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_args_for_json_option_to_parser + ): + result = self.minion.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_args_for_json_option_to_parser.assert_called_once_with( + mock_get_parser.return_value, "environment-options") + + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + args.name = mock.sentinel.name + args.os_type = mock.sentinel.os_type + args.minimum_minions = mock.sentinel.minimum_minions + args.maximum_minions = mock.sentinel.maximum_minions + args.minion_max_idle_time = mock.sentinel.minion_max_idle_time + args.minion_retention_strategy = \ + mock.sentinel.minion_retention_strategy + args.notes = mock.sentinel.notes + args.environment_options = mock.sentinel.environment_options + mock_minion_pool = mock.Mock() + self.mock_app.client_manager.coriolis.minion_pools = \ + mock_minion_pool + mock_get_option_value_from_args.return_value = \ + mock.sentinel.environment_options + expected_updated_values = { + "name": mock.sentinel.name, + "os_type": mock.sentinel.os_type, + "minimum_minions": mock.sentinel.minimum_minions, + "maximum_minions": mock.sentinel.maximum_minions, + "minion_max_idle_time": mock.sentinel.minion_max_idle_time, + "minion_retention_strategy": + mock.sentinel.minion_retention_strategy, + "notes": mock.sentinel.notes, + "environment_options": mock.sentinel.environment_options + } + + result = self.minion.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_get_option_value_from_args.assert_called_once_with( + args, 'environment-options', error_on_no_value=False) + mock_minion_pool.update.assert_called_once_with( + mock.sentinel.id, + expected_updated_values + ) + mock_get_formatted_entity.assert_called_once_with( + mock_minion_pool.update.return_value) + + +class ShowMinionPoolTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Show Minion Pool.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ShowMinionPoolTestCase, self).setUp() + self.minion = minion_pools.ShowMinionPool( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + ): + result = self.minion.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + mock_minion_pool = mock.Mock() + self.mock_app.client_manager.coriolis.minion_pools = \ + mock_minion_pool + + result = self.minion.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_minion_pool.get.assert_called_once_with(mock.sentinel.id) + mock_get_formatted_entity.assert_called_once_with( + mock_minion_pool.get.return_value) + + +class DeleteMinionPoolTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Delete Minion Pool.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DeleteMinionPoolTestCase, self).setUp() + self.minion = minion_pools.DeleteMinionPool( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + ): + result = self.minion.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.id = mock.sentinel.id + mock_minion_pool = mock.Mock() + self.mock_app.client_manager.coriolis.minion_pools = \ + mock_minion_pool + + self.minion.take_action(args) + + mock_minion_pool.delete.assert_called_once_with(mock.sentinel.id) + + +class ListMinionPoolsTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis List Minion Pools.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListMinionPoolsTestCase, self).setUp() + self.minion = minion_pools.ListMinionPools( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + ): + result = self.minion.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(minion_pools.MinionPoolFormatter, + 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + mock_minion_pool = mock.Mock() + self.mock_app.client_manager.coriolis.minion_pools = \ + mock_minion_pool + + result = self.minion.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_list_objects.assert_called_once_with( + mock_minion_pool.list.return_value) + + +class AllocateMinionPoolTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Allocate Minion Pool.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(AllocateMinionPoolTestCase, self).setUp() + self.minion = minion_pools.AllocateMinionPool( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + ): + result = self.minion.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + mock_minion_pool = mock.Mock() + self.mock_app.client_manager.coriolis.minion_pools = \ + mock_minion_pool + + result = self.minion.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_minion_pool.allocate_minion_pool.assert_called_once_with( + mock.sentinel.id) + mock_get_formatted_entity.assert_called_once_with( + mock_minion_pool.allocate_minion_pool.return_value) + + +class RefreshMinionPoolTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Refresh Minion Pool.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(RefreshMinionPoolTestCase, self).setUp() + self.minion = minion_pools.RefreshMinionPool( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + ): + result = self.minion.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + mock_minion_pool = mock.Mock() + self.mock_app.client_manager.coriolis.minion_pools = \ + mock_minion_pool + + result = self.minion.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_minion_pool.refresh_minion_pool.assert_called_once_with( + mock.sentinel.id) + mock_get_formatted_entity.assert_called_once_with( + mock_minion_pool.refresh_minion_pool.return_value) + + +class DeallocateMinionPoolTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Deallocate Minion Pool.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DeallocateMinionPoolTestCase, self).setUp() + self.minion = minion_pools.DeallocateMinionPool( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + ): + result = self.minion.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(minion_pools.MinionPoolDetailFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + args.force = mock.sentinel.force + mock_minion_pool = mock.Mock() + self.mock_app.client_manager.coriolis.minion_pools = \ + mock_minion_pool + + result = self.minion.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_minion_pool.deallocate_minion_pool.assert_called_once_with( + mock.sentinel.id, force=mock.sentinel.force) + mock_get_formatted_entity.assert_called_once_with( + mock_minion_pool.deallocate_minion_pool.return_value) diff --git a/coriolisclient/tests/cli/test_providers.py b/coriolisclient/tests/cli/test_providers.py new file mode 100644 index 0000000..d0c8445 --- /dev/null +++ b/coriolisclient/tests/cli/test_providers.py @@ -0,0 +1,181 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from cliff import lister + +from coriolisclient.cli import providers +from coriolisclient.tests import test_base + + +class ProvidersFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Providers Formatter.""" + + def setUp(self): + super(ProvidersFormatterTestCase, self).setUp() + self.provider = providers.ProvidersFormatter() + + def test_get_sorted_list(self): + obj1 = {"name": "name1"} + obj2 = {"name": "name2"} + obj3 = {"name": "name3"} + obj_list = [obj2, obj1, obj3] + + result = self.provider._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = { + "name": mock.sentinel.name, + "types": [1, 2, -1] + } + + with self.assertLogs(providers.LOG, level="DEBUG"): + result = self.provider._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.name, + "%s, %s" % ( + providers.PROVIDERS_TYPE_FEATURE_MAP[1], + providers.PROVIDERS_TYPE_FEATURE_MAP[2]) + ), + result + ) + + +class ProviderSchemasFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Providers Formatter.""" + + def setUp(self): + super(ProviderSchemasFormatterTestCase, self).setUp() + self.provider = providers.ProviderSchemasFormatter() + + def test_get_formatted_data(self): + obj = { + "type": mock.sentinel.type, + "schema": { + "connection_info_schema": {"info": "mock_info"}, + "required": False + } + } + + result = self.provider._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.type, + ('{\n' + ' "connection_info_schema": {\n' + ' "info": "mock_info"\n' + ' },\n' + ' "required": false\n}') + ), + result + ) + + +class ListProviderTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis List Provider.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListProviderTestCase, self).setUp() + self.provider = providers.ListProvider( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + ): + result = self.provider.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(providers.ProvidersFormatter, 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + mock_provider = mock.Mock() + self.mock_app.client_manager.coriolis.providers = mock_provider + + result = self.provider.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_list_objects.assert_called_once_with( + mock_provider.list.return_value.providers_list) + + +class ListProviderSchemasTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis List Provider Schemas.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListProviderSchemasTestCase, self).setUp() + self.provider = providers.ListProviderSchemas( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + ): + result = self.provider.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(providers.ProviderSchemasFormatter, 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + args.type = "connection" + args.platform = mock.sentinel.platform + mock_provider = mock.Mock() + self.mock_app.client_manager.coriolis.providers = mock_provider + + result = self.provider.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_provider.schemas_list.assert_called_once_with( + mock.sentinel.platform, + providers.PROVIDER_SCHEMA_TYPE_MAP["connection"] + ) + mock_obj = mock_provider.schemas_list.return_value.provider_schemas + mock_list_objects(mock_obj.providers_list) + + def test_take_action_value_error(self): + args = mock.Mock() + args.type = "invalid" + mock_provider = mock.Mock() + self.mock_app.client_manager.coriolis.providers = mock_provider + + self.assertRaises( + ValueError, + self.provider.take_action, + args + ) + mock_provider.schemas_list.assert_not_called() diff --git a/coriolisclient/tests/cli/test_regions.py b/coriolisclient/tests/cli/test_regions.py new file mode 100644 index 0000000..b062d46 --- /dev/null +++ b/coriolisclient/tests/cli/test_regions.py @@ -0,0 +1,301 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from cliff import command +from cliff import lister +from cliff import show + +from coriolisclient.cli import regions +from coriolisclient.tests import test_base + + +class RegionsTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Regions.""" + + def test_add_region_enablement_args_to_parser(self): + parser = mock.Mock() + + regions._add_region_enablement_args_to_parser(parser) + + parser.add_mutually_exclusive_group.assert_called_once() + + +class RegionFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Regions Formatter.""" + + def setUp(self): + super(RegionFormatterTestCase, self).setUp() + self.region = regions.RegionFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.created_at = "date1" + obj2.created_at = "date2" + obj3.created_at = "date3" + obj_list = [obj2, obj1, obj3] + + result = self.region._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.name = mock.sentinel.name + obj.enabled = mock.sentinel.enabled + obj.description = mock.sentinel.description + obj.mapped_endpoints = mock.sentinel.mapped_endpoints + obj.mapped_services = mock.sentinel.mapped_services + obj.created_at = mock.sentinel.created_at + obj.updated_at = mock.sentinel.updated_at + + result = self.region._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.name, + mock.sentinel.enabled, + mock.sentinel.description, + mock.sentinel.mapped_endpoints, + mock.sentinel.mapped_services, + mock.sentinel.created_at, + mock.sentinel.updated_at + ), + result + ) + + +class CreateRegionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Create Region.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(CreateRegionTestCase, self).setUp() + self.region = regions.CreateRegion( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(regions, '_add_region_enablement_args_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_region_enablement_args_to_parser + ): + result = self.region.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_region_enablement_args_to_parser.assert_called_once_with( + mock_get_parser.return_value) + + @mock.patch.object(regions.RegionFormatter, 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.name = mock.sentinel.name + args.description = mock.sentinel.description + args.enabled = True + mock_regions = mock.Mock() + self.mock_app.client_manager.coriolis.regions = mock_regions + + result = self.region.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_regions.create.assert_called_once_with( + mock.sentinel.name, + description=mock.sentinel.description, + enabled=True + ) + mock_get_formatted_entity.assert_called_once_with( + mock_regions.create.return_value) + + +class UpdateRegionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Update Region.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(UpdateRegionTestCase, self).setUp() + self.region = regions.UpdateRegion( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(regions, '_add_region_enablement_args_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_region_enablement_args_to_parser + ): + result = self.region.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_region_enablement_args_to_parser.assert_called_once_with( + mock_get_parser.return_value) + + @mock.patch.object(regions.RegionFormatter, 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + args.enabled = True + args.name = mock.sentinel.name + args.description = mock.sentinel.description + mock_regions = mock.Mock() + self.mock_app.client_manager.coriolis.regions = mock_regions + expected_updated_values = { + "enabled": True, + "name": mock.sentinel.name, + "description": mock.sentinel.description, + } + + result = self.region.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_regions.update.assert_called_once_with( + mock.sentinel.id, + expected_updated_values + ) + mock_get_formatted_entity.assert_called_once_with( + mock_regions.update.return_value) + + +class ShowRegionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Show Region.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ShowRegionTestCase, self).setUp() + self.region = regions.ShowRegion( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.region.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(regions.RegionFormatter, 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + mock_regions = mock.Mock() + self.mock_app.client_manager.coriolis.regions = mock_regions + + result = self.region.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_regions.get.assert_called_once_with(mock.sentinel.id) + mock_get_formatted_entity.assert_called_once_with( + mock_regions.get.return_value) + + +class DeleteRegionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Delete Region.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DeleteRegionTestCase, self).setUp() + self.region = regions.DeleteRegion( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.region.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.id = mock.sentinel.id + mock_regions = mock.Mock() + self.mock_app.client_manager.coriolis.regions = mock_regions + + self.region.take_action(args) + + mock_regions.delete.assert_called_once_with(mock.sentinel.id) + + +class ListRegionsTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis List Regions.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListRegionsTestCase, self).setUp() + self.region = regions.ListRegions( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.region.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(regions.RegionFormatter, 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + mock_regions = mock.Mock() + self.mock_app.client_manager.coriolis.regions = mock_regions + + result = self.region.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_list_objects.assert_called_once_with( + mock_regions.list.return_value) diff --git a/coriolisclient/tests/cli/test_services.py b/coriolisclient/tests/cli/test_services.py new file mode 100644 index 0000000..9186897 --- /dev/null +++ b/coriolisclient/tests/cli/test_services.py @@ -0,0 +1,305 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from cliff import command +from cliff import lister +from cliff import show + +from coriolisclient.cli import services +from coriolisclient.tests import test_base + + +class ServicesTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Services.""" + + def test_add_service_enablement_args_to_parser(self): + parser = mock.Mock() + + services._add_service_enablement_args_to_parser(parser) + + parser.add_mutually_exclusive_group.assert_called_once() + + +class ServiceFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Services Formatter.""" + + def setUp(self): + super(ServiceFormatterTestCase, self).setUp() + self.service = services.ServiceFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.created_at = "date1" + obj2.created_at = "date2" + obj3.created_at = "date3" + obj_list = [obj2, obj1, obj3] + + result = self.service._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.host = mock.sentinel.host + obj.binary = mock.sentinel.binary + obj.topic = mock.sentinel.topic + obj.enabled = mock.sentinel.enabled + obj.status = mock.sentinel.status + obj.mapped_regions = mock.sentinel.mapped_regions + obj.created_at = mock.sentinel.created_at + obj.updated_at = mock.sentinel.updated_at + + result = self.service._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.host, + mock.sentinel.binary, + mock.sentinel.topic, + mock.sentinel.enabled, + mock.sentinel.status, + mock.sentinel.mapped_regions, + mock.sentinel.created_at, + mock.sentinel.updated_at + ), + result + ) + + +class CreateServiceTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Create Service.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(CreateServiceTestCase, self).setUp() + self.service = services.CreateService( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(services, '_add_service_enablement_args_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_service_enablement_args_to_parser + ): + result = self.service.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_service_enablement_args_to_parser.assert_called_once_with( + mock_get_parser.return_value) + + @mock.patch.object(services.ServiceFormatter, 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.host = mock.sentinel.host + args.binary = mock.sentinel.binary + args.topic = mock.sentinel.topic + args.enabled = True + args.regions = mock.sentinel.regions + mock_services = mock.Mock() + self.mock_app.client_manager.coriolis.services = mock_services + + result = self.service.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_services.create.assert_called_once_with( + mock.sentinel.host, + mock.sentinel.binary, + topic=mock.sentinel.topic, + enabled=True, + regions=mock.sentinel.regions + ) + mock_get_formatted_entity.assert_called_once_with( + mock_services.create.return_value) + + +class UpdateServiceTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Update Service.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(UpdateServiceTestCase, self).setUp() + self.service = services.UpdateService( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(services, '_add_service_enablement_args_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + mock_add_service_enablement_args_to_parser + ): + result = self.service.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + mock_add_service_enablement_args_to_parser.assert_called_once_with( + mock_get_parser.return_value) + + @mock.patch.object(services.ServiceFormatter, 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + args.enabled = True + args.regions = mock.sentinel.regions + mock_services = mock.Mock() + self.mock_app.client_manager.coriolis.services = mock_services + expected_updated_values = { + "enabled": True, + "mapped_regions": mock.sentinel.regions, + } + + result = self.service.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_services.update.assert_called_once_with( + mock.sentinel.id, + expected_updated_values + ) + mock_get_formatted_entity.assert_called_once_with( + mock_services.update.return_value) + + +class ShowServiceTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Show Service.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ShowServiceTestCase, self).setUp() + self.service = services.ShowService( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.service.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(services.ServiceFormatter, 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + mock_services = mock.Mock() + self.mock_app.client_manager.coriolis.services = mock_services + + result = self.service.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_services.get.assert_called_once_with(mock.sentinel.id) + mock_get_formatted_entity.assert_called_once_with( + mock_services.get.return_value) + + +class DeleteServiceTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Delete Service.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DeleteServiceTestCase, self).setUp() + self.service = services.DeleteService( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.service.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.id = mock.sentinel.id + mock_services = mock.Mock() + self.mock_app.client_manager.coriolis.services = mock_services + + self.service.take_action(args) + + mock_services.delete.assert_called_once_with(mock.sentinel.id) + + +class ListServicesTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis List Services.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListServicesTestCase, self).setUp() + self.service = services.ListServices( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.service.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(services.ServiceFormatter, 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + mock_services = mock.Mock() + self.mock_app.client_manager.coriolis.services = mock_services + + result = self.service.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_list_objects.assert_called_once_with( + mock_services.list.return_value) diff --git a/coriolisclient/tests/cli/test_shell.py b/coriolisclient/tests/cli/test_shell.py new file mode 100644 index 0000000..196808e --- /dev/null +++ b/coriolisclient/tests/cli/test_shell.py @@ -0,0 +1,438 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import ddt +import logging +import os +from unittest import mock + +from cliff import app +from keystoneauth1 import loading +from keystoneauth1 import session + +from coriolisclient.cli import shell +from coriolisclient import client +from coriolisclient import exceptions +from coriolisclient.tests import test_base + + +class CustomMock(mock.MagicMock): + def __getattr__(self, name): + return None + + +@ddt.ddt +class CoriolisTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Shell App.""" + + def setUp(self): + super(CoriolisTestCase, self).setUp() + self.coriolis = shell.Coriolis() + + @ddt.file_data('data/shell_check_auth_arguments.yml') + @ddt.unpack + def test_check_auth_arguments(self, config): + args = CustomMock() + if config.get("args"): + for key, value in config.get("args").items(): + setattr(args, key, value) + expected_result = config.get("expected_result") + + if expected_result is not None: + result = self.coriolis.check_auth_arguments( + args, + api_version=config.get("api_version", None), + raise_exc=config.get("raise_exc", False) + ) + + self.assertEqual( + expected_result, + result + ) + else: + self.assertRaises( + exceptions.CoriolisException, + self.coriolis.check_auth_arguments, + args, + api_version=config.get("api_version", None), + raise_exc=config.get("raise_exc", False) + ) + + @ddt.file_data('data/shell_create_keystone_auth.yml') + @ddt.unpack + @mock.patch.object(session, 'Session') + def test_create_keystone_session( + self, + mock_Session, + config + ): + args = CustomMock() + if config.get("args"): + for key, value in config.get("args").items(): + setattr(args, key, value) + kwargs = config.get("kwargs", {}) + expected_kwargs = config.get("expected_kwargs", {}) + mock_stderr = mock.Mock() + self.coriolis.stderr = mock_stderr + + auth_fun = config['auth_fun'] + with mock.patch(auth_fun) as mock_auth: + result = self.coriolis.create_keystone_session( + args, + api_version=config.get("api_version", None), + kwargs_dict=kwargs, + auth_type=config.get("auth_type", None), + verify=config.get("verify", True) + ) + + self.assertEqual( + mock_Session.return_value, + result + ) + mock_Session.assert_called_once_with( + auth=mock_auth.return_value, verify=config.get("verify", True)) + mock_auth.assert_called_once_with(**expected_kwargs) + + @ddt.data( + { + "args": { + "no_auth": "mock_no_auth", + "os_auth_url": "mock_os_auth_url" + } + }, + { + "args": { + "no_auth": "mock_no_auth", + "endpoint": "mock_endpoint" + } + }, + { + "args": { + "no_auth": "mock_no_auth", + "endpoint": "mock_endpoint", + "os_tenant_id": "mock_os_tenant_id" + }, + "expected_client_kwargs": { + "endpoint": "mock_endpoint", + "project_id": "mock_os_tenant_id", + } + }, + { + "args": { + "os_auth_token": "mock_os_auth_token", + "os_auth_url": "mock_os_auth_url", + "os_tenant_id": "mock_os_tenant_id", + "endpoint": "mock_endpoint", + "region_name": "mock_region_name" + }, + "expected_client_kwargs": { + "endpoint": "mock_endpoint", + "region_name": "mock_region_name" + }, + "expected_ks_session_kwargs": { + "auth_url": "mock_os_auth_url", + "token": "mock_os_auth_token" + }, + "auth_type": "token" + }, + { + "args": { + "os_auth_token": "mock_os_auth_token", + "os_auth_url": "mock_os_auth_url", + "os_tenant_id": "mock_os_tenant_id", + "endpoint": "mock_endpoint", + "region_name": "mock_region_name", + "insecure": True + }, + "expected_client_kwargs": { + "endpoint": "mock_endpoint", + "region_name": "mock_region_name" + }, + "expected_ks_session_kwargs": { + "auth_url": "mock_os_auth_url", + "token": "mock_os_auth_token" + }, + "auth_type": "token", + "verify": False + }, + { + "args": { + "os_auth_token": "mock_os_auth_token", + "os_auth_url": "mock_os_auth_url", + "os_tenant_id": "mock_os_tenant_id", + "endpoint": "mock_endpoint", + "region_name": "mock_region_name", + "insecure": True, + "os_cacert": "mock_os_cacert" + }, + "expected_client_kwargs": { + "endpoint": "mock_endpoint", + "region_name": "mock_region_name" + }, + "expected_ks_session_kwargs": { + "auth_url": "mock_os_auth_url", + "token": "mock_os_auth_token" + }, + "auth_type": "token", + "verify": "mock_os_cacert" + }, + { + "args": { + "os_auth_token": "mock_os_auth_token" + } + }, + { + "args": { + "os_auth_token": "mock_os_auth_token", + "os_auth_url": "mock_os_auth_url", + "os_tenant_id": "mock_os_tenant_id", + "endpoint": "mock_endpoint" + }, + "expected_client_kwargs": { + "endpoint": "mock_endpoint" + }, + "expected_ks_session_kwargs": { + "auth_url": "mock_os_auth_url", + "token": "mock_os_auth_token" + }, + "auth_type": "token" + }, + { + "args": { + "os_auth_url": "mock_os_auth_url", + "os_password": "mock_os_password", + "os_user_id": "mock_os_user_id", + "os_username": "mock_os_username", + "endpoint": "mock_endpoint" + }, + "expected_client_kwargs": { + "endpoint": "mock_endpoint" + }, + "expected_ks_session_kwargs": { + "auth_url": "mock_os_auth_url", + "password": "mock_os_password", + "user_id": "mock_os_user_id", + "username": "mock_os_username", + }, + "auth_type": "password" + }, + {} + ) + @mock.patch.object(shell.Coriolis, 'create_keystone_session') + @mock.patch.object(client, 'Client') + def test_create_client( + self, + data, + mock_Client, + mock_create_ks_session + ): + args = CustomMock() + args.os_identity_api_version = mock.sentinel.os_identity_api_version + for key, value in data.get("args", {}).items(): + setattr(args, key, value) + expected_client_kwargs = data.get("expected_client_kwargs") + expected_ks_session_kwargs = data.get("expected_ks_session_kwargs", {}) + + if expected_client_kwargs is not None: + result = self.coriolis.create_client(args) + self.assertEqual( + result, mock_Client.return_value) + if expected_ks_session_kwargs: + mock_Client.assert_called_once_with( + session=mock_create_ks_session.return_value, + **expected_client_kwargs, + verify=data.get("verify", True)) + mock_create_ks_session.assert_called_once_with( + args, + mock.sentinel.os_identity_api_version, + expected_ks_session_kwargs, + auth_type=data["auth_type"], + verify=data.get("verify", True)) + else: + mock_Client.assert_called_once_with( + **expected_client_kwargs, + verify=data.get("verify", True)) + else: + self.assertRaises( + exceptions.CoriolisException, + self.coriolis.create_client, + args + ) + + @ddt.data( + { + "args": { + "os_project_id": "mock_project_id", + "os_tenant_name": "mock_tenant_name" + }, + "api_version": 3, + "expected_kwargs": { + "tenant_name": "mock_tenant_name" + } + }, + { + "args": { + "os_project_id": "mock_project_id", + "os_tenant_name": "mock_tenant_name" + }, + "api_version": 2, + "expected_kwargs": { + "tenant_name": "mock_tenant_name" + } + }, + { + "args": { + "os_project_id": "mock_project_id", + "os_tenant_name": "mock_tenant_name" + }, + "api_version": None, + "expected_kwargs": { + "project_id": "mock_project_id" + } + }, + { + "args": {}, + "api_version": None, + "expected_kwargs": {} + } + ) + def test_build_kwargs_based_on_version(self, data): + args = CustomMock() + for key, value in data.get("args", {}).items(): + setattr(args, key, value) + api_version = data["api_version"] + + result = self.coriolis.build_kwargs_based_on_version( + args, api_version=api_version) + + self.assertEqual( + data["expected_kwargs"], + result + ) + + def test_get_endpoint_filter_kwargs( + self + ): + args = mock.Mock() + args.interface = mock.sentinel.interface + args.service_type = mock.sentinel.service_type + args.service_name = mock.sentinel.service_name + args.coriolis_api_version = mock.sentinel.coriolis_api_version + args.region_name = mock.sentinel.region_name + expected_result = { + "interface": mock.sentinel.interface, + "service_type": mock.sentinel.service_type, + "service_name": mock.sentinel.service_name, + "region_name": mock.sentinel.region_name, + "version": mock.sentinel.coriolis_api_version, + } + + result = self.coriolis._get_endpoint_filter_kwargs(args) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(shell.Coriolis, '_env') + @mock.patch.object(loading, 'register_session_argparse_arguments') + @mock.patch.object(app.App, 'build_option_parser') + def test_build_option_parser( + self, + mock_build_option_parser, + mock_register_session_argparse_arguments, + *_ + ): + result = self.coriolis.build_option_parser( + mock.sentinel.description, mock.sentinel.version) + + self.assertEqual( + mock_build_option_parser.return_value, + result + ) + mock_build_option_parser.assert_called_once_with( + mock.sentinel.description, mock.sentinel.version, None) + mock_register_session_argparse_arguments.assert_called_once_with( + mock_build_option_parser.return_value) + + def test_env(self): + os.environ["TEST_ENV"] = "test_value" + result = self.coriolis._env( + "TEST_ENV", mock.sentinel.default) + + self.assertEqual( + "test_value", + result + ) + + @mock.patch.object(shell.Coriolis, 'create_client') + def test_prepare_to_run_command(self, mock_create_client): + cmd = mock.Mock() + cmd.auth_required = True + self.coriolis.options = mock.sentinel.options + + self.coriolis.prepare_to_run_command(cmd) + + self.assertEqual( + mock_create_client.return_value, + self.coriolis.client_manager.coriolis + ) + mock_create_client.assert_called_once_with(mock.sentinel.options) + + @mock.patch.object(app.App, 'run') + def test_run(self, mock_run): + mock_stderr = mock.Mock() + mock_parser = mock.Mock() + self.coriolis.stderr = mock_stderr + self.coriolis.parser = mock_parser + + result = self.coriolis.run(None) + + self.assertEqual( + 1, + result + ) + mock_stderr.write.assert_called_once_with( + mock_parser.format_usage.return_value) + mock_run.assert_not_called() + + mock_stderr.reset_mock() + mock_parser.reset_mock() + + result = self.coriolis.run(mock.sentinel.argv) + + self.assertEqual( + mock_run.return_value, + result + ) + mock_parser.format_usage.assert_not_called() + + +class ShellTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Shell.""" + + def test_setup_logging(self): + shell._setup_logging() + self.assertEqual( + ( + logging.WARNING, + logging.ERROR + ), + ( + logging.getLogger("requests").level, + logging.getLogger("keystoneclient").level + ) + ) + + @mock.patch.object(shell.Coriolis, 'run') + def test_main( + self, + mock_run + ): + result = shell.main(mock.sentinel.argv) + + self.assertEqual( + mock_run.return_value, + result + ) + mock_run.assert_called_once_with(mock.sentinel.argv) diff --git a/coriolisclient/tests/cli/test_transfer_executions.py b/coriolisclient/tests/cli/test_transfer_executions.py new file mode 100644 index 0000000..1f3dc97 --- /dev/null +++ b/coriolisclient/tests/cli/test_transfer_executions.py @@ -0,0 +1,440 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import os +from unittest import mock + +from cliff import command +from cliff import lister +from cliff import show + +from coriolisclient.cli import formatter +from coriolisclient.cli import transfer_executions +from coriolisclient.tests import test_base + + +class TransferExecutionFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Replic Execution Formatter.""" + + def setUp(self): + super(TransferExecutionFormatterTestCase, self).setUp() + self.transfer = transfer_executions.TransferExecutionFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.created_at = "date1" + obj2.created_at = "date2" + obj3.created_at = "date3" + obj_list = [obj2, obj1, obj3] + + result = self.transfer._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.action_id = mock.sentinel.action_id + obj.id = mock.sentinel.id + obj.status = mock.sentinel.status + obj.created_at = mock.sentinel.created_at + + result = self.transfer._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.action_id, + mock.sentinel.id, + mock.sentinel.status, + mock.sentinel.created_at + ), + result + ) + + +class TransferExecutionDetailFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Transfer Execution Detail Formatter.""" + + def setUp(self): + super(TransferExecutionDetailFormatterTestCase, self).setUp() + self.transfer = transfer_executions.TransferExecutionDetailFormatter() + + def test_format_instances(self): + obj = mock.Mock() + task1 = mock.Mock() + task1.instance = "mock_instance1" + task2 = mock.Mock() + task2.instance = "mock_instance2" + obj.tasks = [task1, task2] + expected_result = ( + 'mock_instance1%(ls)smock_instance2' + % {"ls": os.linesep} + ) + + result = self.transfer._format_instances(obj) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(formatter.EntityFormatter, '_format_progress_update') + def test_format_progress_updates(self, mock_format_progress_update): + update1 = {"created_at": "date2", "message2": "message2"} + update2 = {"created_at": "date3", "message3": "message3"} + update3 = {"index": 2, "created_at": "date1"} + ret_update1 = "date2 [10] message2" + ret_update2 = "date3 [20] message3" + ret_update3 = "date1 [30] None" + task_dict = {"progress_updates": [update3, update1, update2]} + mock_format_progress_update.side_effect = [ + ret_update1, ret_update2, ret_update3] + expected_result = ( + 'date2 [10] message2%(ls)sdate3 [20] message3%(ls)s' + 'date1 [30] None' % {"ls": os.linesep} + ) + + result = self.transfer._format_progress_updates(task_dict) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(transfer_executions.TransferExecutionDetailFormatter, + '_format_progress_updates') + def test_format_task(self, mock_format_progress_updates): + mock_task = mock.Mock() + task = { + "depends_on": ["mock_dep1", "mock_dep2"], + "id": mock.sentinel.id, + "task_type": mock.sentinel.task_type, + "instance": mock.sentinel.instance, + "status": mock.sentinel.status, + "exception_details": mock.sentinel.exception_details + } + mock_task.to_dict.return_value = task + mock_format_progress_updates.return_value = ( + 'date2 [10] message2%(ls)sdate3 [20] message3%(ls)s' + 'date1 [30] None' % {"ls": os.linesep} + ) + expected_result = ( + 'id: sentinel.id%(ls)s' + 'task_type: sentinel.task_type%(ls)s' + 'instance: sentinel.instance%(ls)s' + 'status: sentinel.status%(ls)s' + 'depends_on: mock_dep1, mock_dep2%(ls)s' + 'exception_details: sentinel.exception_details%(ls)s' + 'progress_updates:%(ls)s' + 'date2 [10] message2%(ls)s' + 'date3 [20] message3%(ls)s' + 'date1 [30] None' % {"ls": os.linesep}) + + result = self.transfer._format_task(mock_task) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(transfer_executions.TransferExecutionDetailFormatter, + '_format_progress_updates') + def test_format_task_no_progress_updates( + self, mock_format_progress_updates): + mock_task = mock.Mock() + task = { + "depends_on": ["mock_dep1", "mock_dep2"], + "id": mock.sentinel.id, + "task_type": mock.sentinel.task_type, + "instance": mock.sentinel.instance, + "status": mock.sentinel.status, + "exception_details": mock.sentinel.exception_details + } + mock_task.to_dict.return_value = task + mock_format_progress_updates.return_value = None + expected_result = ( + 'id: sentinel.id%(ls)s' + 'task_type: sentinel.task_type%(ls)s' + 'instance: sentinel.instance%(ls)s' + 'status: sentinel.status%(ls)s' + 'depends_on: mock_dep1, mock_dep2%(ls)s' + 'exception_details: sentinel.exception_details%(ls)s' + 'progress_updates:' % {"ls": os.linesep} + ) + + result = self.transfer._format_task(mock_task) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(transfer_executions.TransferExecutionDetailFormatter, + '_format_task') + def test_format_tasks(self, mock_format_task): + obj = mock.Mock() + obj.tasks = [mock.sentinel.task1, mock.sentinel.task2] + mock_format_task.side_effect = ["task1", "task2"] + expected_result = 'task1%(ls)s%(ls)stask2' % {"ls": os.linesep} + + result = self.transfer._format_tasks(obj) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(transfer_executions.TransferExecutionDetailFormatter, + '_format_tasks') + @mock.patch.object(transfer_executions.TransferExecutionDetailFormatter, + '_format_instances') + def test_get_formatted_data( + self, + mock_format_instances, + mock_format_tasks + ): + mock_obj = mock.Mock() + obj = { + "storage_mappings": {'default_storage': mock.sentinel.storage} + } + mock_obj.to_dict.return_value = obj + mock_obj.id = mock.sentinel.id + mock_obj.action_id = mock.sentinel.action_id + mock_obj.status = mock.sentinel.status + mock_obj.created_at = mock.sentinel.created_at + mock_obj.updated_at = mock.sentinel.updated_at + mock_format_instances.return_value = mock.sentinel.formatted_instances + mock_format_tasks.return_value = mock.sentinel.formatted_tasks + expected_result = ( + mock.sentinel.id, + mock.sentinel.action_id, + mock.sentinel.status, + mock.sentinel.created_at, + mock.sentinel.updated_at, + mock.sentinel.formatted_instances, + mock.sentinel.formatted_tasks, + ) + + result = self.transfer._get_formatted_data(mock_obj) + + self.assertEqual( + expected_result, + result + ) + + +class CreateTransferExecutionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Create Transfer Execution.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(CreateTransferExecutionTestCase, self).setUp() + self.transfer = transfer_executions.CreateTransferExecution( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfer_executions.TransferExecutionDetailFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + args.shutdown_instances = mock.sentinel.shutdown_instances + args.auto_deploy = mock.sentinel.auto_deploy + mock_execution = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_executions.create = \ + mock_execution + + result = self.transfer.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_execution.assert_called_once_with( + mock.sentinel.transfer, mock.sentinel.shutdown_instances, + mock.sentinel.auto_deploy) + mock_get_formatted_entity.assert_called_once_with( + mock_execution.return_value) + + +class ShowTransferExecutionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Show Transfer Execution.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ShowTransferExecutionTestCase, self).setUp() + self.transfer = transfer_executions.ShowTransferExecution( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfer_executions.TransferExecutionDetailFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + args.id = mock.sentinel.id + execution = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_executions.get = \ + execution + + result = self.transfer.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + execution.assert_called_once_with(args.transfer, args.id) + mock_get_formatted_entity.assert_called_once_with( + execution.return_value) + + +class CancelTransferExecutionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Cancel Transfer Execution.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(CancelTransferExecutionTestCase, self).setUp() + self.transfer = transfer_executions.CancelTransferExecution( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + args.id = mock.sentinel.id + args.force = False + transfer_execution = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_executions = \ + transfer_execution + + self.transfer.take_action(args) + + transfer_execution.cancel.assert_called_once_with( + mock.sentinel.transfer, mock.sentinel.id, False) + + +class DeleteTransferExecutionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Delete Transfer Execution.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DeleteTransferExecutionTestCase, self).setUp() + self.transfer = transfer_executions.DeleteTransferExecution( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + args.id = mock.sentinel.id + transfer_execution = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_executions = \ + transfer_execution + + self.transfer.take_action(args) + + transfer_execution.delete.assert_called_once_with( + mock.sentinel.transfer, mock.sentinel.id) + + +class ListTransferExecutionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Transfer Execution.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListTransferExecutionTestCase, self).setUp() + self.transfer = transfer_executions.ListTransferExecution( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfer_executions.TransferExecutionFormatter, + 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + mock_transfer_list = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_executions.list = \ + mock_transfer_list + + result = self.transfer.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_transfer_list.assert_called_once_with(mock.sentinel.transfer) + mock_list_objects.assert_called_once_with( + mock_transfer_list.return_value) diff --git a/coriolisclient/tests/cli/test_transfer_schedules.py b/coriolisclient/tests/cli/test_transfer_schedules.py new file mode 100644 index 0000000..2bf301f --- /dev/null +++ b/coriolisclient/tests/cli/test_transfer_schedules.py @@ -0,0 +1,502 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import argparse +import datetime +from unittest import mock + +from cliff import command +from cliff import lister +from cliff import show + +from coriolisclient.cli import transfer_schedules +from coriolisclient import exceptions +from coriolisclient.tests import test_base + + +class RangeActionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Transfer Range Action.""" + + def setUp(self): + super(RangeActionTestCase, self).setUp() + self.range_action = transfer_schedules.RangeAction( + 1, 10, ["mock_option"], "dest") + + def test__call__(self): + mock_parser = mock.Mock() + mock_namespace = mock.Mock() + value = 5 + + self.range_action(mock_parser, mock_namespace, value) + + self.assertEqual( + value, + mock_namespace.dest + ) + + def test__call__argument_error(self): + mock_parser = mock.Mock() + mock_namespace = mock.Mock() + value = -1 + + self.assertRaises( + argparse.ArgumentError, + self.range_action, + mock_parser, + mock_namespace, + value + ) + + +class TransferScheduleFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Transfer Schedule Formatter.""" + + def setUp(self): + super(TransferScheduleFormatterTestCase, self).setUp() + self.transfer_schedules = ( + transfer_schedules.TransferScheduleFormatter()) + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.created_at = "date1" + obj2.created_at = "date2" + obj3.created_at = "date3" + obj_list = [obj2, obj1, obj3] + + result = self.transfer_schedules._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.transfer_id = mock.sentinel.transfer_id + obj.id = mock.sentinel.id + obj.schedule = mock.sentinel.schedule + obj.created_at = mock.sentinel.created_at + obj.expiration_date = mock.sentinel.expiration_date + + result = self.transfer_schedules._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.transfer_id, + mock.sentinel.id, + mock.sentinel.schedule, + mock.sentinel.created_at, + mock.sentinel.expiration_date + ), + result + ) + + +class TransferScheduleDetailFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Transfer Schedule Detail Formatter.""" + + def setUp(self): + super(TransferScheduleDetailFormatterTestCase, self).setUp() + self.transfer_schedules = ( + transfer_schedules.TransferScheduleDetailFormatter()) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.transfer_id = mock.sentinel.transfer_id + obj.schedule = mock.sentinel.schedule + obj.created_at = mock.sentinel.created_at + obj.updated_at = mock.sentinel.updated_at + obj.enabled = False + obj.expiration_date = mock.sentinel.expiration_date + obj.shutdown_instance = mock.sentinel.shutdown_instance + obj.auto_deploy = mock.sentinel.auto_deploy + + result = self.transfer_schedules._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.transfer_id, + mock.sentinel.schedule, + mock.sentinel.created_at, + mock.sentinel.updated_at, + False, + mock.sentinel.expiration_date, + mock.sentinel.shutdown_instance, + mock.sentinel.auto_deploy, + ), + result + ) + + +class CreateTransferScheduleTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Create Transfer Schedule.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(CreateTransferScheduleTestCase, self).setUp() + self.transfer_schedules = transfer_schedules.CreateTransferSchedule( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer_schedules.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfer_schedules.TransferScheduleDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(transfer_schedules, '_parse_expiration_date') + @mock.patch.object(transfer_schedules, '_parse_schedule_group_args') + def test_take_action( + self, + mock_parse_schedule_group_args, + mock_parse_expiration_date, + mock_get_formatted_entity + ): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + args.disabled = False + args.shutdown_instance = False + args.auto_deploy = False + mock_schedule_group_args = {"minute": mock.sentinel.minute} + mock_parse_schedule_group_args.return_value = mock_schedule_group_args + mock_schedule = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_schedules.create = \ + mock_schedule + + result = self.transfer_schedules.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_schedule.assert_called_once_with( + mock.sentinel.transfer, + mock_schedule_group_args, + True, + mock_parse_expiration_date.return_value, + False, + False, + ) + mock_get_formatted_entity.assert_called_once_with( + mock_schedule.return_value) + + @mock.patch.object(transfer_schedules, '_parse_expiration_date') + @mock.patch.object(transfer_schedules, '_parse_schedule_group_args') + def test_take_action_no_parsed_schedule( + self, + mock_parse_schedule_group_args, + mock_parse_expiration_date + ): + args = mock.Mock() + mock_parse_schedule_group_args.return_value = {} + + self.assertRaises( + exceptions.CoriolisException, + self.transfer_schedules.take_action, + args + ) + mock_parse_expiration_date.assert_not_called() + + +class ShowTransferScheduleTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Show Transfer Schedule.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ShowTransferScheduleTestCase, self).setUp() + self.transfer_schedules = transfer_schedules.ShowTransferSchedule( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer_schedules.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfer_schedules.TransferScheduleDetailFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity + ): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + args.id = mock.sentinel.id + schedule = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_schedules.get = \ + schedule + + result = self.transfer_schedules.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + schedule.assert_called_once_with(args.transfer, args.id) + mock_get_formatted_entity.assert_called_once_with( + schedule.return_value) + + +class UpdateTransferScheduleTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Update Transfer Schedule.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(UpdateTransferScheduleTestCase, self).setUp() + self.transfer_schedules = transfer_schedules.UpdateTransferSchedule( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer_schedules.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfer_schedules.TransferScheduleDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(transfer_schedules, '_parse_expiration_date') + @mock.patch.object(transfer_schedules, '_parse_schedule_group_args') + def test_take_action( + self, + mock_parse_schedule_group_args, + mock_parse_expiration_date, + mock_get_formatted_entity + ): + args = mock.Mock() + args.expires = True + args.shutdown = True + args.enabled = True + args.transfer = mock.sentinel.transfer + args.id = mock.sentinel.id + args.auto_deploy = False + mock_parse_schedule_group_args.return_value = \ + {"minute": mock.sentinel.minute} + transfer_schedule = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_schedules = \ + transfer_schedule + expected_updated_values = { + "schedule": {"minute": mock.sentinel.minute}, + "expiration_date": mock_parse_expiration_date.return_value, + "shutdown_instance": True, + "enabled": True, + "auto_deploy": False, + } + + result = self.transfer_schedules.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + + transfer_schedule.update.assert_called_once_with( + mock.sentinel.transfer, + mock.sentinel.id, + expected_updated_values + ) + + @mock.patch.object(transfer_schedules.TransferScheduleDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(transfer_schedules, '_parse_expiration_date') + @mock.patch.object(transfer_schedules, '_parse_schedule_group_args') + def test_take_action_no_updated_values( + self, + mock_parse_schedule_group_args, + mock_parse_expiration_date, + mock_get_formatted_entity + ): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + args.id = mock.sentinel.id + args.expires = False + args.shutdown = None + args.enabled = None + args.auto_deploy = None + mock_parse_schedule_group_args.return_value = {} + transfer_schedule = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_schedules = \ + transfer_schedule + expected_updated_values = {"expiration_date": None} + + result = self.transfer_schedules.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + + mock_parse_expiration_date.assert_not_called() + transfer_schedule.update.assert_called_once_with( + mock.sentinel.transfer, + mock.sentinel.id, + expected_updated_values + ) + + +class DeleteTransferScheduleTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Delete Transfer Schedule.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DeleteTransferScheduleTestCase, self).setUp() + self.transfer_schedules = transfer_schedules.DeleteTransferSchedule( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer_schedules.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + args.id = mock.sentinel.id + transfer_schedule = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_schedules = \ + transfer_schedule + + self.transfer_schedules.take_action(args) + + transfer_schedule.delete.assert_called_once_with( + mock.sentinel.transfer, mock.sentinel.id) + + +class ListTransferScheduleTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Transfer Schedule.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListTransferScheduleTestCase, self).setUp() + self.transfer_schedules = transfer_schedules.ListTransferSchedule( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser( + self, + mock_get_parser + ): + result = self.transfer_schedules.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfer_schedules.TransferScheduleFormatter, + 'list_objects') + def test_take_action( + self, + mock_list_objects + ): + args = mock.Mock() + args.transfer = mock.sentinel.transfer + args.hide_expired = mock.sentinel.hide_expired + mock_transfer_list = mock.Mock() + self.mock_app.client_manager.coriolis.transfer_schedules.list = \ + mock_transfer_list + + result = self.transfer_schedules.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_transfer_list.assert_called_once_with( + mock.sentinel.transfer, hide_expired=mock.sentinel.hide_expired) + mock_list_objects.assert_called_once_with( + mock_transfer_list.return_value) + + +class TransferSchedulesTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Transfer Schedules.""" + + def test_add_schedule_group(self): + parser = argparse.ArgumentParser() + + transfer_schedules._add_schedule_group(parser) + + self.assertIn( + "Schedule", + [g.title for g in parser._action_groups] + ) + + def test_parse_schedule_group_args(self): + class CustomMock(mock.MagicMock): + def __getattr__(self, name): + return None + args = CustomMock() + args.minute = mock.sentinel.minute + args.hour = mock.sentinel.hour + expected_result = { + "minute": mock.sentinel.minute, + "hour": mock.sentinel.hour + } + + result = transfer_schedules._parse_schedule_group_args(args) + + self.assertEqual( + expected_result, + result + ) + + def test_parse_expiration_date(self): + value = None + + result = transfer_schedules._parse_expiration_date(value) + + self.assertEqual( + None, + result + ) + + value = "2099-12-31" + + result = transfer_schedules._parse_expiration_date(value) + + self.assertEqual( + datetime.datetime(2099, 12, 31, 0, 0), + result + ) + + value = "2099-31-12" + + self.assertRaises( + exceptions.CoriolisException, + transfer_schedules._parse_expiration_date, + value + ) diff --git a/coriolisclient/tests/cli/test_transfers.py b/coriolisclient/tests/cli/test_transfers.py new file mode 100644 index 0000000..9a49423 --- /dev/null +++ b/coriolisclient/tests/cli/test_transfers.py @@ -0,0 +1,602 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from cliff import command +from cliff import lister +from cliff import show + +from coriolisclient.cli import transfer_executions +from coriolisclient.cli import transfers +from coriolisclient.cli import utils as cli_utils +from coriolisclient.tests import test_base + + +class TransferFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Transfer Formatter.""" + + def setUp(self): + super(TransferFormatterTestCase, self).setUp() + self.transfer = transfers.TransferFormatter() + + def test_get_sorted_list(self): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.created_at = "date1" + obj2.created_at = "date2" + obj3.created_at = "date3" + obj_list = [obj2, obj1, obj3] + + result = self.transfer._get_sorted_list(obj_list) + + self.assertEqual( + [obj1, obj2, obj3], + result + ) + + def test_format_last_execution(self): + obj = mock.Mock() + obj.executions = None + + result = self.transfer._format_last_execution(obj) + + self.assertEqual( + "", + result + ) + + execution1 = mock.Mock() + execution2 = mock.Mock() + execution3 = mock.Mock() + execution1.created_at = "date1" + execution2.created_at = "date2" + execution3.created_at = "date3" + execution1.to_dict.return_value = { + "id": "mock_id1", + "status": "mock_status1" + } + execution2.to_dict.return_value = { + "id": "mock_id2", + "status": "mock_status2" + } + execution3.to_dict.return_value = { + "id": "mock_id3", + "status": "mock_status3" + } + obj.executions = [execution1, execution3, execution2] + + result = self.transfer._format_last_execution(obj) + + self.assertEqual( + "mock_id3 mock_status3", + result + ) + + def test_get_formatted_data(self): + obj = mock.Mock() + obj.id = mock.sentinel.id + obj.last_execution_status = mock.sentinel.last_execution_status + obj.instances = ["mock_instance3", "mock_instance1", "mock_instance2"] + obj.notes = mock.sentinel.notes + obj.scenario = mock.sentinel.scenario + obj.created_at = mock.sentinel.created_at + + result = self.transfer._get_formatted_data(obj) + + self.assertEqual( + ( + mock.sentinel.id, + mock.sentinel.scenario, + ('mock_instance3%(ls)smock_instance1%(ls)smock_instance2' + % {"ls": "\n"}), + mock.sentinel.notes, + mock.sentinel.last_execution_status, + mock.sentinel.created_at + ), + result + ) + + +class TransferDetailFormatterTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Transfer Detail Formatter.""" + + def setUp(self): + super(TransferDetailFormatterTestCase, self).setUp() + self.transfer = transfers.TransferDetailFormatter( + show_instances_data=True) + + def test_format_instances(self): + obj = mock.Mock() + obj.instances = ["mock_instance3", "mock_instance1", "mock_instance2"] + expected_result = ( + 'mock_instance1%(ls)smock_instance2%(ls)smock_instance3' + % {"ls": "\n"} + ) + + result = self.transfer._format_instances(obj) + + self.assertEqual( + expected_result, + result + ) + + def test_format_execution(self): + execution = mock.Mock() + execution.to_dict.return_value = { + "id": "mock_id", + "status": "mock_status" + } + expected_result = "mock_id mock_status" + + result = self.transfer._format_execution(execution) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(transfers.TransferDetailFormatter, '_format_execution') + def test_format_executions(self, mock_format_execution): + executions = mock.Mock() + execution1 = mock.Mock() + execution2 = mock.Mock() + execution3 = mock.Mock() + execution1.created_at = "date1" + execution2.created_at = "date2" + execution3.created_at = "date3" + ret_execution1 = "mock_id1 mock_status1" + ret_execution2 = "mock_id2 mock_status2" + ret_execution3 = "mock_id3 mock_status3" + mock_format_execution.side_effect = [ + ret_execution1, ret_execution2, ret_execution3] + executions = [execution1, execution3, execution2] + expected_result = ( + "mock_id1 mock_status1%(ls)s" + "mock_id2 mock_status2%(ls)s" + "mock_id3 mock_status3" % {"ls": "\n"} + ) + + result = self.transfer._format_executions(executions) + + self.assertEqual( + expected_result, + result + ) + + @mock.patch.object(transfers.TransferDetailFormatter, '_format_executions') + @mock.patch.object(cli_utils, 'format_mapping') + @mock.patch.object(cli_utils, 'format_json_for_object_property') + @mock.patch.object(transfers.TransferDetailFormatter, + '_format_instances') + @mock.patch.object(cli_utils, 'parse_storage_mappings') + def test_get_formatted_data( + self, + mock_parse_storage_mappings, + mock_format_instances, + mock_format_json_for_object_property, + mock_format_mapping, + mock_format_executions + ): + mock_obj = mock.Mock() + obj = { + "storage_mappings": {'default_storage': mock.sentinel.storage} + } + mock_obj.to_dict.return_value = obj + mock_obj.id = mock.sentinel.id + mock_obj.created_at = mock.sentinel.created_at + mock_obj.updated_at = mock.sentinel.updated_at + mock_obj.reservation_id = mock.sentinel.reservation_id + mock_format_instances.return_value = mock.sentinel.formatted_instances + mock_obj.notes = mock.sentinel.notes + mock_obj.origin_endpoint_id = mock.sentinel.origin_endpoint_id + mock_obj.origin_minion_pool_id = mock.sentinel.origin_minion_pool_id + mock_obj.destination_endpoint_id = \ + mock.sentinel.destination_endpoint_id + mock_obj.destination_minion_pool_id = \ + mock.sentinel.destination_minion_pool_id + mock_parse_storage_mappings.return_value = ( + mock.sentinel.default_storage, + mock.sentinel.backend_mappings, + mock.sentinel.disk_mappings + ) + mock_format_json_for_object_property.side_effect = [ + mock.sentinel.instance_osmorphing_minion_pool_mappings, + mock.sentinel.destination_environment, + mock.sentinel.source_environment, + mock.sentinel.network_map, + mock.sentinel.user_scripts + ] + mock_format_mapping.side_effect = [ + mock.sentinel.disk_mapping, mock.sentinel.backend_mappings] + mock_format_executions.return_value = \ + mock.sentinel.formatted_executions + mock_obj.info = mock.sentinel.info + mock_obj.scenario = mock.sentinel.scenario + mock_obj.clone_disks = mock.sentinel.clone_disks + mock_obj.skip_os_morphing = mock.sentinel.skip_os_morphing + expected_result = [ + mock.sentinel.id, + mock.sentinel.created_at, + mock.sentinel.updated_at, + mock.sentinel.scenario, + mock.sentinel.reservation_id, + mock.sentinel.formatted_instances, + mock.sentinel.notes, + mock.sentinel.origin_endpoint_id, + mock.sentinel.origin_minion_pool_id, + mock.sentinel.destination_endpoint_id, + mock.sentinel.destination_minion_pool_id, + mock.sentinel.instance_osmorphing_minion_pool_mappings, + mock.sentinel.destination_environment, + mock.sentinel.source_environment, + mock.sentinel.network_map, + mock.sentinel.disk_mapping, + mock.sentinel.backend_mappings, + mock.sentinel.default_storage, + mock.sentinel.user_scripts, + mock.sentinel.clone_disks, + mock.sentinel.skip_os_morphing, + mock.sentinel.formatted_executions, + mock.sentinel.info, + ] + + result = self.transfer._get_formatted_data(mock_obj) + + self.assertEqual( + expected_result, + result + ) + + +class CreateTransferTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Create Transfer.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(CreateTransferTestCase, self).setUp() + self.transfer = transfers.CreateTransfer( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(cli_utils, 'add_args_for_json_option_to_parser') + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser( + self, + mock_get_parser, + *_ + ): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(cli_utils, 'compose_user_scripts') + @mock.patch.object(cli_utils, 'get_storage_mappings_dict_from_args') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + @mock.patch.object(transfers.TransferDetailFormatter, + 'get_formatted_entity') + def test_take_action( + self, + mock_get_formatted_entity, + mock_get_option_value_from_args, + mock_get_storage_mappings_dict_from_args, + mock_compose_user_scripts + ): + args = mock.Mock() + args.instances = mock.sentinel.instances + args.notes = mock.sentinel.notes + args.scenario = mock.sentinel.scenario + args.origin_minion_pool_id = mock.sentinel.origin_minion_pool_id + args.destination_minion_pool_id = \ + mock.sentinel.destination_minion_pool_id + args.instance_osmorphing_minion_pool_mappings = [ + {'instance_id': "instance_id1", 'pool_id': "pool_id1"}, + {'instance_id': "instance_id2", 'pool_id': "pool_id2"} + ] + args.clone_disks = True + args.skip_os_morphing = False + mock_endpoints = mock.Mock() + mock_transfers = mock.Mock() + self.mock_app.client_manager.coriolis.endpoints = mock_endpoints + mock_endpoints.get_endpoint_id_for_name.side_effect = [ + mock.sentinel.origin_endpoint_id, + mock.sentinel.destination_endpoint_id + ] + self.mock_app.client_manager.coriolis.transfers.create = \ + mock_transfers + mock_get_option_value_from_args.side_effect = [ + mock.sentinel.destination_environment, + mock.sentinel.source_environment, + mock.sentinel.network_map, + ] + + result = self.transfer.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_transfers.assert_called_once_with( + mock.sentinel.origin_endpoint_id, + mock.sentinel.destination_endpoint_id, + mock.sentinel.source_environment, + mock.sentinel.destination_environment, + mock.sentinel.instances, + mock.sentinel.scenario, + network_map=mock.sentinel.network_map, + notes=mock.sentinel.notes, + storage_mappings=(mock_get_storage_mappings_dict_from_args. + return_value), + origin_minion_pool_id=mock.sentinel.origin_minion_pool_id, + destination_minion_pool_id= + mock.sentinel.destination_minion_pool_id, + instance_osmorphing_minion_pool_mappings={ + 'instance_id1': 'pool_id1', 'instance_id2': 'pool_id2'}, + user_scripts=mock_compose_user_scripts.return_value, + clone_disks=True, skip_os_morphing=False, + ) + mock_get_formatted_entity.assert_called_once_with( + mock_transfers.return_value) + + +class ShowTransferTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Show Transfer.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ShowTransferTestCase, self).setUp() + self.transfer = transfers.ShowTransfer( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser(self, mock_get_parser): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfers.TransferDetailFormatter, + 'get_formatted_entity') + def test_take_action(self, mock_get_formatted_entity): + args = mock.Mock() + args.id = mock.sentinel.id + mock_transfer = mock.Mock() + self.mock_app.client_manager.coriolis.transfers.get = mock_transfer + + result = self.transfer.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_transfer.assert_called_once_with(mock.sentinel.id) + mock_get_formatted_entity.assert_called_once_with( + mock_transfer.return_value) + + +class DeleteTransferTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Delete Transfer.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DeleteTransferTestCase, self).setUp() + self.transfer = transfers.DeleteTransfer( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser(self, mock_get_parser): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + def test_take_action(self): + args = mock.Mock() + args.id = mock.sentinel.id + mock_transfer = mock.Mock() + self.mock_app.client_manager.coriolis.transfers.delete = mock_transfer + + self.transfer.take_action(args) + + mock_transfer.assert_called_once_with(mock.sentinel.id) + + +class DeleteTransferDisksTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Delete Transfer Disks.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(DeleteTransferDisksTestCase, self).setUp() + self.transfer = transfers.DeleteTransferDisks( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(command.Command, 'get_parser') + def test_get_parser(self, mock_get_parser): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfer_executions.TransferExecutionDetailFormatter, + 'get_formatted_entity') + def test_take_action(self, mock_get_formatted_entity): + args = mock.Mock() + args.id = mock.sentinel.id + mock_transfer = mock.Mock() + self.mock_app.client_manager.coriolis.transfers.delete_disks = \ + mock_transfer + + result = self.transfer.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_transfer.assert_called_once_with(mock.sentinel.id) + mock_get_formatted_entity.assert_called_once_with( + mock_transfer.return_value) + + +class ListTransferTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client List Transfer.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(ListTransferTestCase, self).setUp() + self.transfer = transfers.ListTransfer( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(lister.Lister, 'get_parser') + def test_get_parser(self, mock_get_parser): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfers.TransferFormatter, 'list_objects') + def test_take_action(self, mock_list_objects): + args = mock.Mock() + mock_transfer = mock.Mock() + self.mock_app.client_manager.coriolis.transfers.list = mock_transfer + + result = self.transfer.take_action(args) + + self.assertEqual( + mock_list_objects.return_value, + result + ) + mock_list_objects.assert_called_once_with(mock_transfer.return_value) + + +class UpdateTransferTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client Delete Transfer Disks.""" + + def setUp(self): + self.mock_app = mock.Mock() + super(UpdateTransferTestCase, self).setUp() + self.transfer = transfers.UpdateTransfer( + self.mock_app, mock.sentinel.app_args) + + @mock.patch.object(show.ShowOne, 'get_parser') + def test_get_parser(self, mock_get_parser): + result = self.transfer.get_parser(mock.sentinel.prog_name) + + self.assertEqual( + mock_get_parser.return_value, + result + ) + mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) + + @mock.patch.object(transfer_executions.TransferExecutionDetailFormatter, + 'get_formatted_entity') + @mock.patch.object(cli_utils, 'compose_user_scripts') + @mock.patch.object(cli_utils, 'get_storage_mappings_dict_from_args') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action( + self, + mock_get_option_value_from_args, + mock_get_storage_mappings_dict_from_args, + mock_compose_user_scripts, + mock_get_formatted_entity + ): + args = mock.Mock() + args.id = mock.sentinel.id + args.global_scripts = mock.sentinel.global_scripts + args.instance_scripts = mock.sentinel.instance_scripts + args.notes = mock.sentinel.notes + args.origin_minion_pool_id = mock.sentinel.origin_minion_pool_id + args.destination_minion_pool_id = \ + mock.sentinel.destination_minion_pool_id + args.instance_osmorphing_minion_pool_mappings = [ + {"instance_id": "mock_instance1", "pool_id": "mock_pool1"}, + {"instance_id": "mock_instance2", "pool_id": "mock_pool2"} + ] + args.clone_disks = True + args.skip_os_morphing = False + mock_transfer = mock.Mock() + self.mock_app.client_manager.coriolis.transfers = mock_transfer + mock_get_option_value_from_args.side_effect = [ + mock.sentinel.destination_environment, + mock.sentinel.source_environment, + mock.sentinel.network_map + ] + mock_get_storage_mappings_dict_from_args.return_value = \ + mock.sentinel.storage_mappings + mock_compose_user_scripts.return_value = mock.sentinel.user_scripts + expected_updated_properties = { + "destination_environment": mock.sentinel.destination_environment, + "source_environment": mock.sentinel.source_environment, + "storage_mappings": mock.sentinel.storage_mappings, + "network_map": mock.sentinel.network_map, + "notes": mock.sentinel.notes, + "origin_minion_pool_id": mock.sentinel.origin_minion_pool_id, + "destination_minion_pool_id": + mock.sentinel.destination_minion_pool_id, + "instance_osmorphing_minion_pool_mappings": + {"mock_instance1": "mock_pool1", "mock_instance2": "mock_pool2"}, + "user_scripts": mock.sentinel.user_scripts, + "clone_disks": True, + "skip_os_morphing": False, + } + + result = self.transfer.take_action(args) + + self.assertEqual( + mock_get_formatted_entity.return_value, + result + ) + mock_compose_user_scripts.assert_called_once_with( + mock.sentinel.global_scripts, mock.sentinel.instance_scripts) + mock_transfer.update.assert_called_once_with( + mock.sentinel.id, + expected_updated_properties + ) + mock_get_formatted_entity.assert_called_once_with( + mock_transfer.update.return_value) + + @mock.patch.object(cli_utils, 'compose_user_scripts') + @mock.patch.object(cli_utils, 'get_storage_mappings_dict_from_args') + @mock.patch.object(cli_utils, 'get_option_value_from_args') + def test_take_action_no_updated_properties( + self, + mock_get_option_value_from_args, + mock_get_storage_mappings_dict_from_args, + mock_compose_user_scripts + ): + class CustomMock(mock.MagicMock): + def __getattr__(self, name): + return None + args = CustomMock() + args.global_scripts = mock.sentinel.global_scripts + args.instance_scripts = mock.sentinel.instance_scripts + mock_transfer = mock.Mock() + self.mock_app.client_manager.coriolis.transfers = mock_transfer + mock_get_option_value_from_args.return_value = None + mock_get_storage_mappings_dict_from_args.return_value = None + mock_compose_user_scripts.return_value = None + + self.assertRaises( + ValueError, + self.transfer.take_action, + args + ) + + mock_compose_user_scripts.assert_called_once_with( + mock.sentinel.global_scripts, mock.sentinel.instance_scripts) + mock_transfer.update.assert_not_called() diff --git a/coriolisclient/tests/cli/test_utils.py b/coriolisclient/tests/cli/test_utils.py new file mode 100644 index 0000000..61461fd --- /dev/null +++ b/coriolisclient/tests/cli/test_utils.py @@ -0,0 +1,325 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import argparse +import ddt +import os +from unittest import mock + +from coriolisclient.cli import utils +from coriolisclient.tests import test_base + + +@ddt.ddt +class UtilsTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Utils.""" + + def test_add_storage_mappings_arguments_to_parser(self): + parser = argparse.ArgumentParser() + + utils.add_storage_mappings_arguments_to_parser(parser) + args = parser.parse_args( + ['--storage-backend-mapping', 'mock_source=mock_destination', + '--disk-storage-mapping', 'mock_disk_id=mock_destination', + '--default-storage-backend', 'mock_default_storage_backend']) + + self.assertEqual( + [{'disk_id': 'mock_disk_id', 'destination': 'mock_destination'}], + args.disk_storage_mappings + ) + self.assertEqual( + [{'source': 'mock_source', 'destination': 'mock_destination'}], + args.storage_backend_mappings + ) + self.assertEqual( + 'mock_default_storage_backend', + args.default_storage_backend + ) + + def test_get_storage_mappings_dict_from_args(self): + args = mock.Mock() + args.default_storage_backend = mock.sentinel.default_storage_backend + args.disk_storage_mappings = mock.sentinel.disk_storage_mappings + args.storage_backend_mappings = mock.sentinel.storage_backend_mappings + + result = utils.get_storage_mappings_dict_from_args(args) + + self.assertEqual( + { + "backend_mappings": mock.sentinel.storage_backend_mappings, + "default": mock.sentinel.default_storage_backend, + "disk_mappings": mock.sentinel.disk_storage_mappings + }, + result + ) + + def test_format_mapping(self): + mapping = { + "mapping1": "mock_mapping1", + "mapping2": "mock_mapping2", + "mapping3": "mock_mapping3" + } + + result = utils.format_mapping(mapping) + + self.assertEqual( + ( + "'mapping1'='mock_mapping1', " + "'mapping2'='mock_mapping2', " + "'mapping3'='mock_mapping3'" + ), + result + ) + + def test_parse_storage_mappings(self): + storage_mappings = { + "default": "mock_default", + "backend_mappings": [ + { + "source": "mock_source", + "destination": "mock_destination" + } + ], + "disk_mappings": [ + { + "disk_id": "mock_disk_id", + "destination": "mock_destination" + } + ] + } + + result = utils.parse_storage_mappings(storage_mappings) + + self.assertEqual( + ( + 'mock_default', + {'mock_source': 'mock_destination'}, + {'mock_disk_id': 'mock_destination'} + ), + result + ) + + def test_parse_storage_mappings_none(self): + result = utils.parse_storage_mappings(None) + + self.assertEqual( + (None, {}, {}), + result + ) + + def test_format_json_for_object_property(self): + obj = mock.Mock() + obj.prop_name = {"key1": "value1", "key2": "value2"} + + result = utils.format_json_for_object_property(obj, "prop_name") + + self.assertEqual( + '{\n "key1": "value1",\n "key2": "value2"\n}', + result + ) + + def test_format_json_for_object_property_to_dict(self): + obj = mock.Mock() + obj.prop_name.to_dict.return_value = \ + {"key1": "value1", "key2": "value2"} + + result = utils.format_json_for_object_property(obj, "prop_name") + + self.assertEqual( + '{\n "key1": "value1",\n "key2": "value2"\n}', + result + ) + + def test_format_json_for_object_property_none(self): + obj = mock.Mock() + obj.prop_name = None + + result = utils.format_json_for_object_property(obj, "prop_name") + + self.assertEqual( + '{}', + result + ) + + def test_validate_uuid_string(self): + result = utils.validate_uuid_string( + "12345678-9ABC-DEF1-2345-6789abcdef12") + + self.assertEqual( + True, + result + ) + + result = utils.validate_uuid_string( + "123456789ABCDEF") + + self.assertEqual( + False, + result + ) + + @mock.patch.object(argparse, 'FileType') + def test_add_args_for_json_option_to_parser(self, mock_file_type): + parser = argparse.ArgumentParser() + + utils.add_args_for_json_option_to_parser( + parser, "option_name") + + args = parser.parse_args( + ['--option-name', 'mock_option']) + + args_file = parser.parse_args( + ['--option-name-file', 'mock_option_file']) + + self.assertEqual( + ('mock_option', mock_file_type.return_value.return_value), + (args.option_name, args_file.option_name_file) + ) + + def test_get_option_value_from_args(self): + args = mock.MagicMock() + args.option_name = '{"option": "raw_value"}' + args.option_name_file.__enter__.return_value.read.return_value = \ + '{"option": "file_value"}' + + result = utils.get_option_value_from_args(args, "option-name") + + self.assertEqual( + {'option': 'raw_value'}, + result + ) + + args.option_name = None + + result = utils.get_option_value_from_args(args, "option-name") + + self.assertEqual( + {'option': 'file_value'}, + result + ) + + def test_get_option_value_from_args_no_value(self): + args = mock.Mock() + args.option_name = None + args.option_name_file = None + + result = utils.get_option_value_from_args( + args, "option-name", error_on_no_value=False) + + self.assertEqual( + None, + result + ) + + self.assertRaises( + ValueError, + utils.get_option_value_from_args, + args, + "option-name" + ) + + def test_get_option_value_from_args_json_value_error(self): + args = mock.Mock() + args.option_name = "invalid" + args.option_name_file = None + + self.assertRaises( + ValueError, + utils.get_option_value_from_args, + args, + "option-name" + ) + + @ddt.data( + { + "global_scripts": None, + "instance_scripts": None, + "expected_result": + { + "global": {}, + "instances": {} + } + }, + { + "global_scripts": ["linux="], + "instance_scripts": ["instance_1="], + "expected_result": + { + "global": {"linux": None}, + "instances": {"instance_1": None} + } + }, + { + "global_scripts": ["linux script"], + "instance_scripts": ["linux script"], + "expected_result": + { + "global": {}, + "instances": {} + } + }, + { + "global_scripts": ["invalid_os=scrips"], + "instance_scripts": None, + "expected_result": None + }, + { + "global_scripts": ["linux='invalid/file/path'"], + "instance_scripts": None, + "expected_result": None + }, + { + "global_scripts": None, + "instance_scripts": ["linux='invalid/file/path'"], + "expected_result": None + }, + ) + def test_compose_user_scripts(self, data): + global_scripts = data["global_scripts"] + instance_scripts = data["instance_scripts"] + expected_result = data["expected_result"] + + if expected_result: + result = utils.compose_user_scripts( + global_scripts, instance_scripts) + + self.assertEqual( + expected_result, + result + ) + else: + self.assertRaises( + ValueError, + utils.compose_user_scripts, + global_scripts, + instance_scripts + ) + + def test_compose_user_scripts_from_file(self): + script_path = os.path.dirname(os.path.realpath(__file__)) + script_path = os.path.join(script_path, 'data/user_scripts.yml') + global_scripts = ["linux=%s" % script_path] + instance_scripts = ["linux=%s" % script_path] + + result = utils.compose_user_scripts(global_scripts, instance_scripts) + + self.assertEqual( + { + 'global': {'linux': '"mock_script1"\n"mock_script2"\n'}, + 'instances': {'linux': '"mock_script1"\n"mock_script2"\n'} + }, + result + ) + + def test_add_minion_pool_args_to_parser(self): + parser = argparse.ArgumentParser() + + utils.add_minion_pool_args_to_parser(parser) + args = parser.parse_args( + ['--osmorphing-minion-pool-mapping', + 'mock_instance_id=mock_pool_id']) + + self.assertEqual( + [{'instance_id': 'mock_instance_id', 'pool_id': 'mock_pool_id'}], + args.instance_osmorphing_minion_pool_mappings + ) diff --git a/coriolisclient/tests/test_base.py b/coriolisclient/tests/test_base.py new file mode 100644 index 0000000..dd6dd2f --- /dev/null +++ b/coriolisclient/tests/test_base.py @@ -0,0 +1,488 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +"""Defines base class for all tests.""" + +from keystoneauth1 import exceptions as keystoneauth_exceptions +from oslotest import base as test_base +from oslotest import mock_fixture +from unittest import mock + +from coriolisclient import base +from coriolisclient import exceptions +from coriolisclient.tests import testutils + +# NOTE(claudiub): this needs to be called before any mock.patch calls are +# being done, and especially before any other test classes load. This fixes +# the mock.patch autospec issue: +# https://github.com/testing-cabal/mock/issues/396 +mock_fixture.patch_mock_module() + + +class CoriolisBaseTestCase(test_base.BaseTestCase): + + def setUp(self): + super(CoriolisBaseTestCase, self).setUp() + + +class BaseTestCase(CoriolisBaseTestCase): + """Test suite for the Coriolis Client Base.""" + + def test_getid_uuid(self): + obj = mock.Mock() + + result = base.getid(obj) + + self.assertEqual( + obj.uuid, + result + ) + + def test_getid_id(self): + obj = mock.Mock() + + result = base.getid(obj, possible_fields=["id"]) + + self.assertEqual( + obj.id, + result + ) + + def test_getid_id_no_fields(self): + obj = mock.Mock() + + result = base.getid(obj, possible_fields=None) + + self.assertEqual( + obj.id, + result + ) + + def test_getid_none(self): + obj = "mock_obj" + + result = base.getid(obj, possible_fields=None) + + self.assertEqual( + obj, + result + ) + + def test_wrap_unauthorized_exception(self): + + @base.wrap_unauthorized_exception + def mock_fun(func): + return func() + + func = mock.Mock() + result = mock_fun(func) + + self.assertEqual( + func.return_value, + result + ) + + def test_wrap_unauthorized_exception_http_unauthorized(self): + + @base.wrap_unauthorized_exception + def mock_fun(func): + return func() + + func = mock.Mock() + func.side_effect = keystoneauth_exceptions.http.Unauthorized() + + with self.assertLogs(logger=base.LOG, level="ERROR"): + self.assertRaises( + exceptions.HTTPAuthError, + mock_fun, + func + ) + + +class ResourceTestCase(CoriolisBaseTestCase): + """Test suite for the Coriolis Client Resource.""" + + def setUp(self): + mock_manager = mock.Mock() + super(ResourceTestCase, self).setUp() + self.resource = base.Resource( + mock_manager, + {"info": mock.sentinel.info} + ) + + def test__repr__(self): + result = repr(self.resource) + self.assertEqual( + "" % mock.sentinel.info, + result + ) + + def test_human_id(self): + self.assertEqual( + None, + self.resource.human_id + ) + + class HumanResource(base.Resource): + HUMAN_ID = True + + mock_manager = mock.Mock() + resource = HumanResource( + mock_manager, + {"name": "mock_name"} + ) + + self.assertEqual( + "mock_name", + resource.human_id + ) + + def test_add_details(self): + new_info = {"new_info": mock.sentinel.new_info} + + self.resource._add_details(new_info) + + self.assertEqual( + mock.sentinel.new_info, + self.resource.new_info + ) + + def test_add_details_attribute_already_defined(self): + info = {"human_id": mock.sentinel.human_id} + + self.resource._add_details(info) + + self.assertEqual( + None, + self.resource._info.get("human_id") + ) + + @mock.patch.object(base.Resource, "is_loaded") + @mock.patch.object(base.Resource, "get") + def test__getattr__(self, mock_get, mock_is_loaded): + mock_is_loaded.side_effect = [False, True] + mock_get.side_effect = lambda: setattr(self.resource, "attr", "value") + + self.assertEqual( + "value", + self.resource.attr + ) + + @mock.patch.object(base.Resource, "is_loaded") + @mock.patch.object(base.Resource, "get") + def test__getattr__error(self, mock_get, mock_is_loaded): + mock_is_loaded.side_effect = [False, True] + + self.assertRaises( + AttributeError, + self.resource.__getattr__, + "attr" + ) + mock_get.assert_called_once() + + @mock.patch.object(base.Resource, "_add_details") + def test_get(self, mock_add_details): + self.resource._loaded = False + self.resource.id = mock.sentinel.id + + self.resource.get() + + mock_add_details.assert_called_once_with( + self.resource.manager.get.return_value._info) + self.assertEqual(True, self.resource._loaded) + + @mock.patch.object(base.Resource, "_add_details") + def test_get_none(self, mock_add_details): + self.resource._loaded = False + self.resource.id = mock.sentinel.id + delattr(self.resource.manager, "get") + + self.resource.get() + + mock_add_details.assert_not_called() + + def test__eq__(self): + mock_manager = mock.Mock() + resource2 = base.Resource( + mock_manager, + {"info": mock.sentinel.info} + ) + + self.assertEqual( + ( + True, + NotImplemented, + ), + ( + self.resource.__eq__(resource2), + self.resource.__eq__("string"), + ) + ) + + resource2._info = {"info": mock.sentinel.info2} + + self.assertEqual( + False, + self.resource.__eq__(resource2) + ) + + def test__eq__different_subclasses(self): + mock_manager = mock.Mock() + + class SubResource1(base.Resource): + pass + + class SubResource2(base.Resource): + pass + + resource1 = SubResource1( + mock_manager, + {"info": mock.sentinel.info} + ) + resource2 = SubResource2( + mock_manager, + {"info": mock.sentinel.info} + ) + + self.assertEqual( + False, + resource1.__eq__(resource2) + ) + + def test_is_loaded(self): + self.resource._loaded = True + self.assertEqual(True, self.resource.is_loaded()) + self.resource._loaded = False + self.assertEqual(False, self.resource.is_loaded()) + + def test_set_loaded(self): + self.resource.set_loaded(True) + self.assertEqual(True, self.resource._loaded) + self.resource.set_loaded(False) + self.assertEqual(False, self.resource._loaded) + + def test_to_dict(self): + result = self.resource.to_dict() + + self.assertEqual( + {'info': mock.sentinel.info}, + result + ) + + +class BaseManagerTestCase(CoriolisBaseTestCase): + """Test suite for the Coriolis Client Base Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(BaseManagerTestCase, self).setUp() + self.manager = base.BaseManager(mock_client) + + def test_list(self): + self.manager.client.get(mock.sentinel.url).json.return_value = { + "mock_response_key": { + "data": [mock.sentinel.data1, mock.sentinel.data2]} + } + obj_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._list)( + self.manager, + url=mock.sentinel.url, + response_key="mock_response_key", + obj_class=obj_class, + json=None, + values_key="data" + ) + + self.assertEqual( + [obj_class.return_value] * 2, + result + ) + obj_class.assert_has_calls([ + mock.call(self.manager, mock.sentinel.data1, loaded=True), + mock.call(self.manager, mock.sentinel.data2, loaded=True) + ]) + + def test_list_json(self): + (self.manager.client.post(mock.sentinel.url, json=True).json. + return_value) = [mock.sentinel.data] + self.manager.resource_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._list)( + self.manager, + url=mock.sentinel.url, + response_key=None, + obj_class=None, + json=True, + values_key=None + ) + + self.assertEqual( + [self.manager.resource_class.return_value], + result + ) + self.manager.resource_class.assert_called_once_with( + self.manager, mock.sentinel.data, loaded=True) + + def test_get(self): + self.manager.client.get().json.return_value = { + "mock_response_key": mock.sentinel.data + } + self.manager.resource_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._get)( + self.manager, + url=mock.sentinel.url, + response_key="mock_response_key" + ) + + self.assertEqual( + self.manager.resource_class.return_value, + result + ) + self.manager.resource_class.assert_called_once_with( + self.manager, mock.sentinel.data, loaded=True) + + def test_get_no_response_key(self): + self.manager.client.get().json.return_value = mock.sentinel.data + self.manager.resource_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._get)( + self.manager, + url=mock.sentinel.url, + response_key=None + ) + + self.assertEqual( + self.manager.resource_class.return_value, + result + ) + self.manager.resource_class.assert_called_once_with( + self.manager, mock.sentinel.data, loaded=True) + + def test_post(self): + self.manager.client.post().json.return_value = { + "mock_response_key": mock.sentinel.data + } + self.manager.resource_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._post)( + self.manager, + url=mock.sentinel.url, + json=True, + response_key="mock_response_key", + return_raw=False + ) + + self.assertEqual( + self.manager.resource_class.return_value, + result + ) + self.manager.resource_class.assert_called_once_with( + self.manager, mock.sentinel.data) + + def test_post_no_response_key(self): + self.manager.client.post().json.return_value = mock.sentinel.data + self.manager.resource_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._post)( + self.manager, + url=mock.sentinel.url, + json=False, + response_key=None, + return_raw=True + ) + + self.assertEqual( + mock.sentinel.data, + result + ) + self.manager.resource_class.assert_not_called() + + def test_put(self): + self.manager.client.put().json.return_value = { + "mock_response_key": mock.sentinel.data + } + self.manager.resource_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._put)( + self.manager, + url=mock.sentinel.url, + json=True, + response_key="mock_response_key" + ) + + self.assertEqual( + self.manager.resource_class.return_value, + result + ) + self.manager.resource_class.assert_called_once_with( + self.manager, mock.sentinel.data) + + def test_put_no_response_key(self): + self.manager.client.put().json.return_value = mock.sentinel.data + self.manager.resource_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._put)( + self.manager, + url=mock.sentinel.url, + json=False, + response_key=None + ) + + self.assertEqual( + self.manager.resource_class.return_value, + result + ) + self.manager.resource_class.assert_called_once_with( + self.manager, mock.sentinel.data) + + def test_patch(self): + self.manager.client.patch().json.return_value = { + "mock_response_key": mock.sentinel.data + } + self.manager.resource_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._patch)( + self.manager, + url=mock.sentinel.url, + json=True, + response_key="mock_response_key" + ) + + self.assertEqual( + self.manager.resource_class.return_value, + result + ) + self.manager.resource_class.assert_called_once_with( + self.manager, mock.sentinel.data) + + def test_patch_no_response_key(self): + self.manager.client.patch().json.return_value = mock.sentinel.data + self.manager.resource_class = mock.Mock() + + result = testutils.get_wrapped_function(self.manager._patch)( + self.manager, + url=mock.sentinel.url, + json=False, + response_key=None + ) + + self.assertEqual( + self.manager.resource_class.return_value, + result + ) + self.manager.resource_class.assert_called_once_with( + self.manager, mock.sentinel.data) + + def test_delete(self): + result = testutils.get_wrapped_function(self.manager._delete)( + self.manager, + url=mock.sentinel.url + ) + + self.assertEqual( + self.manager.client.delete.return_value, + result + ) + self.manager.client.delete.assert_called_once_with(mock.sentinel.url) diff --git a/coriolisclient/tests/test_client.py b/coriolisclient/tests/test_client.py new file mode 100644 index 0000000..f4fdf41 --- /dev/null +++ b/coriolisclient/tests/test_client.py @@ -0,0 +1,40 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient import client as coriolis_client +from coriolisclient.tests import test_base + + +class _HTTPClientTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis HTTP Client.""" + + def test__init__(self): + session = mock.Mock() + self.client = coriolis_client._HTTPClient( + session, endpoint=mock.sentinel.endpoint, version='v0') + self.assertEqual( + "%s/%s" % (mock.sentinel.endpoint, 'v0'), + self.client.endpoint_override + ) + + def test__init__no_endpoint(self): + session = mock.Mock() + self.client = coriolis_client._HTTPClient(session) + self.assertEqual( + None, + self.client.endpoint_override + ) + + +class ClientTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis Client.""" + + @mock.patch.object(coriolis_client, "_HTTPClient") + def test__init__(self, mock_HTTPClient): + try: + self.client = coriolis_client.Client(session=mock.sentinel.session) + except Exception: + self.fail("Failed to initialize Client") + mock_HTTPClient.assert_called_once_with(session=mock.sentinel.session) diff --git a/coriolisclient/tests/testutils.py b/coriolisclient/tests/testutils.py new file mode 100644 index 0000000..418a7b9 --- /dev/null +++ b/coriolisclient/tests/testutils.py @@ -0,0 +1,24 @@ +"""Defines general utilities for all tests.""" + + +def get_wrapped_function(function): + """Get the method at the bottom of a stack of decorators.""" + if not hasattr(function, '__closure__') or not function.__closure__: + return function + + def _get_wrapped_function(function): + if not hasattr(function, '__closure__') or not function.__closure__: + return None + + for closure in function.__closure__: + func = closure.cell_contents + + deeper_func = _get_wrapped_function(func) + if deeper_func: + return deeper_func + elif hasattr(closure.cell_contents, '__call__'): + return closure.cell_contents + + return function + + return _get_wrapped_function(function) diff --git a/coriolisclient/tests/v1/__init__.py b/coriolisclient/tests/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/coriolisclient/tests/v1/test_common.py b/coriolisclient/tests/v1/test_common.py new file mode 100644 index 0000000..0f0086c --- /dev/null +++ b/coriolisclient/tests/v1/test_common.py @@ -0,0 +1,65 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import ddt + +from coriolisclient import exceptions +from coriolisclient.tests import test_base +from coriolisclient.v1 import common + + +class TaskTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Task.""" + + def setUp(self): + super(TaskTestCase, self).setUp() + self.task = common.Task( + None, + { + "progress_updates": [ + {"progress_update1": "mock_update1"}, + {"progress_update2": "mock_update2"} + ] + }, + loaded=False + ) + + def test_progress_updates(self): + result = self.task.progress_updates + + self.assertEqual( + ("mock_update1", "mock_update2"), + (result[0].progress_update1, result[1].progress_update2) + ) + + +@ddt.ddt +class CommonTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Common.""" + + @ddt.data( + ("value", False, "dmFsdWU="), + ({"key": "value"}, True, "eyJrZXkiOiAidmFsdWUifQ==") + ) + @ddt.unpack + def test_encode_base64_param(self, param, is_json, expected_result): + result = common.encode_base64_param(param, is_json=is_json) + + self.assertEqual( + result, + expected_result + ) + + @ddt.data( + (12345, False), + (None, False), + ({"key value"}, True) + ) + @ddt.unpack + def test_encode_base64_param_raises(self, param, is_json): + self.assertRaises( + exceptions.CoriolisException, + common.encode_base64_param, + param, + is_json=is_json + ) diff --git a/coriolisclient/tests/v1/test_deployments.py b/coriolisclient/tests/v1/test_deployments.py new file mode 100644 index 0000000..f92fe9a --- /dev/null +++ b/coriolisclient/tests/v1/test_deployments.py @@ -0,0 +1,124 @@ +# Copyright (c) 2024 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import common +from coriolisclient.v1 import deployments + +DEPLOYMENT_ID = "1" + + +class DeploymentResourceTestCase(test_base.CoriolisBaseTestCase): + + def setUp(self): + super(DeploymentResourceTestCase, self).setUp() + self.source_env = {"source_opt": "env_value"} + self.dest_env = {"dest_opt": "env_value"} + self.transfer_result = {"result": "value"} + self.task = {"task": "type_1"} + self.info = { + "source_environment": self.source_env, + "destination_environment": self.dest_env, + "transfer_result": self.transfer_result, + "tasks": [self.task], + } + self.deployment = deployments.Deployment(None, self.info) + + def test_source_environment(self): + result = self.deployment.source_environment + + self.assertIsInstance(result, common.SourceEnvironment) + self.assertEqual(self.source_env, result._info) + + def test_destination_environment(self): + result = self.deployment.destination_environment + + self.assertIsInstance(result, common.DestinationEnvironment) + self.assertEqual(self.dest_env, result._info) + + def test_transfer_result(self): + result = self.deployment.transfer_result + + self.assertIsInstance(result, common.TransferResult) + self.assertEqual(self.transfer_result, result._info) + + def test_task(self): + result = self.deployment.tasks + + [self.assertIsInstance(t, common.Task) for t in result] + self.assertEqual(self.task, result[0]._info) + + +class DeploymentManagerTestCase(test_base.CoriolisBaseTestCase): + + def setUp(self): + super(DeploymentManagerTestCase, self).setUp() + mock_client = mock.Mock() + self.deployments = deployments.DeploymentManager(mock_client) + + def test_list(self): + with mock.patch.object(self.deployments, '_list') as mock_list: + result = self.deployments.list(detail=True) + self.assertEqual(mock_list.return_value, result) + mock_list.assert_called_once_with( + '/deployments/detail', 'deployments') + + def test_get(self): + deployment = mock.Mock(uuid=DEPLOYMENT_ID) + with mock.patch.object(self.deployments, '_get') as mock_get: + result = self.deployments.get(deployment) + self.assertEqual(mock_get.return_value, result) + mock_get.assert_called_once_with( + f'/deployments/{DEPLOYMENT_ID}', 'deployment') + + def test_create_from_transfer(self): + with mock.patch.object(self.deployments, '_post') as mock_post: + expected_data = { + "deployment": { + "transfer_id": mock.sentinel.transfer_id, + "clone_disks": True, + "force": False, + "skip_os_morphing": False, + "user_scripts": None, + "instance_osmorphing_minion_pool_mappings": + mock.sentinel.pool_mappings, + } + } + result = self.deployments.create_from_transfer( + mock.sentinel.transfer_id, clone_disks=True, force=False, + skip_os_morphing=False, user_scripts=None, + instance_osmorphing_minion_pool_mappings=( + mock.sentinel.pool_mappings)) + self.assertEqual(mock_post.return_value, result) + mock_post.assert_called_once_with( + "/deployments", expected_data, "deployment") + + def test_delete(self): + with mock.patch.object(self.deployments, '_delete') as mock_delete: + result = self.deployments.delete(DEPLOYMENT_ID) + self.assertEqual(mock_delete.return_value, result) + mock_delete.assert_called_once_with( + f'/deployments/{DEPLOYMENT_ID}') + + def test_cancel(self): + force = False + expected_data = {"cancel": {"force": force}} + with mock.patch.object(self.deployments.client, 'post') as mock_post: + result = self.deployments.cancel(DEPLOYMENT_ID, force=force) + self.assertEqual(mock_post.return_value, result) + mock_post.assert_called_once_with( + f"/deployments/{DEPLOYMENT_ID}/actions", json=expected_data) diff --git a/coriolisclient/tests/v1/test_diagnostics.py b/coriolisclient/tests/v1/test_diagnostics.py new file mode 100644 index 0000000..fdb7337 --- /dev/null +++ b/coriolisclient/tests/v1/test_diagnostics.py @@ -0,0 +1,26 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import diagnostics + + +class DiagnosticsManagerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Diagnostics Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(DiagnosticsManagerTestCase, self).setUp() + self.diag = diagnostics.DiagnosticsManager(mock_client) + + @mock.patch.object(diagnostics.DiagnosticsManager, '_list') + def test_get(self, mock_list): + result = self.diag.get() + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with('/diagnostics', 'diagnostics') diff --git a/coriolisclient/tests/v1/test_endpoint_destination_minion_pool_options.py b/coriolisclient/tests/v1/test_endpoint_destination_minion_pool_options.py new file mode 100644 index 0000000..773a50d --- /dev/null +++ b/coriolisclient/tests/v1/test_endpoint_destination_minion_pool_options.py @@ -0,0 +1,49 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import endpoint_destination_minion_pool_options + + +class EndpointDestinationMinionPoolOptionsManagerTestCase( + test_base.CoriolisBaseTestCase): + """ + Test suite for the Coriolis v1 Endpoint Destination Minion Pool Options + Manager. + """ + + def setUp(self): + mock_client = mock.Mock() + super(EndpointDestinationMinionPoolOptionsManagerTestCase, self + ).setUp() + self.endpoint = ( + endpoint_destination_minion_pool_options. + EndpointDestinationMinionPoolOptionsManager)(mock_client) + + @mock.patch.object(endpoint_destination_minion_pool_options. + EndpointDestinationMinionPoolOptionsManager, '_list') + def test_list( + self, + mock_list + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.list( + mock_endpoint, + environment={"env": "mock_env"}, + option_names=["option1", "option2"] + ) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + ('/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/' + 'destination-minion-pool-options' + '?env=eyJlbnYiOiAibW9ja19lbnYifQ==' + '&options=WyJvcHRpb24xIiwgIm9wdGlvbjIiXQ=='), + 'destination_minion_pool_options') diff --git a/coriolisclient/tests/v1/test_endpoint_destination_options.py b/coriolisclient/tests/v1/test_endpoint_destination_options.py new file mode 100644 index 0000000..4cceb1c --- /dev/null +++ b/coriolisclient/tests/v1/test_endpoint_destination_options.py @@ -0,0 +1,45 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import endpoint_destination_options + + +class EndpointDestinationOptionsManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Endpoint Destination Options Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(EndpointDestinationOptionsManagerTestCase, self).setUp() + self.endpoint = ( + endpoint_destination_options. + EndpointDestinationOptionsManager)(mock_client) + + @mock.patch.object(endpoint_destination_options. + EndpointDestinationOptionsManager, '_list') + def test_list( + self, + mock_list + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.list( + mock_endpoint, + environment={"env": "mock_env"}, + option_names=["option1", "option2"] + ) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + ('/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/' + 'destination-options' + '?env=eyJlbnYiOiAibW9ja19lbnYifQ==' + '&options=WyJvcHRpb24xIiwgIm9wdGlvbjIiXQ=='), + 'destination_options') diff --git a/coriolisclient/tests/v1/test_endpoint_instances.py b/coriolisclient/tests/v1/test_endpoint_instances.py new file mode 100644 index 0000000..0b7b3c6 --- /dev/null +++ b/coriolisclient/tests/v1/test_endpoint_instances.py @@ -0,0 +1,115 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import common +from coriolisclient.v1 import endpoint_instances + + +class EndpointInstanceTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Endpoint Instance.""" + + def setUp(self): + mock_client = mock.Mock() + super(EndpointInstanceTestCase, self).setUp() + self.endpoint = endpoint_instances.EndpointInstance( + mock_client, + {"flavor_name": mock.sentinel.flavor_name}) + + def test_flavor_name(self): + result = self.endpoint.flavor_name + + self.assertEqual( + mock.sentinel.flavor_name, + result + ) + + +class EndpointInstanceManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Endpoint Instance Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(EndpointInstanceManagerTestCase, self).setUp() + self.endpoint = endpoint_instances.EndpointInstanceManager(mock_client) + + @mock.patch.object(endpoint_instances.EndpointInstanceManager, '_list') + def test_list( + self, + mock_list + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.list( + mock_endpoint, + env={"env": "mock_env"}, + marker="mock_marker", + limit="mock_limit", + name="mock_name" + ) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + ('/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/instances' + '?marker=mock_marker&limit=mock_limit&name=mock_name&' + 'env=eyJlbnYiOiAibW9ja19lbnYifQ%3D%3D'), + 'instances') + + @mock.patch.object(common, 'encode_base64_param') + def test_list_value_error( + self, + mock_encode_base64_param + ): + mock_endpoint = mock.Mock() + mock_encode_base64_param.return_value = mock.sentinel.encoded_env + + self.assertRaises( + ValueError, + self.endpoint.list, + mock_endpoint, + env=mock.sentinel.env + ) + + mock_encode_base64_param.assert_not_called() + + @mock.patch.object(endpoint_instances.EndpointInstanceManager, '_get') + def test_get( + self, + mock_get + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.get( + mock_endpoint, + "mock_id", + env={"env": "mock_env"}, + ) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + ('/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/instances/' + 'bW9ja19pZA==?env=eyJlbnYiOiAibW9ja19lbnYifQ=='), + 'instance') + + def test_get_value_error(self): + mock_endpoint = mock.Mock() + + self.assertRaises( + ValueError, + self.endpoint.get, + mock_endpoint, + "mock_instance_id", + env=mock.sentinel.env + ) diff --git a/coriolisclient/tests/v1/test_endpoint_networks.py b/coriolisclient/tests/v1/test_endpoint_networks.py new file mode 100644 index 0000000..febe4b4 --- /dev/null +++ b/coriolisclient/tests/v1/test_endpoint_networks.py @@ -0,0 +1,39 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import endpoint_networks + + +class EndpointNetworkManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Endpoint Network Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(EndpointNetworkManagerTestCase, self).setUp() + self.endpoint = endpoint_networks.EndpointNetworkManager(mock_client) + + @mock.patch.object(endpoint_networks.EndpointNetworkManager, '_list') + def test_list( + self, + mock_list + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.list( + mock_endpoint, + environment={"env": "mock_env"}, + ) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + ('/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/networks' + '?env=eyJlbnYiOiAibW9ja19lbnYifQ=='), + 'networks') diff --git a/coriolisclient/tests/v1/test_endpoint_source_minion_pool_options.py b/coriolisclient/tests/v1/test_endpoint_source_minion_pool_options.py new file mode 100644 index 0000000..6fc7e9e --- /dev/null +++ b/coriolisclient/tests/v1/test_endpoint_source_minion_pool_options.py @@ -0,0 +1,49 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import endpoint_source_minion_pool_options + + +class EndpointSourceMinionPoolOptionsManagerTestCase( + test_base.CoriolisBaseTestCase): + """ + Test suite for the Coriolis v1 Endpoint Source Minion Pool Options + Manager. + """ + + def setUp(self): + mock_client = mock.Mock() + super(EndpointSourceMinionPoolOptionsManagerTestCase, self + ).setUp() + self.endpoint = ( + endpoint_source_minion_pool_options. + EndpointSourceMinionPoolOptionsManager)(mock_client) + + @mock.patch.object(endpoint_source_minion_pool_options. + EndpointSourceMinionPoolOptionsManager, '_list') + def test_list( + self, + mock_list + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.list( + mock_endpoint, + environment={"env": "mock_env"}, + option_names=["option1", "option2"] + ) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + ('/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/' + 'source-minion-pool-options' + '?env=eyJlbnYiOiAibW9ja19lbnYifQ==' + '&options=WyJvcHRpb24xIiwgIm9wdGlvbjIiXQ=='), + 'source_minion_pool_options') diff --git a/coriolisclient/tests/v1/test_endpoint_source_options.py b/coriolisclient/tests/v1/test_endpoint_source_options.py new file mode 100644 index 0000000..d2939a4 --- /dev/null +++ b/coriolisclient/tests/v1/test_endpoint_source_options.py @@ -0,0 +1,44 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import endpoint_source_options + + +class EndpointSourceOptionsManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Endpoint Source Options Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(EndpointSourceOptionsManagerTestCase, self).setUp() + self.endpoint = ( + endpoint_source_options. + EndpointSourceOptionsManager)(mock_client) + + @mock.patch.object(endpoint_source_options. + EndpointSourceOptionsManager, '_list') + def test_list( + self, + mock_list + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.list( + mock_endpoint, + environment={"env": "mock_env"}, + option_names=["option1", "option2"] + ) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + ('/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/source-options' + '?env=eyJlbnYiOiAibW9ja19lbnYifQ==' + '&options=WyJvcHRpb24xIiwgIm9wdGlvbjIiXQ=='), + 'source_options') diff --git a/coriolisclient/tests/v1/test_endpoint_storage.py b/coriolisclient/tests/v1/test_endpoint_storage.py new file mode 100644 index 0000000..23d7acc --- /dev/null +++ b/coriolisclient/tests/v1/test_endpoint_storage.py @@ -0,0 +1,63 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import endpoint_storage + + +class EndpointStorageManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Endpoint Storage Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(EndpointStorageManagerTestCase, self).setUp() + self.endpoint = endpoint_storage.EndpointStorageManager(mock_client) + + @mock.patch.object(endpoint_storage.EndpointStorageManager, '_list') + def test_list( + self, + mock_list + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.list( + mock_endpoint, + environment={"env": "mock_env"} + ) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + ('/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/storage' + '?env=eyJlbnYiOiAibW9ja19lbnYifQ=='), + 'storage', + values_key='storage_backends') + + @mock.patch.object(endpoint_storage.EndpointStorageManager, '_get') + def test_get_default( + self, + mock_get + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + mock_get.return_value = {"config_default": "mock_default"} + + result = self.endpoint.get_default( + mock_endpoint, + environment={"env": "mock_env"}, + ) + + self.assertEqual( + "mock_default", + result + ) + mock_get.assert_called_once_with( + ('/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/storage' + '?env=eyJlbnYiOiAibW9ja19lbnYifQ=='), + 'storage') diff --git a/coriolisclient/tests/v1/test_endpoints.py b/coriolisclient/tests/v1/test_endpoints.py new file mode 100644 index 0000000..1e3e058 --- /dev/null +++ b/coriolisclient/tests/v1/test_endpoints.py @@ -0,0 +1,262 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient import exceptions +from coriolisclient.tests import test_base +from coriolisclient.v1 import endpoints + + +class EndpointTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Endpoint.""" + + def setUp(self): + super(EndpointTestCase, self).setUp() + self.endpoint = endpoints.Endpoint( + None, + { + "connection_info": { + "connection_info1": mock.sentinel.connection_info} + } + ) + + def test_connection_info(self): + result = self.endpoint.connection_info + + self.assertEqual( + mock.sentinel.connection_info, + result.connection_info1 + ) + + +class EndpointManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Endpoint Manager.""" + + def setUp(self): + self.mock_client = mock.Mock() + super(EndpointManagerTestCase, self).setUp() + self.endpoint = endpoints.EndpointManager(self.mock_client) + + @mock.patch.object(endpoints.EndpointManager, '_list') + def test_list( + self, + mock_list + ): + result = self.endpoint.list() + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with('/endpoints', 'endpoints') + + @mock.patch.object(endpoints.EndpointManager, '_get') + def test_get( + self, + mock_get + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.get(mock_endpoint) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + '/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb', 'endpoint') + + @mock.patch.object(endpoints.EndpointManager, '_post') + def test_create( + self, + mock_post + ): + result = self.endpoint.create( + mock.sentinel.name, + mock.sentinel.endpoint_type, + mock.sentinel.connection_info, + mock.sentinel.description, + mock.sentinel.regions + ) + expected_data = { + "endpoint": { + "name": mock.sentinel.name, + "type": mock.sentinel.endpoint_type, + "description": mock.sentinel.description, + "connection_info": mock.sentinel.connection_info, + "mapped_regions": mock.sentinel.regions + } + } + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + '/endpoints', expected_data, 'endpoint') + + @mock.patch.object(endpoints.EndpointManager, '_put') + def test_update( + self, + mock_put + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.update( + mock_endpoint, + mock.sentinel.updated_values + ) + expected_data = { + "endpoint": mock.sentinel.updated_values + } + + self.assertEqual( + mock_put.return_value, + result + ) + mock_put.assert_called_once_with( + '/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb', + expected_data, 'endpoint') + + @mock.patch.object(endpoints.EndpointManager, '_delete') + def test_delete( + self, + mock_delete + ): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + + result = self.endpoint.delete(mock_endpoint) + + self.assertEqual( + mock_delete.return_value, + result + ) + mock_delete.assert_called_once_with( + '/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb') + + def test_validate_connection(self): + mock_endpoint = mock.Mock() + mock_endpoint.uuid = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + self.mock_client.post.return_value.json.return_value = { + "validate-connection": { + "valid": mock.sentinel.valid, + "message": mock.sentinel.message + } + } + + result = self.endpoint.validate_connection(mock_endpoint) + + self.assertEqual( + (mock.sentinel.valid, mock.sentinel.message), + result + ) + self.mock_client.post.assert_called_once_with( + '/endpoints/53773ab8-1474-4cf7-bf0c-a496a6595ecb/actions', + json={'validate-connection': None}) + + @mock.patch.object(endpoints.EndpointManager, '_get_endpoint_id_for_name') + def test_get_endpoint_id_for_name_uuid( + self, + mock_get_endpoint_id_for_name + ): + mock_endpoint = '53773ab8-1474-4cf7-bf0c-a496a6595ecb' + result = self.endpoint.get_endpoint_id_for_name(mock_endpoint) + + self.assertEqual( + '53773ab8-1474-4cf7-bf0c-a496a6595ecb', + result + ) + mock_get_endpoint_id_for_name.assert_not_called() + + @mock.patch.object(endpoints.EndpointManager, '_get_endpoint_id_for_name') + def test_get_endpoint_id_for_name( + self, + mock_get_endpoint_id_for_name + ): + mock_endpoint = mock.Mock() + + result = self.endpoint.get_endpoint_id_for_name(mock_endpoint) + + self.assertEqual( + mock_get_endpoint_id_for_name.return_value, + result + ) + + @mock.patch.object(endpoints.EndpointManager, 'list') + def test__get_endpoint_id_for_name( + self, + mock_list + ): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.id = '1' + obj2.id = '2' + obj3.id = '3' + obj1.name = 'mock_name1' + obj2.name = 'mock_name2' + obj3.name = 'mock_name3' + obj_list = [obj1, obj2, obj3] + mock_list.return_value = obj_list + endpoint_name = "mock_name2" + + result = self.endpoint._get_endpoint_id_for_name(endpoint_name) + + self.assertEqual( + '2', + result + ) + + @mock.patch.object(endpoints.EndpointManager, 'list') + def test__get_endpoint_id_for_name_not_found( + self, + mock_list + ): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.id = '1' + obj2.id = '2' + obj3.id = '3' + obj1.name = 'mock_name1' + obj2.name = 'mock_name2' + obj3.name = 'mock_name3' + obj_list = [obj1, obj2, obj3] + mock_list.return_value = obj_list + endpoint_name = "mock_name4" + + self.assertRaises( + exceptions.EndpointIDNotFound, + self.endpoint._get_endpoint_id_for_name, + endpoint_name + ) + + @mock.patch.object(endpoints.EndpointManager, 'list') + def test__get_endpoint_id_for_name_not_unique( + self, + mock_list + ): + obj1 = mock.Mock() + obj2 = mock.Mock() + obj3 = mock.Mock() + obj1.id = '1' + obj2.id = '2' + obj3.id = '3' + obj1.name = 'mock_name1' + obj2.name = 'mock_name2' + obj3.name = 'mock_name2' + obj_list = [obj1, obj2, obj3] + mock_list.return_value = obj_list + endpoint_name = "mock_name2" + + self.assertRaises( + exceptions.NoUniqueEndpointNameMatch, + self.endpoint._get_endpoint_id_for_name, + endpoint_name + ) diff --git a/coriolisclient/tests/v1/test_licensing.py b/coriolisclient/tests/v1/test_licensing.py new file mode 100644 index 0000000..c8b4de8 --- /dev/null +++ b/coriolisclient/tests/v1/test_licensing.py @@ -0,0 +1,371 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +import requests + +from coriolisclient import exceptions +from coriolisclient.tests import test_base +from coriolisclient.v1 import licensing + + +class LicensingClientTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Licensing Client.""" + + def setUp(self): + mock_client = mock.Mock() + super(LicensingClientTestCase, self).setUp() + self.licence = licensing.LicensingClient( + mock_client, "endpoint_name") + mock_client.verify = True + self.licence._cli = mock_client + + def test_get_licensing_endpoint_url(self): + self.licence._cli.get_endpoint.return_value = "url/endpoint_url/" + + result = self.licence._get_licensing_endpoint_url() + + self.assertEqual( + "url/endpoint_url", + result + ) + self.licence._cli.get_endpoint.assert_called_once_with( + service_type="endpoint_name") + + def test_get_licensing_endpoint_url_raises(self): + self.licence._cli.get_endpoint.side_effect = Exception() + + with self.assertLogs(level="WARN"): + self.assertRaises( + exceptions.LicensingEndpointNotFound, + self.licence._get_licensing_endpoint_url + ) + self.licence._cli.get_endpoint.assert_called_once_with( + service_type="endpoint_name") + + @mock.patch.object(licensing.LicensingClient, + "_get_licensing_endpoint_url") + def test_do_req_raw( + self, + mock_get_licensing_endpoint_url + ): + mock_method = mock.Mock() + mock_resp = mock.Mock() + mock_resp.ok = True + mock_method.return_value = mock_resp + setattr(requests, "mock_method", mock_method) + mock_get_licensing_endpoint_url.return_value = 'url/endpoint_url/' + result = self.licence._do_req( + method_name="mock_method", + resource='url/resource_url/', + body=None, + response_key=None, + raw_response=True + ) + + self.assertEqual( + mock_resp, + result + ) + mock_method.assert_called_once_with( + 'url/endpoint_url/url/resource_url/', + verify=self.licence._cli.verify + ) + + @mock.patch.object(licensing.LicensingClient, + "_get_licensing_endpoint_url") + def test_do_req_json( + self, + mock_get_licensing_endpoint_url + ): + mock_method = mock.Mock() + mock_resp = mock.Mock() + mock_resp.ok = True + mock_resp.json.return_value = {"response_key": mock.sentinel.data} + mock_method.return_value = mock_resp + setattr(requests, "mock_method", mock_method) + mock_get_licensing_endpoint_url.return_value = 'url/endpoint_url/' + result = self.licence._do_req( + method_name="mock_method", + resource='url/resource_url/', + body={"mock_body": "value"}, + response_key='response_key', + raw_response=False + ) + + self.assertEqual( + mock.sentinel.data, + result + ) + mock_method.assert_called_once_with( + 'url/endpoint_url/url/resource_url/', + verify=self.licence._cli.verify, + data='{"mock_body": "value"}' + ) + + @mock.patch.object(licensing.LicensingClient, + "_get_licensing_endpoint_url") + def test_do_req_error( + self, + mock_get_licensing_endpoint_url + ): + mock_method = mock.Mock() + mock_resp = mock.Mock() + mock_resp.ok = False + mock_resp.json.side_effect = Exception + mock_resp.raise_for_status.side_effect = exceptions.CoriolisException + mock_method.return_value = mock_resp + setattr(requests, "mock_method", mock_method) + mock_get_licensing_endpoint_url.return_value = 'url/endpoint_url/' + + with self.assertLogs(level="DEBUG"): + self.assertRaises( + exceptions.CoriolisException, + self.licence._do_req, + method_name="mock_method", + resource='url/resource_url/', + body=None, + response_key='response_key', + raw_response=False + ) + mock_method.assert_called_once_with( + 'url/endpoint_url/url/resource_url/', + verify=self.licence._cli.verify + ) + + @mock.patch.object(licensing.LicensingClient, + "_get_licensing_endpoint_url") + def test_do_req_http_error( + self, + mock_get_licensing_endpoint_url + ): + mock_method = mock.Mock() + mock_resp = mock.Mock() + mock_resp.ok = False + mock_resp.json.return_value = {"error": {"code": 123, "message": ""}} + mock_method.return_value = mock_resp + setattr(requests, "mock_method", mock_method) + mock_get_licensing_endpoint_url.return_value = 'url/endpoint_url/' + + self.assertRaises( + exceptions.HTTPError, + self.licence._do_req, + method_name="mock_method", + resource='url/resource_url/', + body=None, + response_key='response_key', + raw_response=False + ) + mock_method.assert_called_once_with( + 'url/endpoint_url/url/resource_url/', + verify=self.licence._cli.verify + ) + + @mock.patch.object(licensing.LicensingClient, + "_get_licensing_endpoint_url") + def test_do_req_response_key_error( + self, + mock_get_licensing_endpoint_url + ): + mock_method = mock.Mock() + mock_resp = mock.Mock() + mock_resp.ok = False + mock_resp.json.return_value = {"response_key": mock.sentinel.data} + mock_method.return_value = mock_resp + setattr(requests, "mock_method", mock_method) + mock_get_licensing_endpoint_url.return_value = 'url/endpoint_url/' + + self.assertRaises( + ValueError, + self.licence._do_req, + method_name="mock_method", + resource='url/resource_url/', + body=None, + response_key='invalid', + raw_response=False + ) + mock_method.assert_called_once_with( + 'url/endpoint_url/url/resource_url/', + verify=self.licence._cli.verify + ) + + def test_do_req_method_error(self): + setattr(requests, "mock_method", None) + + self.assertRaises( + ValueError, + self.licence._do_req, + method_name="mock_method", + resource='url/resource_url/', + body=None, + response_key='invalid', + raw_response=False + ) + + @mock.patch.object(licensing.LicensingClient, '_do_req') + def test_get(self, mock_do_req): + result = self.licence.get( + resource=mock.sentinel.resource, + body=mock.sentinel.body, + response_key=mock.sentinel.response_key, + raw_response=False + ) + self.assertEqual( + mock_do_req.return_value, + result + ) + mock_do_req.assert_called_once_with( + 'GET', + mock.sentinel.resource, + response_key=mock.sentinel.response_key, + body=mock.sentinel.body, + raw_response=False + ) + + @mock.patch.object(licensing.LicensingClient, '_do_req') + def test_post(self, mock_do_req): + result = self.licence.post( + resource=mock.sentinel.resource, + body=mock.sentinel.body, + response_key=mock.sentinel.response_key, + raw_response=False + ) + self.assertEqual( + mock_do_req.return_value, + result + ) + mock_do_req.assert_called_once_with( + 'POST', + mock.sentinel.resource, + response_key=mock.sentinel.response_key, + body=mock.sentinel.body, + raw_response=False + ) + + @mock.patch.object(licensing.LicensingClient, '_do_req') + def test_delete(self, mock_do_req): + result = self.licence.delete( + resource=mock.sentinel.resource, + body=mock.sentinel.body, + response_key=mock.sentinel.response_key, + raw_response=False + ) + self.assertEqual( + mock_do_req.return_value, + result + ) + mock_do_req.assert_called_once_with( + 'DELETE', + mock.sentinel.resource, + response_key=mock.sentinel.response_key, + body=mock.sentinel.body, + raw_response=False + ) + + +class LicensingManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Licensing Client.""" + + @mock.patch.object(licensing, 'LicensingClient') + def setUp(self, mock_LicensingClient): + mock_client = mock.Mock() + super(LicensingManagerTestCase, self).setUp() + self.licence = licensing.LicensingManager(mock_client) + self.licence._licensing_cli = mock_LicensingClient + + def test_status(self): + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.status(mock.sentinel.appliance_id) + + self.assertEqual( + mock_resource_class.return_value, + result + ) + self.licence._licensing_cli.get.assert_called_once_with( + '/appliances/%s/status' % mock.sentinel.appliance_id, + response_key='appliance_licence_status') + mock_resource_class.assert_called_once_with( + self.licence, self.licence._licensing_cli.get.return_value, + loaded=True) + + def test_list(self): + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + self.licence._licensing_cli.get.return_value = { + "licence1": "mock_licence1", + "licence2": "mock_licence2" + } + + result = self.licence.list(mock.sentinel.appliance_id) + + self.assertEqual( + [mock_resource_class.return_value, + mock_resource_class.return_value], + result + ) + self.licence._licensing_cli.get.assert_called_once_with( + '/appliances/%s/licences' % mock.sentinel.appliance_id, + response_key='licences') + mock_resource_class.assert_has_calls([ + mock.call(self.licence, "licence1", loaded=True), + mock.call(self.licence, "licence2", loaded=True) + ]) + + def test_register(self): + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.register( + mock.sentinel.appliance_id, mock.sentinel.licence) + + self.assertEqual( + mock_resource_class.return_value, + result + ) + self.licence._licensing_cli.post.assert_called_once_with( + '/appliances/%s/licences' % mock.sentinel.appliance_id, + body=mock.sentinel.licence, + response_key='licence') + mock_resource_class.assert_called_once_with( + self.licence, self.licence._licensing_cli.post.return_value, + loaded=True) + + def test_show(self): + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.show( + mock.sentinel.appliance_id, mock.sentinel.licence_id) + + self.assertEqual( + mock_resource_class.return_value, + result + ) + self.licence._licensing_cli.get.assert_called_once_with( + '/appliances/%s/licences/%s' % (mock.sentinel.appliance_id, + mock.sentinel.licence_id), + response_key='licence') + mock_resource_class.assert_called_once_with( + self.licence, self.licence._licensing_cli.get.return_value, + loaded=True) + + def test_delete(self): + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.delete( + mock.sentinel.appliance_id, mock.sentinel.licence_id) + + self.assertEqual( + self.licence._licensing_cli.delete.return_value, + result + ) + self.licence._licensing_cli.delete.assert_called_once_with( + '/appliances/%s/licences/%s' % (mock.sentinel.appliance_id, + mock.sentinel.licence_id), + raw_response=True) diff --git a/coriolisclient/tests/v1/test_licensing_appliances.py b/coriolisclient/tests/v1/test_licensing_appliances.py new file mode 100644 index 0000000..394defe --- /dev/null +++ b/coriolisclient/tests/v1/test_licensing_appliances.py @@ -0,0 +1,79 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import licensing +from coriolisclient.v1 import licensing_appliances + + +class LicensingAppliancesManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Licensing Appliances Manager.""" + + @mock.patch.object(licensing, 'LicensingClient') + def setUp(self, mock_LicensingClient): + mock_client = mock.Mock() + self.mock_licencing_client = mock.Mock() + mock_LicensingClient.return_value = self.mock_licencing_client + super(LicensingAppliancesManagerTestCase, self).setUp() + self.licence = licensing_appliances.LicensingAppliancesManager( + mock_client) + + def test_list(self): + self.licence._licensing_cli.get.return_value = { + "appliance1": "mock_appliance1", + "appliance2": "mock_appliance2" + } + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.list() + + self.assertEqual( + [mock_resource_class.return_value, + mock_resource_class.return_value], + result + ) + self.mock_licencing_client.get.assert_called_once_with( + '/appliances', response_key='appliances') + mock_resource_class.assert_has_calls([ + mock.call(self.licence, "appliance1", loaded=True), + mock.call(self.licence, "appliance2", loaded=True) + ]) + + def test_show(self): + self.licence._licensing_cli.get.return_value = mock.sentinel.data + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.show(mock.sentinel.appliance_id) + + self.assertEqual( + mock_resource_class.return_value, + result + ) + self.mock_licencing_client.get.assert_called_once_with( + '/appliances/%s' % mock.sentinel.appliance_id, + response_key='appliance') + mock_resource_class.assert_called_once_with( + self.licence, mock.sentinel.data, loaded=True + ) + + def test_create(self): + self.licence._licensing_cli.post.return_value = mock.sentinel.data + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.create() + + self.assertEqual( + mock_resource_class.return_value, + result + ) + self.mock_licencing_client.post.assert_called_once_with( + '/appliances', response_key='appliance') + mock_resource_class.assert_called_once_with( + self.licence, mock.sentinel.data, loaded=True + ) diff --git a/coriolisclient/tests/v1/test_licensing_reservations.py b/coriolisclient/tests/v1/test_licensing_reservations.py new file mode 100644 index 0000000..bbf8cb1 --- /dev/null +++ b/coriolisclient/tests/v1/test_licensing_reservations.py @@ -0,0 +1,85 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import licensing +from coriolisclient.v1 import licensing_reservations + + +class LicensingReservationsManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Licensing Reservations Manager.""" + + @mock.patch.object(licensing, 'LicensingClient') + def setUp(self, mock_LicensingClient): + mock_client = mock.Mock() + self.mock_licencing_client = mock.Mock() + mock_LicensingClient.return_value = self.mock_licencing_client + super(LicensingReservationsManagerTestCase, self).setUp() + self.licence = licensing_reservations.LicensingReservationsManager( + mock_client) + + def test_list(self): + self.licence._licensing_cli.get.return_value = { + "reservation1": "mock_reservation1", + "reservation2": "mock_reservation2" + } + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.list(mock.sentinel.appliance_id) + + self.assertEqual( + [mock_resource_class.return_value, + mock_resource_class.return_value], + result + ) + self.mock_licencing_client.get.assert_called_once_with( + '/appliances/%s/reservations' % mock.sentinel.appliance_id, + response_key='reservations') + mock_resource_class.assert_has_calls([ + mock.call(self.licence, "reservation1", loaded=True), + mock.call(self.licence, "reservation2", loaded=True) + ]) + + def test_show(self): + self.licence._licensing_cli.get.return_value = mock.sentinel.data + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.show( + mock.sentinel.appliance_id, mock.sentinel.reservation_id) + + self.assertEqual( + mock_resource_class.return_value, + result + ) + self.mock_licencing_client.get.assert_called_once_with( + '/appliances/%s/reservations/%s' % (mock.sentinel.appliance_id, + mock.sentinel.reservation_id), + response_key='reservation') + mock_resource_class.assert_called_once_with( + self.licence, mock.sentinel.data, loaded=True + ) + + def test_refresh(self): + self.licence._licensing_cli.post.return_value = mock.sentinel.data + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.refresh( + mock.sentinel.appliance_id, mock.sentinel.reservation_id) + + self.assertEqual( + mock_resource_class.return_value, + result + ) + self.mock_licencing_client.post.assert_called_once_with( + '/appliances/%s/reservations/%s/refresh' % + (mock.sentinel.appliance_id, mock.sentinel.reservation_id), + response_key='reservation') + mock_resource_class.assert_called_once_with( + self.licence, mock.sentinel.data, loaded=True + ) diff --git a/coriolisclient/tests/v1/test_licensing_server.py b/coriolisclient/tests/v1/test_licensing_server.py new file mode 100644 index 0000000..721f49f --- /dev/null +++ b/coriolisclient/tests/v1/test_licensing_server.py @@ -0,0 +1,39 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import licensing +from coriolisclient.v1 import licensing_server + + +class LicensingServerManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Licensing Server Manager.""" + + @mock.patch.object(licensing, 'LicensingClient') + def setUp(self, mock_LicensingClient): + mock_client = mock.Mock() + self.mock_licencing_client = mock.Mock() + mock_LicensingClient.return_value = self.mock_licencing_client + super(LicensingServerManagerTestCase, self).setUp() + self.licence = licensing_server.LicensingServerManager( + mock_client) + + def test_status(self): + mock_resource_class = mock.Mock() + self.licence.resource_class = mock_resource_class + + result = self.licence.status() + + self.assertEqual( + mock_resource_class.return_value, + result + ) + self.mock_licencing_client.get.assert_called_once_with( + '/status', + response_key='status') + mock_resource_class.assert_called_once_with( + self.licence, self.mock_licencing_client.get.return_value, + loaded=True) diff --git a/coriolisclient/tests/v1/test_logging.py b/coriolisclient/tests/v1/test_logging.py new file mode 100644 index 0000000..a2b449e --- /dev/null +++ b/coriolisclient/tests/v1/test_logging.py @@ -0,0 +1,282 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import copy +import datetime +import ddt +import requests +import tempfile +from unittest import mock + +from keystoneauth1.exceptions import http + +from coriolisclient import exceptions +from coriolisclient.tests import test_base +from coriolisclient.v1 import logging + + +@ddt.ddt +class LoggingClientTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Logging Client.""" + + @mock.patch.object(logging.LoggingClient, '_get_endpoint_url') + def setUp(self, mock_get_endpoint_url): + mock_get_endpoint_url.return_value = mock.sentinel.ep_url + mock_client = mock.Mock() + mock_client.verify = True + super(LoggingClientTestCase, self).setUp() + self.logger = logging.LoggingClient(mock_client) + self.datetime = copy.deepcopy(datetime.datetime) + + @mock.patch.object(logging.LoggingClient, '_get_endpoint_url') + def test__init__(self, mock_get_endpoint_url): + mock_get_endpoint_url.side_effect = Exception + + with self.assertLogs(logger=logging.LOG, level="WARNING"): + logger = logging.LoggingClient(None) + + self.assertEqual( + None, + logger._ep_url + ) + + def test_get_endpoint_url(self): + self.logger._cli.get_endpoint.return_value = "url/endpoint_url/" + + result = self.logger._get_endpoint_url(mock.sentinel.name) + + self.assertEqual( + "url/endpoint_url", + result + ) + self.logger._cli.get_endpoint.assert_called_once_with( + service_type=mock.sentinel.name) + + def test_get_endpoint_url_not_found(self): + self.logger._cli.get_endpoint.return_value = None + + self.assertRaises( + exceptions.LoggingEndpointNotFound, + self.logger._get_endpoint_url, + mock.sentinel.name + ) + self.logger._cli.get_endpoint.assert_called_once_with( + service_type=mock.sentinel.name) + + def test_get_endpoint_url_http_unauthorized(self): + self.logger._cli.get_endpoint.side_effect = http.Unauthorized + + with self.assertLogs(logger=logging.LOG, level="ERROR"): + self.assertRaises( + exceptions.HTTPAuthError, + self.logger._get_endpoint_url, + mock.sentinel.name + ) + self.logger._cli.get_endpoint.assert_called_once_with( + service_type=mock.sentinel.name) + + @ddt.data( + { + "query_args": { + "arg1": None, + "arg2": None + }, + "is_websocket": True, + "expected_result": + "ws:///None/sentinel.resource" + }, + { + "query_args": { + "arg1": None, + "arg2": "mock_arg2" + }, + "_ep_url": "https:///ep_url", + "is_websocket": True, + "expected_result": + "wss:///ep_url/sentinel.resource?arg2=mock_arg2" + }, + { + "query_args": { + "arg1": "mock_arg1", + "arg2": "mock_arg2" + }, + "_ep_url": "https:///ep_url", + "is_websocket": False, + "expected_result": "https:///ep_url/sentinel.resource" + "?arg1=mock_arg1&arg2=mock_arg2" + } + ) + @mock.patch.object(logging.LoggingClient, '_get_endpoint_url') + def test_construct_url(self, data, mock_get_endpoint_url): + self.logger._ep_url = None + mock_get_endpoint_url.return_value = data.get("_ep_url", None) + + result = self.logger._construct_url( + mock.sentinel.resource, + data.get("query_args"), + is_websocket=data.get("is_websocket", False), + ) + + self.assertEqual( + data.get("expected_result"), + result + ) + + @ddt.data( + (None, None, False), + ("1", 1, False), + ("1234567890123456789", None, True), + ("abc", None, True), + ("", None, True), + ) + @ddt.unpack + def test_convert_period_to_timestamp( + self, + period, + expected_result, + raises + ): + if raises is False: + result = self.logger._convert_period_to_timestamp(period) + self.assertEqual( + expected_result, + result + ) + else: + self.assertRaises( + exceptions.CoriolisException, + self.logger._convert_period_to_timestamp, + period + ) + + @mock.patch.object(datetime, 'datetime') + def test_convert_period_to_timestamp_period( + self, + mock_datetime + ): + mock_datetime.utcnow.return_value = self.datetime.fromtimestamp(100000) + result = self.logger._convert_period_to_timestamp("1d") + self.assertEqual( + 13600, + result + ) + + @mock.patch.object(requests, "get") + @mock.patch.object(logging.LoggingClient, "_construct_url") + @mock.patch.object(logging.LoggingClient, "_convert_period_to_timestamp") + def test_download_logs( + self, + mock_convert_period_to_timestamp, + mock_construct_url, + mock_get + ): + mock_r = mock.Mock() + mock_r.iter_content.return_value = [b'test_chunk1', b'test_chunk2'] + mock_get.return_value.__enter__.return_value = mock_r + with tempfile.NamedTemporaryFile() as fd: + self.logger.download_logs( + mock.sentinel.app, + fd.name, + start_time=mock.sentinel.start_time, + end_time=mock.sentinel.end_time + ) + + result = fd.read() + self.assertEqual( + result, + b'test_chunk1test_chunk2' + ) + mock_get.assert_called_once_with( + mock_construct_url.return_value, + headers=self.logger._auth_headers, + stream=True, + verify=True + ) + mock_construct_url.assert_called_once_with( + "logs/sentinel.app/", + { + "start_date": mock_convert_period_to_timestamp.return_value, + "end_date": mock_convert_period_to_timestamp.return_value, + } + ) + + def test_download_logs_no_app(self): + self.assertRaises( + exceptions.CoriolisException, + self.logger.download_logs, + "", + None + ) + + @mock.patch.object(requests, "get") + def test_list_logs(self, mock_get): + mock_get.return_value.raise_for_status.return_value = None + mock_get.return_value.json.return_value = { + "logs": ["mock_log1", "mock_log2"] + } + + result = self.logger.list_logs() + + self.assertEqual( + ['mock_log1', 'mock_log2'], + result + ) + + +class CoriolisLogDownloadManagerTestCase( + test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Coriolis Log Download Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(CoriolisLogDownloadManagerTestCase, self).setUp() + self.logger = logging.CoriolisLogDownloadManager(mock_client) + self.logger._coriolis_cli = mock.Mock() + self.logger.resource_class = mock.Mock() + + def test_list(self): + self.logger._coriolis_cli.list_logs.return_value = [ + "mock_log1", "mock_log2"] + + result = self.logger.list() + + self.assertEqual( + [self.logger.resource_class.return_value, + self.logger.resource_class.return_value], + result + ) + self.logger.resource_class.assert_has_calls([ + mock.call(self.logger, "mock_log1", loaded=True), + mock.call(self.logger, "mock_log2", loaded=True) + ]) + + def test_get(self): + result = self.logger.get( + mock.sentinel.app, + mock.sentinel.to, + start_time=mock.sentinel.start_time, + end_time=mock.sentinel.end_time, + ) + + self.assertEqual( + self.logger._coriolis_cli.download_logs.return_value, + result + ) + self.logger._coriolis_cli.download_logs.assert_called_once_with( + mock.sentinel.app, + mock.sentinel.to, + start_time=mock.sentinel.start_time, + end_time=mock.sentinel.end_time, + ) + + def test_stream(self): + self.logger.stream( + app_name=mock.sentinel.app_name, + severity=mock.sentinel.severity, + ) + + self.logger._coriolis_cli.stream_logs.assert_called_once_with( + app_name=mock.sentinel.app_name, + severity=mock.sentinel.severity, + ) diff --git a/coriolisclient/tests/v1/test_minion_pools.py b/coriolisclient/tests/v1/test_minion_pools.py new file mode 100644 index 0000000..d1d950c --- /dev/null +++ b/coriolisclient/tests/v1/test_minion_pools.py @@ -0,0 +1,141 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import minion_pools + + +class MinionPoolManagerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Migration Pool Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(MinionPoolManagerTestCase, self).setUp() + self.minion_pool = minion_pools.MinionPoolManager(mock_client) + + @mock.patch.object(minion_pools.MinionPoolManager, "_list") + def test_list(self, mock_list): + result = self.minion_pool.list() + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + "/minion_pools", response_key="minion_pools") + + @mock.patch.object(minion_pools.MinionPoolManager, "_get") + def test_get(self, mock_get): + result = self.minion_pool.get(mock.sentinel.minion_pool) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + "/minion_pools/sentinel.minion_pool", response_key="minion_pool") + + @mock.patch.object(minion_pools.MinionPoolManager, "_post") + def test_create(self, mock_post): + expected_data = { + "name": mock.sentinel.name, + "endpoint_id": mock.sentinel.endpoint, + "platform": mock.sentinel.platform, + "os_type": mock.sentinel.os_type, + "environment_options": mock.sentinel.environment_options, + "minimum_minions": mock.sentinel.minimum_minions, + "maximum_minions": mock.sentinel.maximum_minions, + "minion_max_idle_time": mock.sentinel.minion_max_idle_time, + "minion_retention_strategy": + mock.sentinel.minion_retention_strategy, + "notes": mock.sentinel.notes, + "skip_allocation": False, + } + expected_data = {"minion_pool": expected_data} + + result = self.minion_pool.create( + mock.sentinel.name, + mock.sentinel.endpoint, + mock.sentinel.platform, + mock.sentinel.os_type, + environment_options=mock.sentinel.environment_options, + minimum_minions=mock.sentinel.minimum_minions, + maximum_minions=mock.sentinel.maximum_minions, + minion_max_idle_time=mock.sentinel.minion_max_idle_time, + minion_retention_strategy=mock.sentinel.minion_retention_strategy, + notes=mock.sentinel.notes, + skip_allocation=False, + ) + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + "/minion_pools", expected_data, response_key="minion_pool") + + @mock.patch.object(minion_pools.MinionPoolManager, "_put") + def test_update(self, mock_put): + result = self.minion_pool.update( + mock.sentinel.minion_pool, mock.sentinel.updated_values) + + self.assertEqual( + mock_put.return_value, + result + ) + mock_put.assert_called_once_with( + "/minion_pools/%s" % mock.sentinel.minion_pool, + {"minion_pool": mock.sentinel.updated_values}, 'minion_pool') + + @mock.patch.object(minion_pools.MinionPoolManager, "_delete") + def test_delete(self, mock_delete): + result = self.minion_pool.delete(mock.sentinel.minion_pool) + + self.assertEqual( + mock_delete.return_value, + result + ) + mock_delete.assert_called_once_with( + "/minion_pools/%s" % mock.sentinel.minion_pool) + + @mock.patch.object(minion_pools.MinionPoolManager, "_post") + def test_allocate_minion_pool(self, mock_post): + result = self.minion_pool.allocate_minion_pool( + mock.sentinel.minion_pool) + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + "/minion_pools/%s/actions" % mock.sentinel.minion_pool, + {'allocate': None}, response_key='minion_pool') + + @mock.patch.object(minion_pools.MinionPoolManager, "_post") + def test_refresh_minion_pool(self, mock_post): + result = self.minion_pool.refresh_minion_pool( + mock.sentinel.minion_pool) + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + "/minion_pools/%s/actions" % mock.sentinel.minion_pool, + {'refresh': None}, response_key='minion_pool') + + @mock.patch.object(minion_pools.MinionPoolManager, "_post") + def test_deallocate_minion_pool(self, mock_post): + result = self.minion_pool.deallocate_minion_pool( + mock.sentinel.minion_pool, force=False) + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + "/minion_pools/%s/actions" % mock.sentinel.minion_pool, + {'deallocate': {'force': False}}, + response_key='minion_pool') diff --git a/coriolisclient/tests/v1/test_providers.py b/coriolisclient/tests/v1/test_providers.py new file mode 100644 index 0000000..0d14d39 --- /dev/null +++ b/coriolisclient/tests/v1/test_providers.py @@ -0,0 +1,85 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import providers + + +class ProvidersTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Providers.""" + + def test_properties(self): + mock_client = mock.Mock() + self.provider = providers.Providers( + mock_client, + { + "provider1": { + "types": [mock.sentinel.type1, mock.sentinel.type2], + }, + "provider2": { + "types": [mock.sentinel.type3, mock.sentinel.type4], + } + } + ) + expected_provider_list = [ + { + 'name': 'provider1', + 'types': [mock.sentinel.type1, mock.sentinel.type2] + }, + { + 'name': 'provider2', + 'types': [mock.sentinel.type3, mock.sentinel.type4] + } + ] + expected_provider_schemas = [ + { + 'schema': { + 'types': [mock.sentinel.type1, mock.sentinel.type2]}, + 'type': 'provider1' + }, + { + 'schema': { + 'types': [mock.sentinel.type3, mock.sentinel.type4]}, + 'type': 'provider2' + } + ] + + self.assertEqual( + (expected_provider_list, expected_provider_schemas), + (self.provider.providers_list, self.provider.provider_schemas) + ) + + +class ProvidersManagerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Providers Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(ProvidersManagerTestCase, self).setUp() + self.provider = providers.ProvidersManager(mock_client) + + @mock.patch.object(providers.ProvidersManager, "_get") + def test_list(self, mock_get): + result = self.provider.list() + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with("/providers", "providers") + + @mock.patch.object(providers.ProvidersManager, "_get") + def test_schemas_list(self, mock_get): + result = self.provider.schemas_list( + mock.sentinel.provider_name, mock.sentinel.provider_type) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + '/providers/%s/schemas/%s' % (mock.sentinel.provider_name, + mock.sentinel.provider_type), + "schemas") diff --git a/coriolisclient/tests/v1/test_regions.py b/coriolisclient/tests/v1/test_regions.py new file mode 100644 index 0000000..f413821 --- /dev/null +++ b/coriolisclient/tests/v1/test_regions.py @@ -0,0 +1,196 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import ddt +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import regions + + +@ddt.ddt +class RegionManagerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Region Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(RegionManagerTestCase, self).setUp() + self.region = regions.RegionManager(mock_client) + + @mock.patch.object(regions.RegionManager, "_list") + def test_list(self, mock_list): + result = self.region.list() + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + "/regions", "regions") + + @mock.patch.object(regions.RegionManager, "_get") + def test_get(self, mock_get): + result = self.region.get(mock.sentinel.region) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + "/regions/sentinel.region", "region") + + @mock.patch.object(regions.RegionManager, "_post") + def test_create(self, mock_post): + expected_data = { + "name": mock.sentinel.name, + "description": mock.sentinel.description, + "enabled": False, + } + expected_data = {"region": expected_data} + + result = self.region.create( + mock.sentinel.name, + description=mock.sentinel.description, + enabled=False, + ) + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + "/regions", expected_data, "region") + + @mock.patch.object(regions.RegionManager, "_put") + def test_update(self, mock_put): + result = self.region.update( + mock.sentinel.region, mock.sentinel.updated_values) + + self.assertEqual( + mock_put.return_value, + result + ) + mock_put.assert_called_once_with( + "/regions/%s" % mock.sentinel.region, + {"region": mock.sentinel.updated_values}, 'region') + + @mock.patch.object(regions.RegionManager, "_delete") + def test_delete(self, mock_delete): + result = self.region.delete(mock.sentinel.region) + + self.assertEqual( + mock_delete.return_value, + result + ) + mock_delete.assert_called_once_with( + "/regions/%s" % mock.sentinel.region) + + @mock.patch.object(regions.RegionManager, "list") + @ddt.data( + { + "region_name_or_id": "mock_name", + "has_regions_cache": True, + "expected_region": "region1", + "raise_on_not_found": True, + "raises": False + }, + { + "region_name_or_id": "mock_name_duplicate", + "has_regions_cache": True, + "raise_on_not_found": True, + "raises": True + }, + { + "region_name_or_id": "mock_name4", + "has_regions_cache": False, + "expected_region": "region4", + "raise_on_not_found": True, + "raises": False + }, + { + "region_name_or_id": "mock_name_not_found", + "has_regions_cache": True, + "raise_on_not_found": True, + "raises": True + }, + { + "region_name_or_id": "mock_name_not_found", + "has_regions_cache": True, + "expected_region": None, + "raise_on_not_found": False, + "raises": False + }, + { + "region_name_or_id": "mock_id", + "has_regions_cache": True, + "expected_region": "region1", + "raise_on_not_found": True, + "raises": False + }, + { + "region_name_or_id": "mock_id_duplicate", + "has_regions_cache": True, + "raise_on_not_found": True, + "raises": True + }, + { + "region_name_or_id": "mock_id4", + "has_regions_cache": False, + "expected_region": "region4", + "raise_on_not_found": True, + "raises": False + }, + { + "region_name_or_id": "mock_id_not_found", + "has_regions_cache": True, + "raise_on_not_found": True, + "raises": True + }, + { + "region_name_or_id": "mock_id_not_found", + "has_regions_cache": True, + "expected_region": None, + "raise_on_not_found": False, + "raises": False + } + ) + def test_get_region_by_name_or_id( + self, + data, + mock_list + ): + self.region1 = mock.Mock() + self.region2 = mock.Mock() + self.region3 = mock.Mock() + self.region4 = mock.Mock() + self.region1.id = "mock_id" + self.region1.name = "mock_name" + self.region2.id = "mock_id_duplicate" + self.region2.name = "mock_name_duplicate" + self.region3.id = "mock_id_duplicate" + self.region3.name = "mock_name_duplicate" + self.region4.id = "mock_id4" + self.region4.name = "mock_name4" + regions_cache = [self.region1, self.region2, self.region3] + if data.get("has_regions_cache") is False: + regions_cache = None + mock_list.return_value = [self.region4] + + if data.get("raises", False): + self.assertRaises( + ValueError, + self.region.get_region_by_name_or_id, + data.get("region_name_or_id"), + regions_cache=regions_cache, + raise_on_not_found=data.get("raise_on_not_found") + ) + else: + result = self.region.get_region_by_name_or_id( + data.get("region_name_or_id"), + regions_cache=regions_cache, + raise_on_not_found=data.get("raise_on_not_found") + ) + self.assertEqual( + getattr(self, str(data.get("expected_region")), None), + result + ) diff --git a/coriolisclient/tests/v1/test_services.py b/coriolisclient/tests/v1/test_services.py new file mode 100644 index 0000000..98cf133 --- /dev/null +++ b/coriolisclient/tests/v1/test_services.py @@ -0,0 +1,148 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import ddt +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import services + + +@ddt.ddt +class ServiceManagerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Service Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(ServiceManagerTestCase, self).setUp() + self.service = services.ServiceManager(mock_client) + + @mock.patch.object(services.ServiceManager, "_list") + def test_list(self, mock_list): + result = self.service.list() + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + "/services", "services") + + @mock.patch.object(services.ServiceManager, "_get") + def test_get(self, mock_get): + result = self.service.get(mock.sentinel.service) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + "/services/sentinel.service", "service") + + @mock.patch.object(services.ServiceManager, "_post") + def test_create(self, mock_post): + expected_data = { + "host": mock.sentinel.host, + "binary": mock.sentinel.binary, + "topic": mock.sentinel.topic, + "mapped_regions": mock.sentinel.regions, + "enabled": True + } + expected_data = {"service": expected_data} + + result = self.service.create( + mock.sentinel.host, + mock.sentinel.binary, + mock.sentinel.topic, + mock.sentinel.regions, + enabled=True, + ) + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + "/services", expected_data, "service") + + @mock.patch.object(services.ServiceManager, "_put") + def test_update(self, mock_put): + result = self.service.update( + mock.sentinel.service, mock.sentinel.updated_values) + + self.assertEqual( + mock_put.return_value, + result + ) + mock_put.assert_called_once_with( + "/services/%s" % mock.sentinel.service, + {"service": mock.sentinel.updated_values}, 'service') + + @mock.patch.object(services.ServiceManager, "_delete") + def test_delete(self, mock_delete): + result = self.service.delete(mock.sentinel.service) + + self.assertEqual( + mock_delete.return_value, + result + ) + mock_delete.assert_called_once_with( + "/services/%s" % mock.sentinel.service) + + @mock.patch.object(services.ServiceManager, "list") + @ddt.data( + { + "host": "mock_host", + "topic": "mock_topic", + "expected_service": "service1", + "raises": False + }, + { + "host": "mock_host", + "topic": "mock_topic_not_found", + "raises": True + }, + { + "host": "mock_host_duplicate", + "topic": "mock_topic_duplicate", + "raises": True + }, + { + "host": "mock_host_not_found", + "topic": "mock_topic", + "raises": True + }, + ) + def test_find_service_by_host_and_topic( + self, + data, + mock_list + ): + self.service1 = mock.Mock() + self.service2 = mock.Mock() + self.service3 = mock.Mock() + self.service1.host = "mock_host" + self.service1.topic = "mock_topic" + self.service2.host = "mock_host_duplicate" + self.service2.topic = "mock_topic_duplicate" + self.service3.host = "mock_host_duplicate" + self.service3.topic = "mock_topic_duplicate" + services = [self.service1, self.service2, self.service3] + mock_list.return_value = services + + if data.get("raises", False): + self.assertRaises( + ValueError, + self.service.find_service_by_host_and_topic, + data.get("host"), + data.get("topic") + ) + else: + result = self.service.find_service_by_host_and_topic( + data.get("host"), + data.get("topic") + ) + self.assertEqual( + getattr(self, str(data.get("expected_service")), None), + result + ) diff --git a/coriolisclient/tests/v1/test_transfer_executions.py b/coriolisclient/tests/v1/test_transfer_executions.py new file mode 100644 index 0000000..ab5a7f0 --- /dev/null +++ b/coriolisclient/tests/v1/test_transfer_executions.py @@ -0,0 +1,114 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import transfer_executions + + +class TransferExecutionTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Transfer Execution.""" + + def test_tasks(self): + mock_client = mock.Mock() + self.transfer_execution = transfer_executions.TransferExecution( + mock_client, + { + "tasks": [{"task1": mock.sentinel.task1}, + {"task2": mock.sentinel.task2}], + } + ) + self.assertEqual( + ( + mock.sentinel.task1, + mock.sentinel.task2 + ), + ( + self.transfer_execution.tasks[0].task1, + self.transfer_execution.tasks[1].task2 + ) + ) + + +class TransferExecutionManagerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Transfer Execution Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(TransferExecutionManagerTestCase, self).setUp() + self.transfer_execution = transfer_executions.TransferExecutionManager( + mock_client) + + @mock.patch.object(transfer_executions.TransferExecutionManager, "_list") + def test_list(self, mock_list): + result = self.transfer_execution.list(mock.sentinel.transfer) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + '/transfers/%s/executions' % mock.sentinel.transfer, "executions") + + @mock.patch.object(transfer_executions.TransferExecutionManager, "_get") + def test_get(self, mock_get): + result = self.transfer_execution.get( + mock.sentinel.transfer, mock.sentinel.execution) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + "/transfers/%s/executions/%s" % (mock.sentinel.transfer, + mock.sentinel.execution), + "execution") + + @mock.patch.object(transfer_executions.TransferExecutionManager, "_post") + def test_create(self, mock_post): + expected_data = { + "shutdown_instances": mock.sentinel.shutdown_instances, + "auto_deploy": mock.sentinel.auto_deploy, + } + expected_data = {"execution": expected_data} + + result = self.transfer_execution.create( + mock.sentinel.transfer, + shutdown_instances=mock.sentinel.shutdown_instances, + auto_deploy=mock.sentinel.auto_deploy, + ) + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + '/transfers/%s/executions' % mock.sentinel.transfer, + expected_data, "execution") + + @mock.patch.object(transfer_executions.TransferExecutionManager, "_delete") + def test_delete(self, mock_delete): + result = self.transfer_execution.delete( + mock.sentinel.transfer, mock.sentinel.execution) + + self.assertEqual( + mock_delete.return_value, + result + ) + mock_delete.assert_called_once_with( + "/transfers/%s/executions/%s" % (mock.sentinel.transfer, + mock.sentinel.execution)) + + def test_cancel(self): + result = self.transfer_execution.cancel( + mock.sentinel.transfer, mock.sentinel.execution, force=False) + + self.assertEqual( + self.transfer_execution.client.post.return_value, + result + ) + self.transfer_execution.client.post.assert_called_once_with( + "/transfers/%s/executions/%s/actions" % (mock.sentinel.transfer, + mock.sentinel.execution), + json={'cancel': {'force': False}}) diff --git a/coriolisclient/tests/v1/test_transfer_schedules.py b/coriolisclient/tests/v1/test_transfer_schedules.py new file mode 100644 index 0000000..4cebaa2 --- /dev/null +++ b/coriolisclient/tests/v1/test_transfer_schedules.py @@ -0,0 +1,128 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +import datetime +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import transfer_schedules + + +class TransferScheduleManagerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Transfer Schedule Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(TransferScheduleManagerTestCase, self).setUp() + self.transfer_schedule = transfer_schedules.TransferScheduleManager( + mock_client) + + @mock.patch.object(transfer_schedules.TransferScheduleManager, "_list") + def test_list(self, mock_list): + result = self.transfer_schedule.list( + mock.sentinel.transfer, hide_expired=False) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + '/transfers/%s/schedules' % mock.sentinel.transfer, "schedules") + + @mock.patch.object(transfer_schedules.TransferScheduleManager, "_list") + def test_list_hide_expired(self, mock_list): + result = self.transfer_schedule.list( + mock.sentinel.transfer, hide_expired=True) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with( + '/transfers/%s/schedules?show_expired=False' + % mock.sentinel.transfer, "schedules") + + @mock.patch.object(transfer_schedules.TransferScheduleManager, "_get") + def test_get(self, mock_get): + result = self.transfer_schedule.get( + mock.sentinel.transfer, mock.sentinel.schedule) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + "/transfers/%s/schedules/%s" % (mock.sentinel.transfer, + mock.sentinel.schedule), + "schedule") + + @mock.patch.object(transfer_schedules.TransferScheduleManager, "_post") + def test_create(self, mock_post): + expiration_date = datetime.datetime.fromisoformat("2034-11-26") + expected_data = { + "schedule": mock.sentinel.schedule, + "enabled": True, + "expiration_date": '2034-11-26T00:00:00Z', + "shutdown_instance": mock.sentinel.shutdown_instance, + "auto_deploy": mock.sentinel.auto_deploy, + } + + result = self.transfer_schedule.create( + mock.sentinel.transfer, + mock.sentinel.schedule, + True, + expiration_date, + mock.sentinel.shutdown_instance, + mock.sentinel.auto_deploy, + ) + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + '/transfers/%s/schedules' % mock.sentinel.transfer, + expected_data, "schedule") + + @mock.patch.object(transfer_schedules.TransferScheduleManager, "_put") + def test_update(self, mock_put): + expiration_date = datetime.datetime.fromisoformat("2034-11-26") + updated_values = {"expiration_date": expiration_date} + result = self.transfer_schedule.update( + mock.sentinel.transfer_id, + mock.sentinel.schedule_id, + updated_values + ) + + self.assertEqual( + mock_put.return_value, + result + ) + mock_put.assert_called_once_with( + "/transfers/%s/schedules/%s" % (mock.sentinel.transfer_id, + mock.sentinel.schedule_id), + {"expiration_date": '2034-11-26T00:00:00Z'}, + "schedule") + + @mock.patch.object(transfer_schedules.TransferScheduleManager, "_delete") + def test_delete(self, mock_delete): + result = self.transfer_schedule.delete( + mock.sentinel.transfer, mock.sentinel.schedule) + + self.assertEqual( + mock_delete.return_value, + result + ) + mock_delete.assert_called_once_with( + "/transfers/%s/schedules/%s" % (mock.sentinel.transfer, + mock.sentinel.schedule),) + + def test_format_rfc3339_datetime(self): + dt = datetime.date(2024, 1, 1) + + result = self.transfer_schedule._format_rfc3339_datetime(dt) + + self.assertEqual( + "2024-01-01Z", + result + ) diff --git a/coriolisclient/tests/v1/test_transfers.py b/coriolisclient/tests/v1/test_transfers.py new file mode 100644 index 0000000..7966dc3 --- /dev/null +++ b/coriolisclient/tests/v1/test_transfers.py @@ -0,0 +1,213 @@ +# Copyright 2024 Cloudbase Solutions Srl +# All Rights Reserved. + +from unittest import mock + +from coriolisclient.tests import test_base +from coriolisclient.v1 import transfer_executions +from coriolisclient.v1 import transfers + + +class TransferTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Transfer.""" + + @mock.patch.object(transfers.Transfer, "get") + def test_properties(self, mock_get): + mock_client = mock.Mock() + self.transfer = transfers.Transfer( + mock_client, + { + "source_environment": { + "source_environment1": + mock.sentinel.source_environment1, + }, + "destination_environment": { + "destination_environment1": + mock.sentinel.destination_environment1, + }, + "executions": [{"execution1": mock.sentinel.execution1}, + {"execution2": mock.sentinel.execution2}], + } + ) + + self.assertEqual( + ( + mock.sentinel.source_environment1, + mock.sentinel.destination_environment1, + mock.sentinel.execution1, + mock.sentinel.execution2 + ), + ( + self.transfer.source_environment.source_environment1, + self.transfer.destination_environment.destination_environment1, + self.transfer.executions[0].execution1, + self.transfer.executions[1].execution2, + ) + ) + mock_get.assert_not_called() + + @mock.patch.object(transfers.Transfer, "get") + def test_properties_none(self, mock_get): + mock_client = mock.Mock() + self.transfer = transfers.Transfer( + mock_client, + {} + ) + + self.assertEqual( + ( + None, + None, + [] + ), + ( + self.transfer.source_environment, + self.transfer.destination_environment, + self.transfer.executions + ) + ) + mock_get.assert_called_once() + + +class TransferManagerTestCase(test_base.CoriolisBaseTestCase): + """Test suite for the Coriolis v1 Transfer Manager.""" + + def setUp(self): + mock_client = mock.Mock() + super(TransferManagerTestCase, self).setUp() + self.transfer = transfers.TransferManager(mock_client) + + @mock.patch.object(transfers.TransferManager, "_list") + def test_list(self, mock_list): + result = self.transfer.list(detail=False) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with("/transfers", "transfers") + + @mock.patch.object(transfers.TransferManager, "_list") + def test_list_details(self, mock_list): + result = self.transfer.list(detail=True) + + self.assertEqual( + mock_list.return_value, + result + ) + mock_list.assert_called_once_with("/transfers/detail", "transfers") + + @mock.patch.object(transfers.TransferManager, "_get") + def test_get(self, mock_get): + result = self.transfer.get(mock.sentinel.transfer) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + "/transfers/%s" % mock.sentinel.transfer, "transfer") + + @mock.patch.object(transfers.TransferManager, "_post") + def test_create(self, mock_post): + expected_data = { + "origin_endpoint_id": mock.sentinel.origin_endpoint_id, + "destination_endpoint_id": mock.sentinel.destination_endpoint_id, + "source_environment": mock.sentinel.source_environment, + "destination_environment": { + "network_map": mock.sentinel.network_map, + "storage_mappings": mock.sentinel.storage_mappings + }, + "instances": mock.sentinel.instances, + "scenario": mock.sentinel.scenario, + "network_map": mock.sentinel.network_map, + "notes": mock.sentinel.notes, + "storage_mappings": mock.sentinel.storage_mappings, + "user_scripts": mock.sentinel.user_scripts, + "origin_minion_pool_id": mock.sentinel.origin_minion_pool_id, + "destination_minion_pool_id": + mock.sentinel.destination_minion_pool_id, + "instance_osmorphing_minion_pool_mappings": + mock.sentinel.instance_osmorphing_minion_pool_mappings, + "clone_disks": True, + "skip_os_morphing": False, + } + expected_data = {"transfer": expected_data} + + result = self.transfer.create( + mock.sentinel.origin_endpoint_id, + mock.sentinel.destination_endpoint_id, + mock.sentinel.source_environment, + { + "network_map": mock.sentinel.network_map, + "storage_mappings": mock.sentinel.storage_mappings + }, + mock.sentinel.instances, + mock.sentinel.scenario, + network_map=None, + notes=mock.sentinel.notes, + storage_mappings=None, + user_scripts=mock.sentinel.user_scripts, + origin_minion_pool_id=mock.sentinel.origin_minion_pool_id, + destination_minion_pool_id= + mock.sentinel.destination_minion_pool_id, + instance_osmorphing_minion_pool_mappings= + mock.sentinel.instance_osmorphing_minion_pool_mappings, + clone_disks=True, + skip_os_morphing=False, + ) + + self.assertEqual( + mock_post.return_value, + result + ) + mock_post.assert_called_once_with( + "/transfers", expected_data, "transfer") + + @mock.patch.object(transfers.TransferManager, "_delete") + def test_delete(self, mock_delete): + result = self.transfer.delete(mock.sentinel.transfer) + + self.assertEqual( + mock_delete.return_value, + result + ) + mock_delete.assert_called_once_with( + "/transfers/%s" % mock.sentinel.transfer) + + @mock.patch.object(transfer_executions, "TransferExecution") + def test_delete_disks(self, mock_TransferExecution): + result = self.transfer.delete_disks(mock.sentinel.transfer) + + self.assertEqual( + mock_TransferExecution.return_value, + result + ) + self.transfer.client.post.assert_called_once_with( + "/transfers/%s/actions" % mock.sentinel.transfer, + json={'delete-disks': None}) + mock_TransferExecution.assert_called_once_with( + self.transfer, + (self.transfer.client.post.return_value.json.return_value. + get("execution")), + loaded=True + ) + + @mock.patch.object(transfer_executions, "TransferExecution") + def test_update(self, mock_TransferExecution): + updated_values = {"network_map": mock.sentinel.network_map} + result = self.transfer.update(mock.sentinel.transfer, updated_values) + + self.assertEqual( + mock_TransferExecution.return_value, + result + ) + self.transfer.client.put.assert_called_once_with( + "/transfers/%s" % mock.sentinel.transfer, + json={"transfer": updated_values}) + mock_TransferExecution.assert_called_once_with( + self.transfer, + (self.transfer.client.put.return_value.json.return_value. + get("execution")), + loaded=True + ) diff --git a/coriolisclient/v1/deployments.py b/coriolisclient/v1/deployments.py new file mode 100644 index 0000000..78558e7 --- /dev/null +++ b/coriolisclient/v1/deployments.py @@ -0,0 +1,86 @@ +# Copyright (c) 2024 Cloudbase Solutions Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from coriolisclient import base +from coriolisclient.v1 import common + + +class Deployment(base.Resource): + _tasks = None + + @property + def source_environment(self): + source_env = self._info.get("source_environment") + if source_env is not None: + return common.SourceEnvironment(None, source_env, loaded=True) + + @property + def destination_environment(self): + dest_env = self._info.get("destination_environment") + if dest_env is not None: + return common.DestinationEnvironment(None, dest_env, loaded=True) + + @property + def transfer_result(self): + res = self._info.get("transfer_result") + if res is not None: + return common.TransferResult(None, res, loaded=True) + + @property + def tasks(self): + if self._info.get('tasks') is None: + self.get() + return [common.Task(None, d, loaded=True) for d in + self._info.get('tasks', [])] + + +class DeploymentManager(base.BaseManager): + resource_class = Deployment + + def __init__(self, api): + super(DeploymentManager, self).__init__(api) + + def list(self, detail=False): + path = "/deployments" + if detail: + path = "%s/detail" % path + return self._list(path, 'deployments') + + def get(self, deployment): + return self._get( + '/deployments/%s' % base.getid(deployment), 'deployment') + + def create_from_transfer(self, transfer_id, clone_disks=True, force=False, + skip_os_morphing=False, user_scripts=None, + instance_osmorphing_minion_pool_mappings=None): + data = { + "deployment": { + "transfer_id": transfer_id, + "clone_disks": clone_disks, + "force": force, + "skip_os_morphing": skip_os_morphing, + "user_scripts": user_scripts}} + if instance_osmorphing_minion_pool_mappings is not None: + data['deployment']['instance_osmorphing_minion_pool_mappings'] = ( + instance_osmorphing_minion_pool_mappings) + return self._post('/deployments', data, 'deployment') + + def delete(self, deployment): + return self._delete('/deployments/%s' % base.getid(deployment)) + + def cancel(self, deployment, force=False): + return self.client.post( + '/deployments/%s/actions' % base.getid(deployment), + json={'cancel': {'force': force}}) diff --git a/coriolisclient/v1/endpoint_instances.py b/coriolisclient/v1/endpoint_instances.py index 4a62a55..0f36993 100644 --- a/coriolisclient/v1/endpoint_instances.py +++ b/coriolisclient/v1/endpoint_instances.py @@ -33,7 +33,7 @@ def __init__(self, api): def list( self, endpoint, env=None, marker=None, - limit=None, name=None): + limit=None, name=None, refresh=False): query = {} if marker is not None: @@ -42,6 +42,8 @@ def list( query['limit'] = limit if name is not None: query["name"] = name + if refresh: + query['refresh'] = True if env is not None: if not isinstance(env, dict): raise ValueError("'env' param must be a dict") diff --git a/coriolisclient/v1/endpoints.py b/coriolisclient/v1/endpoints.py index a19b354..700f8d6 100644 --- a/coriolisclient/v1/endpoints.py +++ b/coriolisclient/v1/endpoints.py @@ -14,8 +14,8 @@ # limitations under the License. from coriolisclient import base -from coriolisclient import exceptions from coriolisclient.cli import utils +from coriolisclient import exceptions class ConnectionInfo(base.Resource): diff --git a/coriolisclient/v1/licensing.py b/coriolisclient/v1/licensing.py index 5e1c328..81b5628 100644 --- a/coriolisclient/v1/licensing.py +++ b/coriolisclient/v1/licensing.py @@ -56,7 +56,7 @@ def _do_req(self, method_name, resource, body=None, response_key=None, endpoint_url = self._get_licensing_endpoint_url() url = '%s/%s' % (endpoint_url.rstrip('/'), resource.lstrip('/')) - kwargs = dict() + kwargs = {"verify": self._cli.verify} if body: if not isinstance(body, (str, bytes)): body = json.dumps(body) @@ -100,13 +100,13 @@ def get(self, resource, body=None, response_key=None, raw_response=False): body=body, raw_response=raw_response) def post(self, resource, body=None, response_key=None, - raw_response=False): + raw_response=False): return self._do_req( 'POST', resource, body=body, response_key=response_key, raw_response=raw_response) def delete(self, resource, body=None, response_key=None, - raw_response=False): + raw_response=False): return self._do_req('DELETE', resource, raw_response=raw_response, body=body, response_key=response_key) diff --git a/coriolisclient/v1/licensing_appliances.py b/coriolisclient/v1/licensing_appliances.py index 0121107..7cfc208 100644 --- a/coriolisclient/v1/licensing_appliances.py +++ b/coriolisclient/v1/licensing_appliances.py @@ -41,5 +41,5 @@ def show(self, appliance_id): def create(self): url = '/appliances' - data = self._licensing_cli.post(url, response_key='apppliance') + data = self._licensing_cli.post(url, response_key='appliance') return self.resource_class(self, data, loaded=True) diff --git a/coriolisclient/v1/logging.py b/coriolisclient/v1/logging.py index e8cd6f8..2e841c2 100644 --- a/coriolisclient/v1/logging.py +++ b/coriolisclient/v1/logging.py @@ -17,6 +17,7 @@ import datetime import json import logging +import ssl import traceback import requests @@ -100,15 +101,25 @@ def stream_logs(self, app_name=None, severity=None): "severity": severity, } url = self._construct_url("ws", args, is_websocket=True) + if self._cli.verify: + cafile = None + if isinstance(self._cli.verify, str): + cafile = self._cli.verify + ssl_context = ssl.create_default_context(cafile=cafile) + else: + ssl_context = ssl.SSLContext() + ssl_context.verify_mode = ssl.CERT_NONE async def nested(): - async with websockets.connect(url, extra_headers=headers) as ws: + async with websockets.connect( + url, extra_headers=headers, ssl=ssl_context) as ws: while True: msg = await ws.recv() as_dict = json.loads(msg) if app_name is None: app = "\033[2m\033[1m%s\x1b[0m" % as_dict["app_name"] - spacing = " " * (max((22-len(as_dict["app_name"]), 1))) + spacing = (" " * + (max((22 - len(as_dict["app_name"]), 1)))) print("%s>>%s%s" % (app, spacing, as_dict["message"])) else: print(as_dict["message"]) @@ -125,8 +136,11 @@ def _convert_period_to_timestamp(self, period): try: _ = datetime.datetime.fromtimestamp(int(period)) return int(period) - except: - pass + except (OverflowError, OSError): + LOG.warning("Failed to initialize timestamp from period value: %s", + period) + except ValueError: + LOG.warning("Invalid value type for period: %s", period) units = { "s": "seconds", "m": "minutes", @@ -136,14 +150,14 @@ def _convert_period_to_timestamp(self, period): try: count = period[:-1] unit = period[-1] - except: + except Exception: raise exceptions.CoriolisException("Failed to parse period") conv_unit = units.get(unit) if conv_unit is None: raise exceptions.CoriolisException("invalid period %s" % period) - args = {conv_unit: count} + args = {conv_unit: int(count)} tm = datetime.datetime.utcnow() - datetime.timedelta(**args) return int(tm.timestamp()) @@ -158,7 +172,9 @@ def download_logs(self, app, to, start_time=None, end_time=None): } resource = "logs/%s/" % app url = self._construct_url(resource, args) - with requests.get(url, headers=headers, stream=True) as r: + verify = self._cli.verify + with requests.get( + url, headers=headers, stream=True, verify=verify) as r: r.raise_for_status() with open(to, 'wb') as fd: for chunk in r.iter_content(chunk_size=8192): @@ -168,7 +184,7 @@ def download_logs(self, app, to, start_time=None, end_time=None): def list_logs(self): headers = self._auth_headers url = self._construct_url("logs/") - req = requests.get(url, headers=headers) + req = requests.get(url, headers=headers, verify=self._cli.verify) req.raise_for_status() ret = req.json() return ret.get("logs", []) diff --git a/coriolisclient/v1/migrations.py b/coriolisclient/v1/migrations.py deleted file mode 100644 index e625abd..0000000 --- a/coriolisclient/v1/migrations.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (c) 2016 Cloudbase Solutions Srl -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from coriolisclient import base -from coriolisclient.v1 import common - - -class Migration(base.Resource): - _tasks = None - - @property - def source_environment(self): - source_env = self._info.get("source_environment") - if source_env is not None: - return common.SourceEnvironment(None, source_env, loaded=True) - - @property - def destination_environment(self): - dest_env = self._info.get("destination_environment") - if dest_env is not None: - return common.DestinationEnvironment(None, dest_env, loaded=True) - - @property - def transfer_result(self): - res = self._info.get("transfer_result") - if res is not None: - return common.TransferResult(None, res, loaded=True) - - @property - def tasks(self): - if self._info.get('tasks') is None: - self.get() - return [common.Task(None, d, loaded=True) for d in - self._info.get('tasks', [])] - - -class MigrationManager(base.BaseManager): - resource_class = Migration - - def __init__(self, api): - super(MigrationManager, self).__init__(api) - - def list(self, detail=False): - path = "/migrations" - if detail: - path = "%s/detail" % path - return self._list(path, 'migrations') - - def get(self, migration): - return self._get('/migrations/%s' % base.getid(migration), 'migration') - - def create(self, origin_endpoint_id, destination_endpoint_id, - source_environment, destination_environment, instances, - network_map=None, notes=None, storage_mappings=None, - skip_os_morphing=False, replication_count=None, - shutdown_instances=None, user_scripts=None, - origin_minion_pool_id=None, destination_minion_pool_id=None, - instance_osmorphing_minion_pool_mappings=None): - if not network_map: - network_map = destination_environment.get('network_map', {}) - if not storage_mappings: - storage_mappings = destination_environment.get( - 'storage_mappings', {}) - - data = { - "migration": { - "origin_endpoint_id": origin_endpoint_id, - "destination_endpoint_id": destination_endpoint_id, - "destination_environment": destination_environment, - "instances": instances, - "skip_os_morphing": skip_os_morphing, - "network_map": network_map, - "notes": notes, - "storage_mappings": storage_mappings, - "user_scripts": user_scripts}} - if source_environment is not None: - data['migration']['source_environment'] = source_environment - if shutdown_instances is not None: - data['migration']['shutdown_instances'] = shutdown_instances - if replication_count is not None: - data['migration']['replication_count'] = replication_count - if origin_minion_pool_id is not None: - data['migration']['origin_minion_pool_id'] = origin_minion_pool_id - if destination_minion_pool_id is not None: - data['migration']['destination_minion_pool_id'] = ( - destination_minion_pool_id) - if instance_osmorphing_minion_pool_mappings: - data['migration']['instance_osmorphing_minion_pool_mappings'] = ( - instance_osmorphing_minion_pool_mappings) - - return self._post('/migrations', data, 'migration') - - def create_from_replica(self, replica_id, clone_disks=True, force=False, - skip_os_morphing=False, user_scripts=None, - instance_osmorphing_minion_pool_mappings=None): - data = {"migration": { - "replica_id": replica_id, - "clone_disks": clone_disks, - "force": force, - "skip_os_morphing": skip_os_morphing, - "user_scripts": user_scripts}} - if instance_osmorphing_minion_pool_mappings is not None: - data['migration']['instance_osmorphing_minion_pool_mappings'] = ( - instance_osmorphing_minion_pool_mappings) - return self._post('/migrations', data, 'migration') - - def delete(self, migration): - return self._delete('/migrations/%s' % base.getid(migration)) - - def cancel(self, migration, force=False): - return self.client.post( - '/migrations/%s/actions' % base.getid(migration), - json={'cancel': {'force': force}}) diff --git a/coriolisclient/v1/regions.py b/coriolisclient/v1/regions.py index cb54ee2..7864d33 100644 --- a/coriolisclient/v1/regions.py +++ b/coriolisclient/v1/regions.py @@ -14,8 +14,6 @@ # limitations under the License. from coriolisclient import base -from coriolisclient import exceptions -from coriolisclient.cli import utils class Region(base.Resource): diff --git a/coriolisclient/v1/services.py b/coriolisclient/v1/services.py index ab05c81..da9b3b6 100644 --- a/coriolisclient/v1/services.py +++ b/coriolisclient/v1/services.py @@ -14,8 +14,6 @@ # limitations under the License. from coriolisclient import base -from coriolisclient import exceptions -from coriolisclient.cli import utils class Service(base.Resource): @@ -66,7 +64,7 @@ def find_service_by_host_and_topic(self, host, topic): host, topic)) if len(matches) > 1: raise ValueError( - "Multiple services with the host/topic %s/%s were found: %s" % ( - host, topic, matches)) + "Multiple services with the host/topic %s/%s were found: %s" + % (host, topic, matches)) return matches[0] diff --git a/coriolisclient/v1/replica_executions.py b/coriolisclient/v1/transfer_executions.py similarity index 54% rename from coriolisclient/v1/replica_executions.py rename to coriolisclient/v1/transfer_executions.py index 2f906f8..0552aae 100644 --- a/coriolisclient/v1/replica_executions.py +++ b/coriolisclient/v1/transfer_executions.py @@ -17,7 +17,7 @@ from coriolisclient.v1 import common -class ReplicaExecution(base.Resource): +class TransferExecution(base.Resource): _tasks = None @property @@ -28,37 +28,40 @@ def tasks(self): self._info.get('tasks', [])] -class ReplicaExecutionManager(base.BaseManager): - resource_class = ReplicaExecution +class TransferExecutionManager(base.BaseManager): + resource_class = TransferExecution def __init__(self, api): - super(ReplicaExecutionManager, self).__init__(api) + super(TransferExecutionManager, self).__init__(api) - def list(self, replica): + def list(self, transfer): return self._list( - '/replicas/%s/executions' % base.getid(replica), 'executions') + '/transfers/%s/executions' % base.getid(transfer), 'executions') - def get(self, replica, execution): + def get(self, transfer, execution): return self._get( - '/replicas/%(replica_id)s/executions/%(execution_id)s' % - {"replica_id": base.getid(replica), + '/transfers/%(transfer_id)s/executions/%(execution_id)s' % + {"transfer_id": base.getid(transfer), "execution_id": base.getid(execution)}, 'execution') - def create(self, replica, shutdown_instances=False): - data = {"execution": {"shutdown_instances": shutdown_instances}} + def create(self, transfer, shutdown_instances=False, auto_deploy=False): + data = {"execution": { + "shutdown_instances": shutdown_instances, + "auto_deploy": auto_deploy}} return self._post( - '/replicas/%s/executions' % base.getid(replica), data, 'execution') + '/transfers/%s/executions' % + base.getid(transfer), data, 'execution') - def delete(self, replica, execution): + def delete(self, transfer, execution): return self._delete( - '/replicas/%(replica_id)s/executions/%(execution_id)s' % - {"replica_id": base.getid(replica), + '/transfers/%(transfer_id)s/executions/%(execution_id)s' % + {"transfer_id": base.getid(transfer), "execution_id": base.getid(execution)}) - def cancel(self, replica, execution, force=False): + def cancel(self, transfer, execution, force=False): return self.client.post( - '/replicas/%(replica_id)s/executions/%(execution_id)s/actions' % - {"replica_id": base.getid(replica), + '/transfers/%(transfer_id)s/executions/%(execution_id)s/actions' % + {"transfer_id": base.getid(transfer), "execution_id": base.getid(execution)}, json={'cancel': {'force': force}}) diff --git a/coriolisclient/v1/replica_schedules.py b/coriolisclient/v1/transfer_schedules.py similarity index 65% rename from coriolisclient/v1/replica_schedules.py rename to coriolisclient/v1/transfer_schedules.py index c439196..0dca527 100644 --- a/coriolisclient/v1/replica_schedules.py +++ b/coriolisclient/v1/transfer_schedules.py @@ -18,47 +18,47 @@ from coriolisclient import base -class ReplicaSchedule(base.Resource): +class TransferSchedule(base.Resource): _tasks = None -class ReplicaScheduleManager(base.BaseManager): - resource_class = ReplicaSchedule +class TransferScheduleManager(base.BaseManager): + resource_class = TransferSchedule def __init__(self, api): - super(ReplicaScheduleManager, self).__init__(api) + super(TransferScheduleManager, self).__init__(api) - def list(self, replica, hide_expired=False): + def list(self, transfer, hide_expired=False): query = {} if hide_expired: query["show_expired"] = hide_expired is False - url = '/replicas/%s/schedules' % base.getid(replica) + url = '/transfers/%s/schedules' % base.getid(transfer) if query: url += "?" + urlparse.urlencode(query) - return self._list( - url, 'schedules') + return self._list(url, 'schedules') - def get(self, replica, schedule): + def get(self, transfer, schedule): return self._get( - '/replicas/%(replica_id)s/schedules/%(schedule_id)s' % - {"replica_id": base.getid(replica), + '/transfers/%(transfer_id)s/schedules/%(schedule_id)s' % + {"transfer_id": base.getid(transfer), "schedule_id": base.getid(schedule)}, 'schedule') - def create(self, replica, schedule, enabled, expiration_date, - shutdown_instance): + def create(self, transfer, schedule, enabled, expiration_date, + shutdown_instance, auto_deploy): data = { "schedule": schedule, "enabled": enabled, "shutdown_instance": shutdown_instance, + "auto_deploy": auto_deploy, } if expiration_date: data["expiration_date"] = self._format_rfc3339_datetime( expiration_date) return self._post( - '/replicas/%s/schedules' % base.getid(replica), data, 'schedule') + '/transfers/%s/schedules' % base.getid(transfer), data, 'schedule') - def update(self, replica_id, schedule_id, updated_values): + def update(self, transfer_id, schedule_id, updated_values): expiration_date = updated_values.get("expiration_date") if expiration_date: updated_values = updated_values.copy() @@ -66,15 +66,15 @@ def update(self, replica_id, schedule_id, updated_values): expiration_date) return self._put( - '/replicas/%(replica_id)s/schedules/%(schedule_id)s' % { - "replica_id": base.getid(replica_id), + '/transfers/%(transfer_id)s/schedules/%(schedule_id)s' % { + "transfer_id": base.getid(transfer_id), "schedule_id": base.getid(schedule_id)}, updated_values, 'schedule') - def delete(self, replica, schedule): + def delete(self, transfer, schedule): return self._delete( - '/replicas/%(replica_id)s/schedules/%(schedule_id)s' % - {"replica_id": base.getid(replica), + '/transfers/%(transfer_id)s/schedules/%(schedule_id)s' % + {"transfer_id": base.getid(transfer), "schedule_id": base.getid(schedule)}) @staticmethod diff --git a/coriolisclient/v1/replicas.py b/coriolisclient/v1/transfers.py similarity index 67% rename from coriolisclient/v1/replicas.py rename to coriolisclient/v1/transfers.py index e82c023..d4559d5 100644 --- a/coriolisclient/v1/replicas.py +++ b/coriolisclient/v1/transfers.py @@ -15,10 +15,10 @@ from coriolisclient import base from coriolisclient.v1 import common -from coriolisclient.v1 import replica_executions +from coriolisclient.v1 import transfer_executions -class Replica(base.Resource): +class Transfer(base.Resource): _tasks = None @property @@ -41,74 +41,78 @@ def executions(self): self._info.get('executions', [])] -class ReplicaManager(base.BaseManager): - resource_class = Replica +class TransferManager(base.BaseManager): + resource_class = Transfer def __init__(self, api): - super(ReplicaManager, self).__init__(api) + super(TransferManager, self).__init__(api) def list(self, detail=False): - path = "/replicas" + path = "/transfers" if detail: path = "%s/detail" % path - return self._list(path, 'replicas') + return self._list(path, 'transfers') - def get(self, replica): - return self._get('/replicas/%s' % base.getid(replica), 'replica') + def get(self, transfer): + return self._get('/transfers/%s' % base.getid(transfer), 'transfer') def create(self, origin_endpoint_id, destination_endpoint_id, source_environment, destination_environment, instances, + transfer_scenario, network_map=None, notes=None, storage_mappings=None, origin_minion_pool_id=None, destination_minion_pool_id=None, instance_osmorphing_minion_pool_mappings=None, - user_scripts=None): + user_scripts=None, clone_disks=True, skip_os_morphing=False): if not network_map: network_map = destination_environment.get('network_map', {}) if not storage_mappings: storage_mappings = destination_environment.get( 'storage_mappings', {}) data = { - "replica": { + "transfer": { "origin_endpoint_id": origin_endpoint_id, "destination_endpoint_id": destination_endpoint_id, "destination_environment": destination_environment, "instances": instances, + "scenario": transfer_scenario, "network_map": network_map, "notes": notes, "storage_mappings": storage_mappings, "user_scripts": user_scripts, + "clone_disks": clone_disks, + "skip_os_morphing": skip_os_morphing, } } if source_environment: - data['replica']['source_environment'] = source_environment + data['transfer']['source_environment'] = source_environment if origin_minion_pool_id is not None: - data['replica']['origin_minion_pool_id'] = origin_minion_pool_id + data['transfer']['origin_minion_pool_id'] = origin_minion_pool_id if destination_minion_pool_id is not None: - data['replica']['destination_minion_pool_id'] = ( + data['transfer']['destination_minion_pool_id'] = ( destination_minion_pool_id) if instance_osmorphing_minion_pool_mappings: - data['replica']['instance_osmorphing_minion_pool_mappings'] = ( + data['transfer']['instance_osmorphing_minion_pool_mappings'] = ( instance_osmorphing_minion_pool_mappings) - return self._post('/replicas', data, 'replica') + return self._post('/transfers', data, 'transfer') - def delete(self, replica): - return self._delete('/replicas/%s' % base.getid(replica)) + def delete(self, transfer): + return self._delete('/transfers/%s' % base.getid(transfer)) - def delete_disks(self, replica): + def delete_disks(self, transfer): response = self.client.post( - '/replicas/%s/actions' % base.getid(replica), + '/transfers/%s/actions' % base.getid(transfer), json={'delete-disks': None}) - return replica_executions.ReplicaExecution( + return transfer_executions.TransferExecution( self, response.json().get("execution"), loaded=True) - def update(self, replica, updated_values): + def update(self, transfer, updated_values): data = { - "replica": updated_values + "transfer": updated_values } response = self.client.put( - '/replicas/%s' % base.getid(replica), json=data) + '/transfers/%s' % base.getid(transfer), json=data) - return replica_executions.ReplicaExecution( + return transfer_executions.TransferExecution( self, response.json().get("execution"), loaded=True) diff --git a/samples/endpoint_resouces_samples.py b/samples/endpoint_resouces_samples.py index cd9c703..fb32d76 100644 --- a/samples/endpoint_resouces_samples.py +++ b/samples/endpoint_resouces_samples.py @@ -4,9 +4,8 @@ """ import json - import jsonschema -from barbicanclient import client as barbican_client + from keystoneauth1.identity import v3 from keystoneauth1 import session as ksession @@ -62,7 +61,8 @@ def get_schema_for_plugin(coriolis, platform_type, schema_type): - """ Returns a JSON schema detailing params expected by a given plugin. + """ + Returns a JSON schema detailing params expected by a given plugin. :param platform_type: type of platform (e.g. 'openstack', 'oci', 'azure') :param schema_type: schema type(e.g. 'connection', 'source', 'destination') @@ -80,7 +80,8 @@ def get_schema_for_plugin(coriolis, platform_type, schema_type): def check_mapped_networks_exist(coriolis, destination_endpoint, network_map, destination_environment=None): - """ Checks whether all of the mapped networks exist on the destination. + """ + Checks whether all of the mapped networks exist on the destination. :param destination_endpoint: destination endpoint object or ID :param network_map: network mapping dict @@ -104,7 +105,8 @@ def check_mapped_networks_exist(coriolis, destination_endpoint, network_map, def check_mapped_storage_exists(coriolis, destination_endpoint, storage_mappings, destination_environment=None): - """ Checks whether all of the mapped storage types exist on the destination. + """ + Checks whether all of the mapped storage types exist on the destination. :param destination_endpoint: destination endpoint object or ID :param storage_mappings: storage mappings dict @@ -117,7 +119,8 @@ def check_mapped_storage_exists(coriolis, destination_endpoint, def _check_stor(storage_name_or_id): matches = [ stor for stor in storage - if stor.id == storage_name_or_id or stor.name == storage_name_or_id] + if (stor.id == storage_name_or_id or + stor.name == storage_name_or_id)] if not matches: raise Exception( "Could not find storage type '%s' in: %s" % ( @@ -130,7 +133,8 @@ def _check_stor(storage_name_or_id): _check_stor(default) # check backend mappings: - for mapped_storage in storage_mappings.get('backend_mappings', {}).values(): + backend_mappings = storage_mappings.get('backend_mappings', {}) + for mapped_storage in backend_mappings.values(): _check_stor(mapped_storage) # check per-disk mappings: @@ -146,9 +150,10 @@ def get_linux_vms_for_endpoint(coriolis, source_endpoint, source_options=None): return [vm for vm in vms if vm.os_type == 'linux'] -def check_vm_network_mapping(coriolis, source_endpoint, vm_name_or_id, network_map, - source_options=None): - """ Fetches detailed info on the given source VM and checks that all of its +def check_vm_network_mapping(coriolis, source_endpoint, vm_name_or_id, + network_map, source_options=None): + """ + Fetches detailed info on the given source VM and checks that all of its NICs have an appropriate mapping in the given network_map :param source_endpoint: source endpoint name or ID @@ -171,7 +176,6 @@ def main(): auth=v3.Password(**CORIOLIS_CONNECTION_INFO)) coriolis = coriolis_client.Client(session=session) - barbican = barbican_client.Client(session=session) # fetch and validate options for Azure: azure_schema = get_schema_for_plugin( diff --git a/samples/replica_samples.py b/samples/replica_samples.py index 56fdf58..7fec360 100644 --- a/samples/replica_samples.py +++ b/samples/replica_samples.py @@ -100,10 +100,9 @@ def create_vm_replicas(coriolis, source_endpoint, destination_endpoint, "Could not find source VM '%s' on source endpoint '%s'" % ( vm_name_or_id, source_endpoint)) - print("Creating Replica for VM(s): %s" % vm_group) replicas.append( - coriolis.replicas.create( + coriolis.transfers.create( source_endpoint, destination_endpoint, source_options, destination_options, vm_group, @@ -119,7 +118,7 @@ def get_replicas_for_endpoint(coriolis, endpoint, endpoint = coriolis.endpoints.get(endpoint) found = [] - for replica in coriolis.replicas.list(): + for replica in coriolis.transfers.list(): if as_source and replica.origin_endpoint_id == endpoint.id: found.append(replica) @@ -132,7 +131,7 @@ def get_replicas_for_endpoint(coriolis, endpoint, def get_replicas_for_vm(coriolis, vm_name): """ Returns all Replicas which include the given VM name. """ replicas = [] - for replica in coriolis.replicas.list(): + for replica in coriolis.transfers.list(): if vm_name in replica.instances: replicas.append(replica) @@ -142,7 +141,7 @@ def get_replicas_for_vm(coriolis, vm_name): def get_errord_replicas(coriolis): """ Returns a list of all the Replicas whose last execution error'd. """ errord = [] - for replica in coriolis.replicas.list(): + for replica in coriolis.transfers.list(): if not replica.executions: # Replica was never executed continue @@ -163,13 +162,13 @@ def wait_for_replica_execution(coriolis, replica, execution, """ tries = 0 - execution = coriolis.replica_executions.get( + execution = coriolis.transfer_executions.get( replica, execution) while tries < max_tries and execution.status == "RUNNING": print("Waiting on execution %s (currently %s)" % ( execution.id, execution.status)) time.sleep(retry_period) - execution = coriolis.replica_executions.get( + execution = coriolis.transfer_executions.get( replica, execution) if execution.status == "ERROR": @@ -184,7 +183,8 @@ def wait_for_replica_execution(coriolis, replica, execution, return execution -def wait_for_replica_migration(coriolis, migration, max_tries=600, retry_period=5): +def wait_for_replica_migration(coriolis, migration, max_tries=600, + retry_period=5): """ Waits for a maximum amount of time for a given Migration to finish. :param migration_id: Migration object/ID @@ -220,15 +220,15 @@ def delete_replica(coriolis, replica, delete_replica_disks=True): the Replica itself. """ if delete_replica_disks: - execution = coriolis.replicas.delete_disks(replica) + execution = coriolis.transfers.delete_disks(replica) wait_for_replica_execution(coriolis, replica, execution) # NOTE: this is redundant as executions are cascade-deleted on # the deletion of the parent Replica anyway: for execution in reversed(replica.executions): - coriolis.replica_executions.delete(execution) + coriolis.transfer_executions.delete(execution) - coriolis.replicas.delete(replica) + coriolis.transfers.delete(replica) def main(): @@ -252,14 +252,14 @@ def main(): # execute the Replica: test_replica = replicas[0] - execution = coriolis.replica_executions.create( + execution = coriolis.transfer_executions.create( test_replica, shutdown_instances=True) # wait for execution to finish: wait_for_replica_execution(coriolis, test_replica, execution) # create a Migration from the Replica: - migration = coriolis.migrations.create_from_replica( + migration = coriolis.migrations.create_from_transfer( test_replica.id, clone_disks=True, skip_os_morphing=False) migration = wait_for_replica_migration(coriolis, migration) print("Migrated VM info is: %s" % ( diff --git a/setup.cfg b/setup.cfg index eeaabb9..b67cc70 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,34 +44,33 @@ coriolis.v1 = endpoint_source_minion_pool_options_list = coriolisclient.cli.endpoint_source_minion_pool_options:ListEndpointSourceMinionPoolOptions endpoint_destination_minion_pool_options_list = coriolisclient.cli.endpoint_destination_minion_pool_options:ListEndpointDestinationMinionPoolOptions - migration_cancel = coriolisclient.cli.migrations:CancelMigration - migration_create = coriolisclient.cli.migrations:CreateMigration - migration_deploy_replica = coriolisclient.cli.migrations:CreateMigrationFromReplica - migration_delete = coriolisclient.cli.migrations:DeleteMigration - migration_list = coriolisclient.cli.migrations:ListMigration - migration_show = coriolisclient.cli.migrations:ShowMigration + deployment_cancel = coriolisclient.cli.deployments:CancelDeployment + deployment_create = coriolisclient.cli.deployments:CreateDeployment + deployment_delete = coriolisclient.cli.deployments:DeleteDeployment + deployment_list = coriolisclient.cli.deployments:ListDeployment + deployment_show = coriolisclient.cli.deployments:ShowDeployment provider_list = coriolisclient.cli.providers:ListProvider provider_schema_list = coriolisclient.cli.providers:ListProviderSchemas - replica_create = coriolisclient.cli.replicas:CreateReplica - replica_delete = coriolisclient.cli.replicas:DeleteReplica - replica_disks_delete = coriolisclient.cli.replicas:DeleteReplicaDisks - replica_list = coriolisclient.cli.replicas:ListReplica - replica_show = coriolisclient.cli.replicas:ShowReplica - replica_update = coriolisclient.cli.replicas:UpdateReplica - - replica_execution_cancel = coriolisclient.cli.replica_executions:CancelReplicaExecution - replica_execute = coriolisclient.cli.replica_executions:CreateReplicaExecution - replica_execution_delete = coriolisclient.cli.replica_executions:DeleteReplicaExecution - replica_execution_list = coriolisclient.cli.replica_executions:ListReplicaExecution - replica_execution_show = coriolisclient.cli.replica_executions:ShowReplicaExecution - - replica_schedule_delete = coriolisclient.cli.replica_schedules:DeleteReplicaSchedule - replica_schedule_list = coriolisclient.cli.replica_schedules:ListReplicaSchedule - replica_schedule_show = coriolisclient.cli.replica_schedules:ShowReplicaSchedule - replica_schedule_create = coriolisclient.cli.replica_schedules:CreateReplicaSchedule - replica_schedule_update = coriolisclient.cli.replica_schedules:UpdateReplicaSchedule + transfer_create = coriolisclient.cli.transfers:CreateTransfer + transfer_delete = coriolisclient.cli.transfers:DeleteTransfer + transfer_disks_delete = coriolisclient.cli.transfers:DeleteTransferDisks + transfer_list = coriolisclient.cli.transfers:ListTransfer + transfer_show = coriolisclient.cli.transfers:ShowTransfer + transfer_update = coriolisclient.cli.transfers:UpdateTransfer + + transfer_execution_cancel = coriolisclient.cli.transfer_executions:CancelTransferExecution + transfer_execute = coriolisclient.cli.transfer_executions:CreateTransferExecution + transfer_execution_delete = coriolisclient.cli.transfer_executions:DeleteTransferExecution + transfer_execution_list = coriolisclient.cli.transfer_executions:ListTransferExecution + transfer_execution_show = coriolisclient.cli.transfer_executions:ShowTransferExecution + + transfer_schedule_delete = coriolisclient.cli.transfer_schedules:DeleteTransferSchedule + transfer_schedule_list = coriolisclient.cli.transfer_schedules:ListTransferSchedule + transfer_schedule_show = coriolisclient.cli.transfer_schedules:ShowTransferSchedule + transfer_schedule_create = coriolisclient.cli.transfer_schedules:CreateTransferSchedule + transfer_schedule_update = coriolisclient.cli.transfer_schedules:UpdateTransferSchedule region_create = coriolisclient.cli.regions:CreateRegion region_list = coriolisclient.cli.regions:ListRegions diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..fea111a --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,10 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +hacking>=6.0.1,<=6.0.1 # Apache-2.0 +coverage!=4.4,>=4.0 # Apache-2.0 +ddt>=1.2.1 # MIT +oslotest>=3.8.0 # Apache-2.0 +stestr>=2.0.0 # Apache-2.0 + diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..82ae014 --- /dev/null +++ b/tox.ini @@ -0,0 +1,40 @@ +[tox] +minversion = 4.0.2 +envlist = py3,pep8,cover +skipsdist = True + +[testenv] +usedevelop = True +setenv = VIRTUAL_ENV={envdir} +deps = + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt +commands = stestr run --slowest {posargs} + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:cover] +setenv = + {[testenv]setenv} + PYTHON=coverage run --source coriolisclient --parallel-mode +commands = + stestr run --no-subunit-trace {posargs} + coverage combine + coverage report --fail-under=82 --skip-covered + coverage html -d cover + coverage xml -o cover/coverage.xml + +[testenv:venv] +commands = {posargs} + +[coverage:run] +source = coriolisclient +omit = coriolisclient/tests/* + +[flake8] +# E125 is deliberately excluded. See https://github.com/jcrocholl/pep8/issues/126 +# E251 Skipped due to https://github.com/jcrocholl/pep8/issues/301 + +ignore = E125,E251,W503,W504,E305,E731,E117,W605,F632,H102,H401,H403,H404,H405 +exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools