diff --git a/SoftLayer/CLI/dedicatedhost/detail.py b/SoftLayer/CLI/dedicatedhost/detail.py index e1c46b962..ca966d03a 100644 --- a/SoftLayer/CLI/dedicatedhost/detail.py +++ b/SoftLayer/CLI/dedicatedhost/detail.py @@ -19,7 +19,7 @@ @click.option('--guests', is_flag=True, help='Show guests on dedicated host') @environment.pass_env def cli(env, identifier, price=False, guests=False): - """Get details for a virtual server.""" + """Get details for a dedicated host.""" dhost = SoftLayer.DedicatedHostManager(env.client) table = formatting.KeyValueTable(['name', 'value']) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index a84de0157..8d9c8b1e8 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -46,6 +46,7 @@ ('virtual:credentials', 'SoftLayer.CLI.virt.credentials:cli'), ('virtual:capacity', 'SoftLayer.CLI.virt.capacity:cli'), ('virtual:placementgroup', 'SoftLayer.CLI.virt.placementgroup:cli'), + ('virtual:migrate', 'SoftLayer.CLI.virt.migrate:cli'), ('dedicatedhost', 'SoftLayer.CLI.dedicatedhost'), ('dedicatedhost:list', 'SoftLayer.CLI.dedicatedhost.list:cli'), diff --git a/SoftLayer/CLI/virt/migrate.py b/SoftLayer/CLI/virt/migrate.py new file mode 100644 index 000000000..d06673365 --- /dev/null +++ b/SoftLayer/CLI/virt/migrate.py @@ -0,0 +1,82 @@ +"""Manage Migrations of Virtual Guests""" +# :license: MIT, see LICENSE for more details. +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + + +@click.command() +@click.option('--guest', '-g', type=click.INT, help="Guest ID to immediately migrate.") +@click.option('--all', '-a', 'migrate_all', is_flag=True, default=False, + help="Migrate ALL guests that require migration immediately.") +@click.option('--host', '-h', type=click.INT, + help="Dedicated Host ID to migrate to. Only works on guests that are already on a dedicated host.") +@environment.pass_env +def cli(env, guest, migrate_all, host): + """Manage VSIs that require migration. Can migrate Dedicated Host VSIs as well.""" + + vsi = SoftLayer.VSManager(env.client) + pending_filter = {'virtualGuests': {'pendingMigrationFlag': {'operation': 1}}} + dedicated_filter = {'virtualGuests': {'dedicatedHost': {'id': {'operation': 'not null'}}}} + mask = """mask[ + id, hostname, domain, datacenter, pendingMigrationFlag, powerState, + primaryIpAddress,primaryBackendIpAddress, dedicatedHost + ]""" + + # No options, just print out a list of guests that can be migrated + if not (guest or migrate_all): + require_migration = vsi.list_instances(filter=pending_filter, mask=mask) + require_table = formatting.Table(['id', 'hostname', 'domain', 'datacenter'], title="Require Migration") + + for vsi_object in require_migration: + require_table.add_row([ + vsi_object.get('id'), + vsi_object.get('hostname'), + vsi_object.get('domain'), + utils.lookup(vsi_object, 'datacenter', 'name') + ]) + + if require_migration: + env.fout(require_table) + else: + click.secho("No guests require migration at this time", fg='green') + + migrateable = vsi.list_instances(filter=dedicated_filter, mask=mask) + migrateable_table = formatting.Table(['id', 'hostname', 'domain', 'datacenter', 'Host Name', 'Host Id'], + title="Dedicated Guests") + for vsi_object in migrateable: + migrateable_table.add_row([ + vsi_object.get('id'), + vsi_object.get('hostname'), + vsi_object.get('domain'), + utils.lookup(vsi_object, 'datacenter', 'name'), + utils.lookup(vsi_object, 'dedicatedHost', 'name'), + utils.lookup(vsi_object, 'dedicatedHost', 'id') + ]) + env.fout(migrateable_table) + # Migrate all guests with pendingMigrationFlag=True + elif migrate_all: + require_migration = vsi.list_instances(filter=pending_filter, mask="mask[id]") + if not require_migration: + click.secho("No guests require migration at this time", fg='green') + for vsi_object in require_migration: + migrate(vsi, vsi_object['id']) + # Just migrate based on the options + else: + migrate(vsi, guest, host) + + +def migrate(vsi_manager, vsi_id, host_id=None): + """Handles actually migrating virtual guests and handling the exception""" + + try: + if host_id: + vsi_manager.migrate_dedicated(vsi_id, host_id) + else: + vsi_manager.migrate(vsi_id) + click.secho("Started a migration on {}".format(vsi_id), fg='green') + except SoftLayer.exceptions.SoftLayerAPIError as ex: + click.secho("Failed to migrate {}. {}".format(vsi_id, str(ex)), fg='red') diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py index c42963c8e..755c284ff 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py @@ -767,3 +767,6 @@ } } ] + +migrate = True +migrateDedicatedHost = True diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index a322c78a1..0e27c4cd3 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -1159,6 +1159,26 @@ def get_local_disks(self, instance_id): mask = 'mask[diskImage]' return self.guest.getBlockDevices(mask=mask, id=instance_id) + def migrate(self, instance_id): + """Calls SoftLayer_Virtual_Guest::migrate + + Only actually does anything if the virtual server requires a migration. + Will return an exception otherwise. + + :param int instance_id: Id of the virtual server + """ + return self.guest.migrate(id=instance_id) + + def migrate_dedicated(self, instance_id, host_id): + """Calls SoftLayer_Virtual_Guest::migrate + + Only actually does anything if the virtual server requires a migration. + Will return an exception otherwise. + + :param int instance_id: Id of the virtual server + """ + return self.guest.migrateDedicatedHost(host_id, id=instance_id) + def get_hardware_guests(self): """Returns all virtualHost capable hardware objects and their guests. diff --git a/SoftLayer/testing/__init__.py b/SoftLayer/testing/__init__.py index 9c8b81c47..f1404b423 100644 --- a/SoftLayer/testing/__init__.py +++ b/SoftLayer/testing/__init__.py @@ -142,6 +142,15 @@ def assert_called_with(self, service, method, **props): raise AssertionError('%s::%s was not called with given properties: %s' % (service, method, props)) + def assert_not_called_with(self, service, method, **props): + """Used to assert that API calls were NOT called with given properties. + + Props are properties of the given transport.Request object. + """ + + if self.calls(service, method, **props): + raise AssertionError('%s::%s was called with given properties: %s' % (service, method, props)) + def assert_no_fail(self, result): """Fail when a failing click result has an error""" if result.exception: diff --git a/docs/cli/vs.rst b/docs/cli/vs.rst index 49b99e09c..09539a72b 100644 --- a/docs/cli/vs.rst +++ b/docs/cli/vs.rst @@ -264,6 +264,11 @@ If no timezone is specified, IMS local time (CST) will be assumed, which might n :prog: virtual credentials :show-nested: +.. click:: SoftLayer.CLI.virt.migrate:cli + :prog: virtual migrate + :show-nested: + +Manages the migration of virutal guests. Supports migrating virtual guests on Dedicated Hosts as well. Reserved Capacity ----------------- diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index 783b52743..8278ab23f 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -802,3 +802,65 @@ def test_billing(self): } self.assert_no_fail(result) self.assertEqual(json.loads(result.output), vir_billing) + + def test_vs_migrate_list(self): + result = self.run_command(['vs', 'migrate']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getVirtualGuests') + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrate') + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrateDedicatedHost') + + def test_vs_migrate_list_empty(self): + mock = self.set_mock('SoftLayer_Account', 'getVirtualGuests') + mock.return_value = [] + result = self.run_command(['vs', 'migrate']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getVirtualGuests') + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrate') + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrateDedicatedHost') + self.assertIn("No guests require migration at this time", result.output) + + def test_vs_migrate_guest(self): + result = self.run_command(['vs', 'migrate', '-g', '100']) + self.assert_no_fail(result) + self.assertIn('Started a migration on', result.output) + self.assert_not_called_with('SoftLayer_Account', 'getVirtualGuests') + self.assert_called_with('SoftLayer_Virtual_Guest', 'migrate', identifier=100) + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrateDedicatedHost') + + def test_vs_migrate_all(self): + result = self.run_command(['vs', 'migrate', '-a']) + self.assert_no_fail(result) + self.assertIn('Started a migration on', result.output) + self.assert_called_with('SoftLayer_Virtual_Guest', 'migrate', identifier=100) + self.assert_called_with('SoftLayer_Virtual_Guest', 'migrate', identifier=104) + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrateDedicatedHost') + + def test_vs_migrate_all_empty(self): + mock = self.set_mock('SoftLayer_Account', 'getVirtualGuests') + mock.return_value = [] + result = self.run_command(['vs', 'migrate', '-a']) + self.assert_no_fail(result) + self.assertIn('No guests require migration at this time', result.output) + self.assert_called_with('SoftLayer_Virtual_Guest', 'migrate', identifier=100) + self.assert_called_with('SoftLayer_Virtual_Guest', 'migrate', identifier=104) + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrateDedicatedHost') + + def test_vs_migrate_dedicated(self): + result = self.run_command(['vs', 'migrate', '-g', '100', '-h', '999']) + self.assert_no_fail(result) + self.assertIn('Started a migration on', result.output) + self.assert_not_called_with('SoftLayer_Account', 'getVirtualGuests') + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrate', identifier=100) + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrateDedicatedHost', args=(999), identifier=100) + + def test_vs_migrate_exception(self): + ex = SoftLayerAPIError('SoftLayer_Exception', 'PROBLEM') + mock = self.set_mock('SoftLayer_Virtual_Guest', 'migrate') + mock.side_effect = ex + result = self.run_command(['vs', 'migrate', '-g', '100']) + self.assert_no_fail(result) + self.assertIn('Failed to migrate', result.output) + self.assert_not_called_with('SoftLayer_Account', 'getVirtualGuests') + self.assert_called_with('SoftLayer_Virtual_Guest', 'migrate', identifier=100) + self.assert_not_called_with('SoftLayer_Virtual_Guest', 'migrateDedicatedHost', args=(999), identifier=100) diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py index 34e07dd06..a9f256c80 100644 --- a/tests/managers/vs/vs_tests.py +++ b/tests/managers/vs/vs_tests.py @@ -1133,6 +1133,16 @@ def test_get_local_disks_swap(self): } ], result) + def test_migrate(self): + result = self.vs.migrate(1234) + self.assertTrue(result) + self.assert_called_with('SoftLayer_Virtual_Guest', 'migrate', identifier=1234) + + def test_migrate_dedicated(self): + result = self.vs.migrate_dedicated(1234, 5555) + self.assertTrue(result) + self.assert_called_with('SoftLayer_Virtual_Guest', 'migrateDedicatedHost', args=(5555,), identifier=1234) + def test_get_hardware_guests(self): mock = self.set_mock('SoftLayer_Account', 'getHardware') mock.return_value = [{ @@ -1163,5 +1173,4 @@ def test_get_hardware_guests(self): }]}}] result = self.vs.get_hardware_guests() - self.assertEqual("NSX-T Manager", result[0]['virtualHost']['guests'][0]['hostname'])