From 47fb7896c3c004ea1abb7aa669c897e92b5d0163 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 16 Jul 2020 15:10:35 -0500 Subject: [PATCH 1/7] #874 added 'vs migrate' command --- SoftLayer/CLI/dedicatedhost/detail.py | 2 +- SoftLayer/CLI/routes.py | 1 + SoftLayer/CLI/virt/migrate.py | 80 +++++++++++++++++++++++++++ SoftLayer/managers/vs.py | 20 +++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 SoftLayer/CLI/virt/migrate.py 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 049b43c3b..424f58957 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..9e48fb208 --- /dev/null +++ b/SoftLayer/CLI/virt/migrate.py @@ -0,0 +1,80 @@ +"""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_filer = {'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_filer, 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]") + for vsi_object in require_migration: + migrate(vsi, guest) + # 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/managers/vs.py b/SoftLayer/managers/vs.py index e63d7a80f..d9df28704 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -1158,3 +1158,23 @@ 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) From 8a22d39708cde41729bb84a2c8d4d3555b195e61 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 16 Jul 2020 17:26:24 -0500 Subject: [PATCH 2/7] adding unit tests --- SoftLayer/CLI/virt/migrate.py | 2 +- SoftLayer/fixtures/SoftLayer_Virtual_Guest.py | 3 ++ SoftLayer/testing/__init__.py | 10 +++++ tests/CLI/modules/vs/vs_tests.py | 43 +++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/migrate.py b/SoftLayer/CLI/virt/migrate.py index 9e48fb208..5b97ea46b 100644 --- a/SoftLayer/CLI/virt/migrate.py +++ b/SoftLayer/CLI/virt/migrate.py @@ -61,7 +61,7 @@ def cli(env, guest, migrate_all, host): elif migrate_all: require_migration = vsi.list_instances(filter=pending_filter, mask="mask[id]") for vsi_object in require_migration: - migrate(vsi, guest) + migrate(vsi, vsi_object['id']) # Just migrate based on the options else: migrate(vsi, guest, host) diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py index c42963c8e..08742a784 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py @@ -767,3 +767,6 @@ } } ] + +migrate = True +migrateDedicatedHost = True \ No newline at end of file diff --git a/SoftLayer/testing/__init__.py b/SoftLayer/testing/__init__.py index 9c8b81c47..40a224aac 100644 --- a/SoftLayer/testing/__init__.py +++ b/SoftLayer/testing/__init__.py @@ -142,6 +142,16 @@ 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/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index a4bf28509..ee743a137 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -815,3 +815,46 @@ 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_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_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) From 3f8361834f46f17e56d6662922d3e5d466a0d175 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 17 Jul 2020 16:42:01 -0500 Subject: [PATCH 3/7] finishing up tests --- SoftLayer/fixtures/SoftLayer_Virtual_Guest.py | 2 +- SoftLayer/testing/__init__.py | 1 - tests/CLI/modules/vs/vs_tests.py | 11 ++++++++++- tests/managers/vs/vs_tests.py | 10 ++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py index 08742a784..755c284ff 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py @@ -769,4 +769,4 @@ ] migrate = True -migrateDedicatedHost = True \ No newline at end of file +migrateDedicatedHost = True diff --git a/SoftLayer/testing/__init__.py b/SoftLayer/testing/__init__.py index 40a224aac..f1404b423 100644 --- a/SoftLayer/testing/__init__.py +++ b/SoftLayer/testing/__init__.py @@ -151,7 +151,6 @@ def assert_not_called_with(self, service, method, **props): 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/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index ee743a137..cb451fa60 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -823,9 +823,18 @@ def test_vs_migrate_list(self): 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') diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py index 40fb3063f..1128e3b0e 100644 --- a/tests/managers/vs/vs_tests.py +++ b/tests/managers/vs/vs_tests.py @@ -1132,3 +1132,13 @@ 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) From e331f57803d2bde0fe15d3181d5ec9bad141afeb Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 17 Jul 2020 17:09:23 -0500 Subject: [PATCH 4/7] documentation --- docs/cli/vs.rst | 5 +++++ 1 file changed, 5 insertions(+) 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 ----------------- From 730ed0fc48b8c0f2257f3aab0bfc0efe1c58e27b Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 24 Jul 2020 16:59:16 -0500 Subject: [PATCH 5/7] fixed a typo --- SoftLayer/CLI/virt/migrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/virt/migrate.py b/SoftLayer/CLI/virt/migrate.py index 5b97ea46b..55ba251ec 100644 --- a/SoftLayer/CLI/virt/migrate.py +++ b/SoftLayer/CLI/virt/migrate.py @@ -20,7 +20,7 @@ def cli(env, guest, migrate_all, host): vsi = SoftLayer.VSManager(env.client) pending_filter = {'virtualGuests': {'pendingMigrationFlag': {'operation': 1}}} - dedicated_filer = {'virtualGuests': {'dedicatedHost': {'id': {'operation': 'not null'}}}} + dedicated_filter = {'virtualGuests': {'dedicatedHost': {'id': {'operation': 'not null'}}}} mask = """mask[ id, hostname, domain, datacenter, pendingMigrationFlag, powerState, primaryIpAddress,primaryBackendIpAddress, dedicatedHost @@ -44,7 +44,7 @@ def cli(env, guest, migrate_all, host): else: click.secho("No guests require migration at this time", fg='green') - migrateable = vsi.list_instances(filter=dedicated_filer, mask=mask) + 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: From 2e15494c935346bd85258b8b4a36f9ba340d0e8e Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 24 Jul 2020 17:58:16 -0500 Subject: [PATCH 6/7] tox fix --- tests/managers/vs/vs_tests.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py index fa2e7464e..a9f256c80 100644 --- a/tests/managers/vs/vs_tests.py +++ b/tests/managers/vs/vs_tests.py @@ -1133,7 +1133,6 @@ def test_get_local_disks_swap(self): } ], result) - def test_migrate(self): result = self.vs.migrate(1234) self.assertTrue(result) @@ -1175,4 +1174,3 @@ def test_get_hardware_guests(self): result = self.vs.get_hardware_guests() self.assertEqual("NSX-T Manager", result[0]['virtualHost']['guests'][0]['hostname']) - From 47f236c61000ce43b1581ad8d133136cb9e77e02 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 24 Jul 2020 18:18:34 -0500 Subject: [PATCH 7/7] added message for empty migrations --- SoftLayer/CLI/virt/migrate.py | 2 ++ tests/CLI/modules/vs/vs_tests.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/SoftLayer/CLI/virt/migrate.py b/SoftLayer/CLI/virt/migrate.py index 55ba251ec..d06673365 100644 --- a/SoftLayer/CLI/virt/migrate.py +++ b/SoftLayer/CLI/virt/migrate.py @@ -60,6 +60,8 @@ def cli(env, guest, migrate_all, host): # 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 diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index 60bc8183c..8278ab23f 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -836,6 +836,16 @@ def test_vs_migrate_all(self): 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)