summaryrefslogtreecommitdiff
path: root/vmmsclient/v2/client.py
diff options
context:
space:
mode:
Diffstat (limited to 'vmmsclient/v2/client.py')
-rw-r--r--vmmsclient/v2/client.py382
1 files changed, 382 insertions, 0 deletions
diff --git a/vmmsclient/v2/client.py b/vmmsclient/v2/client.py
new file mode 100644
index 0000000..9fce6bb
--- /dev/null
+++ b/vmmsclient/v2/client.py
@@ -0,0 +1,382 @@
+# Copyright (c) 2025 Thomas Goirand
+#
+# 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 json
+from urllib.parse import urljoin
+
+from keystoneauth1 import adapter
+from osc_lib.cli import parseractions
+from osc_lib.command import command
+from osc_lib import exceptions
+from osc_lib import utils
+
+# Common column definitions
+COMMON_COLUMNS = (
+ 'id',
+ 'vm_id',
+ 'vm_name',
+ 'scheduled_time',
+ 'state',
+ 'workflow_exec'
+)
+
+LONG_COLUMNS = (
+ 'id',
+ 'vm_id',
+ 'vm_name',
+ 'scheduled_time',
+ 'state',
+ 'workflow_exec',
+ 'created_at',
+ 'updated_at'
+)
+
+
+class Client(adapter.Adapter):
+ """Client for the VM Migration Scheduler API."""
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('user_agent', 'python-vmmsclient')
+ kwargs.setdefault('version', '2')
+ super(Client, self).__init__(*args, **kwargs)
+ self.api_version = '2'
+
+ def _get_url(self, path):
+ """Build URL for API requests."""
+ # Ensure path starts with /
+ if not path.startswith('/'):
+ path = '/' + path
+ return path
+
+ def add_vm(self, vm_identifier, scheduled_time=None):
+ """Add a VM to migration scheduler."""
+ url = self._get_url('vms')
+ data = {
+ 'vm_identifier': vm_identifier
+ }
+ if scheduled_time:
+ data['scheduled_time'] = scheduled_time
+
+ try:
+ resp = self.post(url, json=data)
+ resp.raise_for_status()
+ return resp.json()
+ except Exception as e:
+ raise self._handle_exception(e)
+
+ def list_vms(self, state=None):
+ """List all VM migrations."""
+ url = self._get_url('vms')
+ if state:
+ url += f'?state={state}'
+ try:
+ resp = self.get(url)
+ resp.raise_for_status()
+ return resp.json()
+ except Exception as e:
+ raise self._handle_exception(e)
+
+ def remove_vm(self, migration_id):
+ """Remove a VM from migration scheduler."""
+ url = self._get_url('vms/{0}'.format(migration_id))
+ try:
+ resp = self.delete(url)
+ resp.raise_for_status()
+ return True
+ except Exception as e:
+ raise self._handle_exception(e)
+
+ def show_vm(self, migration_id):
+ """Show details of a specific VM migration."""
+ url = self._get_url('vms/{0}'.format(migration_id))
+ try:
+ resp = self.get(url)
+ resp.raise_for_status()
+ return resp.json()
+ except Exception as e:
+ raise self._handle_exception(e)
+
+ def update_vm(self, migration_id, scheduled_time=None, state=None):
+ """Update a VM migration."""
+ url = self._get_url('vms/{0}'.format(migration_id))
+ data = {}
+ if scheduled_time is not None:
+ data['scheduled_time'] = scheduled_time
+ if state is not None:
+ data['state'] = state
+ try:
+ resp = self.put(url, json=data)
+ resp.raise_for_status()
+ return resp.json()
+ except Exception as e:
+ raise self._handle_exception(e)
+
+ def unset_vm(self, migration_id, scheduled_time=False):
+ """Unset VM migration properties."""
+ url = self._get_url('vms/{0}'.format(migration_id))
+ data = {}
+ if scheduled_time:
+ data['scheduled_time'] = None
+ try:
+ resp = self.put(url, json=data)
+ resp.raise_for_status()
+ return resp.json()
+ except Exception as e:
+ raise self._handle_exception(e)
+
+ def _is_404_error(self, e):
+ """Check if exception represents a 404 Not Found error."""
+ # Check if it's already a NotFound exception
+ if isinstance(e, exceptions.NotFound):
+ return True
+ # Check if it's an HTTP error with 404 status code
+ if hasattr(e, 'response') and e.response is not None:
+ return getattr(e.response, 'status_code', None) == 404
+ return False
+
+ def _handle_exception(self, e):
+ """Handle exceptions and raise appropriate OSC exceptions."""
+ # Handle 404 errors by raising NotFound exception
+ if self._is_404_error(e):
+ raise exceptions.NotFound('Migration not found (404).')
+ # For other errors, raise CommandError
+ raise exceptions.CommandError(self._format_exception_message(e))
+
+ def _format_exception_message(self, e):
+ """Format exception message for better user experience."""
+ if hasattr(e, 'response') and e.response is not None:
+ try:
+ error_data = e.response.json()
+ if 'error' in error_data:
+ return error_data['error']
+ except:
+ pass
+ return f"HTTP {e.response.status_code}: {e.response.reason}"
+ return str(e)
+
+ def get_vm_output(self, migration_id):
+ """Get output from a VM migration.
+
+ :param migration_id: ID of the migration
+ :returns: Dictionary containing stdout, stderr, and exit_code
+ """
+ formatted_path = 'vms/{0}/output'.format(migration_id)
+ url = self._get_url(formatted_path)
+ try:
+ resp = self.get(url)
+ resp.raise_for_status()
+ return resp.json()
+ except Exception as e:
+ raise self._handle_exception(e)
+
+
+class ShowVMCommand(command.ShowOne):
+ """Show details of a VM migration."""
+
+ def get_parser(self, prog_name):
+ parser = super(ShowVMCommand, self).get_parser(prog_name)
+ parser.add_argument(
+ 'migration_id',
+ metavar='<migration-id>',
+ help='Migration ID')
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.vmms
+ data = client.show_vm(parsed_args.migration_id)
+
+ return COMMON_COLUMNS + ('created_at', 'updated_at'), utils.get_dict_properties(data, COMMON_COLUMNS + ('created_at', 'updated_at'))
+
+
+class AddVMCommand(command.ShowOne):
+ """Add a VM to migration scheduler."""
+
+ def get_parser(self, prog_name):
+ parser = super(AddVMCommand, self).get_parser(prog_name)
+ parser.add_argument(
+ 'vm_identifier',
+ metavar='<vm-uuid-or-name>',
+ help='VM UUID or name')
+ parser.add_argument(
+ '--schedule-time',
+ metavar='<schedule-time>',
+ help='Scheduled migration time (ISO format or "now")')
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.vmms
+
+ # Handle "now" keyword for schedule time
+ schedule_time = parsed_args.schedule_time
+ if schedule_time and schedule_time.lower() == 'now':
+ # Use current UTC time
+ import datetime
+ schedule_time = datetime.datetime.utcnow().isoformat() + 'Z'
+
+ data = client.add_vm(
+ vm_identifier=parsed_args.vm_identifier,
+ scheduled_time=schedule_time)
+
+ return COMMON_COLUMNS + ('created_at', 'updated_at'), utils.get_dict_properties(data, COMMON_COLUMNS + ('created_at', 'updated_at'))
+
+
+class ListVMsCommand(command.Lister):
+ """List VM migrations."""
+
+ def get_parser(self, prog_name):
+ parser = super(ListVMsCommand, self).get_parser(prog_name)
+ parser.add_argument(
+ '--long',
+ action='store_true',
+ help='Show detailed information including created_at and updated_at'
+ )
+ parser.add_argument(
+ '--state',
+ metavar='<state>',
+ choices=['SCHEDULED', 'MIGRATING', 'MIGRATED', 'ERROR'],
+ help='Filter by migration state'
+ )
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.vmms
+ data = client.list_vms(state=parsed_args.state)
+
+ columns = LONG_COLUMNS if parsed_args.long else COMMON_COLUMNS
+ return (
+ columns,
+ (utils.get_dict_properties(s, columns) for s in data)
+ )
+
+
+class RemoveVMCommand(command.Command):
+ """Remove a VM from migration scheduler."""
+
+ def get_parser(self, prog_name):
+ parser = super(RemoveVMCommand, self).get_parser(prog_name)
+ parser.add_argument(
+ 'migration_id',
+ metavar='<migration-id>',
+ help='Migration ID')
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.vmms
+ try:
+ client.remove_vm(parsed_args.migration_id)
+ self.app.stdout.write(
+ 'Migration {0} removed successfully\n'.format(
+ parsed_args.migration_id))
+ except exceptions.NotFound:
+ raise exceptions.CommandError(
+ 'Failed to remove migration {0}: not found'.format(
+ parsed_args.migration_id))
+
+
+class SetVMCommand(command.ShowOne):
+ """Set properties of a VM migration."""
+
+ def get_parser(self, prog_name):
+ parser = super(SetVMCommand, self).get_parser(prog_name)
+ parser.add_argument(
+ 'migration_id',
+ metavar='<migration-id>',
+ help='Migration ID')
+ parser.add_argument(
+ '--schedule-time',
+ metavar='<schedule-time>',
+ help='New scheduled migration time (ISO format or "now")')
+ parser.add_argument(
+ '--state',
+ metavar='<state>',
+ choices=['SCHEDULED', 'MIGRATING', 'MIGRATED', 'ERROR'],
+ help='New state (SCHEDULED, MIGRATING, MIGRATED, ERROR)')
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.vmms
+
+ # Prepare data based on provided arguments
+ if not parsed_args.schedule_time and not parsed_args.state:
+ raise exceptions.CommandError('Either --schedule-time or --state must be specified')
+
+ # Handle "now" keyword for schedule time
+ schedule_time = parsed_args.schedule_time
+ if schedule_time and schedule_time.lower() == 'now':
+ # Use current UTC time
+ import datetime
+ schedule_time = datetime.datetime.utcnow().isoformat() + 'Z'
+
+ data = client.update_vm(
+ migration_id=parsed_args.migration_id,
+ scheduled_time=schedule_time,
+ state=parsed_args.state)
+
+ return COMMON_COLUMNS + ('created_at', 'updated_at'), utils.get_dict_properties(data, COMMON_COLUMNS + ('created_at', 'updated_at'))
+
+
+class UnsetVMCommand(command.ShowOne):
+ """Unset properties of a VM migration."""
+
+ def get_parser(self, prog_name):
+ parser = super(UnsetVMCommand, self).get_parser(prog_name)
+ parser.add_argument(
+ 'migration_id',
+ metavar='<migration-id>',
+ help='Migration ID')
+ parser.add_argument(
+ '--schedule-time',
+ action='store_true',
+ help='Unset scheduled migration time')
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.vmms
+ data = client.unset_vm(
+ migration_id=parsed_args.migration_id,
+ scheduled_time=parsed_args.schedule_time)
+
+ return COMMON_COLUMNS + ('created_at', 'updated_at'), utils.get_dict_properties(data, COMMON_COLUMNS + ('created_at', 'updated_at'))
+
+class ShowVMOutputCommand(command.Command):
+ """Show output from VM migration."""
+
+ def get_parser(self, prog_name):
+ parser = super(ShowVMOutputCommand, self).get_parser(prog_name)
+ parser.add_argument(
+ 'migration_id',
+ metavar='<migration-id>',
+ help='Migration ID')
+ return parser
+
+ def take_action(self, parsed_args):
+ client = self.app.client_manager.vmms
+ try:
+ data = client.get_vm_output(parsed_args.migration_id)
+
+ # Show only stdout, equivalent to jq .stdout -r
+ if 'stdout' in data:
+ self.app.stdout.write(data['stdout'])
+ if not data['stdout'].endswith('\n'):
+ self.app.stdout.write('\n')
+ else:
+ self.app.stdout.write('\n')
+
+ except exceptions.NotFound:
+ raise exceptions.CommandError(
+ f'Failed to get output for migration {parsed_args.migration_id}: not found')
+ except Exception as e:
+ raise exceptions.CommandError(
+ f'Failed to get output for migration {parsed_args.migration_id}: {str(e)}')