From 83266f6f9645eb2f9f9bf81dfbcbb117526e463d Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 8 Jul 2020 17:22:26 -0500 Subject: [PATCH 01/46] #828 refactored hardware create to use the ordering manager primarily instead of doing price lookups on its own. Added option to specify a particular networking keyname instead of just a speed --- SoftLayer/CLI/formatting.py | 9 +- SoftLayer/CLI/hardware/create.py | 67 ++---- SoftLayer/CLI/hardware/create_options.py | 28 ++- SoftLayer/managers/hardware.py | 211 +++++++---------- tests/CLI/modules/server_tests.py | 32 +-- tests/managers/hardware_tests.py | 289 +++++------------------ 6 files changed, 193 insertions(+), 443 deletions(-) diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py index b88fe056b..d02ceb1a1 100644 --- a/SoftLayer/CLI/formatting.py +++ b/SoftLayer/CLI/formatting.py @@ -301,8 +301,13 @@ def prettytable(self): else: msg = "Column (%s) doesn't exist to sort by" % self.sortby raise exceptions.CLIAbort(msg) - for a_col, alignment in self.align.items(): - table.align[a_col] = alignment + + if isinstance(self.align, str): + table.align = self.align + else: + # Required because PrettyTable has a strict setter function for alignment + for a_col, alignment in self.align.items(): + table.align[a_col] = alignment if self.title: table.title = self.title diff --git a/SoftLayer/CLI/hardware/create.py b/SoftLayer/CLI/hardware/create.py index 472cec2e2..eb83e6664 100644 --- a/SoftLayer/CLI/hardware/create.py +++ b/SoftLayer/CLI/hardware/create.py @@ -12,56 +12,40 @@ @click.command(epilog="See 'slcli server create-options' for valid options.") -@click.option('--hostname', '-H', - help="Host portion of the FQDN", - required=True, - prompt=True) -@click.option('--domain', '-D', - help="Domain portion of the FQDN", - required=True, - prompt=True) -@click.option('--size', '-s', - help="Hardware size", - required=True, - prompt=True) -@click.option('--os', '-o', help="OS install code", - required=True, - prompt=True) -@click.option('--datacenter', '-d', help="Datacenter shortname", - required=True, - prompt=True) -@click.option('--port-speed', - type=click.INT, - help="Port speeds", - required=True, - prompt=True) -@click.option('--billing', +@click.option('--hostname', '-H', required=True, prompt=True, + help="Host portion of the FQDN") +@click.option('--domain', '-D', required=True, prompt=True, + help="Domain portion of the FQDN") +@click.option('--size', '-s', required=True, prompt=True, + help="Hardware size") +@click.option('--os', '-o', required=True, prompt=True, + help="OS Key value") +@click.option('--datacenter', '-d', required=True, prompt=True, + help="Datacenter shortname") +@click.option('--port-speed', type=click.INT, required=True, prompt=True, + help="Port speeds. DEPRECATED, use --network") +@click.option('--no-public', is_flag=True, + help="Private network only. DEPRECATED, use --network.") +@click.option('--network', + help="Network Option Key.") +@click.option('--billing', default='hourly', show_default=True, type=click.Choice(['hourly', 'monthly']), - default='hourly', - show_default=True, help="Billing rate") -@click.option('--postinstall', '-i', help="Post-install script to download") -@helpers.multi_option('--key', '-k', - help="SSH keys to add to the root user") -@click.option('--no-public', - is_flag=True, - help="Private network only") -@helpers.multi_option('--extra', '-e', help="Extra options") -@click.option('--test', - is_flag=True, +@click.option('--postinstall', '-i', + help="Post-install script. Should be a HTTPS URL.") +@click.option('--test', is_flag=True, help="Do not actually create the server") -@click.option('--template', '-t', - is_eager=True, +@click.option('--template', '-t', is_eager=True, callback=template.TemplateCallback(list_args=['key']), help="A template file that defaults the command-line options", type=click.Path(exists=True, readable=True, resolve_path=True)) -@click.option('--export', - type=click.Path(writable=True, resolve_path=True), +@click.option('--export', type=click.Path(writable=True, resolve_path=True), help="Exports options to a template file") -@click.option('--wait', - type=click.INT, +@click.option('--wait', type=click.INT, help="Wait until the server is finished provisioning for up to " "X seconds before returning") +@helpers.multi_option('--key', '-k', help="SSH keys to add to the root user") +@helpers.multi_option('--extra', '-e', help="Extra option Key Names") @environment.pass_env def cli(env, **args): """Order/create a dedicated server.""" @@ -86,6 +70,7 @@ def cli(env, **args): 'port_speed': args.get('port_speed'), 'no_public': args.get('no_public') or False, 'extras': args.get('extra'), + 'network': args.get('network') } # Do not create hardware server with --test or --export diff --git a/SoftLayer/CLI/hardware/create_options.py b/SoftLayer/CLI/hardware/create_options.py index 4eba88c84..2e9949d09 100644 --- a/SoftLayer/CLI/hardware/create_options.py +++ b/SoftLayer/CLI/hardware/create_options.py @@ -19,36 +19,42 @@ def cli(env): tables = [] # Datacenters - dc_table = formatting.Table(['datacenter', 'value']) - dc_table.sortby = 'value' + dc_table = formatting.Table(['Datacenter', 'Value'], title="Datacenters") + dc_table.sortby = 'Value' + dc_table.align = 'l' for location in options['locations']: dc_table.add_row([location['name'], location['key']]) tables.append(dc_table) # Presets - preset_table = formatting.Table(['size', 'value']) - preset_table.sortby = 'value' + preset_table = formatting.Table(['Size', 'Value'], title="Sizes") + preset_table.sortby = 'Value' + preset_table.align = 'l' for size in options['sizes']: preset_table.add_row([size['name'], size['key']]) tables.append(preset_table) # Operating systems - os_table = formatting.Table(['operating_system', 'value', 'operatingSystemReferenceCode ']) - os_table.sortby = 'value' + os_table = formatting.Table(['OS', 'Key', 'Reference Code'], title="Operating Systems") + os_table.sortby = 'Key' + os_table.align = 'l' for operating_system in options['operating_systems']: os_table.add_row([operating_system['name'], operating_system['key'], operating_system['referenceCode']]) tables.append(os_table) # Port speed - port_speed_table = formatting.Table(['port_speed', 'value']) - port_speed_table.sortby = 'value' + port_speed_table = formatting.Table(['Network', 'Speed', 'Key'], title="Network Options") + port_speed_table.sortby = 'Speed' + port_speed_table.align = 'l' + for speed in options['port_speeds']: - port_speed_table.add_row([speed['name'], speed['key']]) + port_speed_table.add_row([speed['name'], speed['speed'], speed['key']]) tables.append(port_speed_table) # Extras - extras_table = formatting.Table(['extras', 'value']) - extras_table.sortby = 'value' + extras_table = formatting.Table(['Extra Option', 'Value'], title="Extras") + extras_table.sortby = 'Value' + extras_table.align = 'l' for extra in options['extras']: extras_table.add_row([extra['name'], extra['key']]) tables.append(extras_table) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index b531910e4..cccf6b29f 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -22,7 +22,9 @@ EXTRA_CATEGORIES = ['pri_ipv6_addresses', 'static_ipv6_addresses', - 'sec_ip_addresses'] + 'sec_ip_addresses', + 'trusted_platform_module', + 'software_guard_extensions'] class HardwareManager(utils.IdentifierMixin, object): @@ -53,6 +55,7 @@ def __init__(self, client, ordering_manager=None): self.hardware = self.client['Hardware_Server'] self.account = self.client['Account'] self.resolvers = [self._get_ids_from_ip, self._get_ids_from_hostname] + self.package_keyname = 'BARE_METAL_SERVER' if ordering_manager is None: self.ordering_manager = ordering.OrderingManager(client) else: @@ -337,16 +340,14 @@ def place_order(self, **kwargs): :param string os: operating system name :param int port_speed: Port speed in Mbps :param list ssh_keys: list of ssh key ids - :param string post_uri: The URI of the post-install script to run - after reload - :param boolean hourly: True if using hourly pricing (default). - False for monthly. - :param boolean no_public: True if this server should only have private - interfaces + :param string post_uri: The URI of the post-install script to run after reload + :param boolean hourly: True if using hourly pricing (default). False for monthly. + :param boolean no_public: True if this server should only have private interfaces :param list extras: List of extra feature names """ create_options = self._generate_create_dict(**kwargs) - return self.client['Product_Order'].placeOrder(create_options) + return self.ordering_manager.place_order(**create_options) + # return self.client['Product_Order'].placeOrder(create_options) def verify_order(self, **kwargs): """Verifies an order for a piece of hardware. @@ -354,7 +355,7 @@ def verify_order(self, **kwargs): See :func:`place_order` for a list of available options. """ create_options = self._generate_create_dict(**kwargs) - return self.client['Product_Order'].verifyOrder(create_options) + return self.ordering_manager.verify_order(**create_options) def get_cancellation_reasons(self): """Returns a dictionary of valid cancellation reasons. @@ -397,33 +398,27 @@ def get_create_options(self): 'key': preset['keyName'] }) - # Operating systems operating_systems = [] + port_speeds = [] + extras = [] for item in package['items']: - if item['itemCategory']['categoryCode'] == 'os': + category = item['itemCategory']['categoryCode'] + # Operating systems + if category == 'os': operating_systems.append({ 'name': item['softwareDescription']['longDescription'], 'key': item['keyName'], 'referenceCode': item['softwareDescription']['referenceCode'] }) - - # Port speeds - port_speeds = [] - for item in package['items']: - if all([item['itemCategory']['categoryCode'] == 'port_speed', - # Hide private options - not _is_private_port_speed_item(item), - # Hide unbonded options - _is_bonded(item)]): + # Port speeds + elif category == 'port_speed': port_speeds.append({ 'name': item['description'], - 'key': item['capacity'], + 'speed': item['capacity'], + 'key': item['keyName'] }) - - # Extras - extras = [] - for item in package['items']: - if item['itemCategory']['categoryCode'] in EXTRA_CATEGORIES: + # Extras + elif category in EXTRA_CATEGORIES: extras.append({ 'name': item['description'], 'key': item['keyName'] @@ -454,9 +449,7 @@ def _get_package(self): accountRestrictedActivePresets, regions[location[location[priceGroups]]] ''' - - package_keyname = 'BARE_METAL_SERVER' - package = self.ordering_manager.get_package_by_key(package_keyname, mask=mask) + package = self.ordering_manager.get_package_by_key(self.package_keyname, mask=mask) return package def _generate_create_dict(self, @@ -470,59 +463,66 @@ def _generate_create_dict(self, post_uri=None, hourly=True, no_public=False, - extras=None): + extras=None, + network=None): """Translates arguments into a dictionary for creating a server.""" extras = extras or [] package = self._get_package() - location = _get_location(package, location) - - prices = [] - for category in ['pri_ip_addresses', - 'vpn_management', - 'remote_management']: - prices.append(_get_default_price_id(package['items'], - option=category, - hourly=hourly, - location=location)) - - prices.append(_get_os_price_id(package['items'], os, - location=location)) - prices.append(_get_bandwidth_price_id(package['items'], - hourly=hourly, - no_public=no_public, - location=location)) - prices.append(_get_port_speed_price_id(package['items'], - port_speed, - no_public, - location=location)) + items = package.get('items', {}) + location_id = _get_location(package, location) + + key_names = [ + '1_IP_ADDRESS', + 'UNLIMITED_SSL_VPN_USERS_1_PPTP_VPN_USER_PER_ACCOUNT', + 'REBOOT_KVM_OVER_IP' + ] + + # Operating System + key_names.append(os) + + # Bandwidth Options + key_names.append( + _get_bandwidth_key(items, hourly=hourly, no_public=no_public, location=location_id) + ) + + # Port Speed Options + # New option in v5.9.0 + if network: + key_names.append(network) + # Legacy Option, doesn't support bonded/redundant + else: + key_names.append( + _get_port_speed_key(items, port_speed, no_public, location=location_id) + ) + # Extras for extra in extras: - prices.append(_get_extra_price_id(package['items'], - extra, hourly, - location=location)) + key_names.append(extra) - hardware = { - 'hostname': hostname, - 'domain': domain, + extras = { + 'hardware': [{ + 'hostname': hostname, + 'domain': domain, + }] } - - order = { - 'hardware': [hardware], - 'location': location['keyname'], - 'prices': [{'id': price} for price in prices], - 'packageId': package['id'], - 'presetId': _get_preset_id(package, size), - 'useHourlyPricing': hourly, - } - if post_uri: - order['provisionScripts'] = [post_uri] + extras['provisionScripts'] = [post_uri] if ssh_keys: - order['sshKeys'] = [{'sshKeyIds': ssh_keys}] + extras['sshKeys'] = [{'sshKeyIds': ssh_keys}] + order = { + 'package_keyname': self.package_keyname, + 'location': location, + 'item_keynames': key_names, + 'complex_type': 'SoftLayer_Container_Product_Order_Hardware_Server', + 'hourly': hourly, + 'preset_keyname': size, + 'extras': extras, + 'quantity': 1, + } return order def _get_ids_from_hostname(self, hostname): @@ -745,78 +745,38 @@ def _get_extra_price_id(items, key_name, hourly, location): return price['id'] - raise SoftLayerError( - "Could not find valid price for extra option, '%s'" % key_name) - + raise SoftLayerError("Could not find valid price for extra option, '%s'" % key_name) -def _get_default_price_id(items, option, hourly, location): - """Returns a 'free' price id given an option.""" - for item in items: - if utils.lookup(item, 'itemCategory', 'categoryCode') != option: - continue - - for price in item['prices']: - if all([float(price.get('hourlyRecurringFee', 0)) == 0.0, - float(price.get('recurringFee', 0)) == 0.0, - _matches_billing(price, hourly), - _matches_location(price, location)]): - return price['id'] - - raise SoftLayerError( - "Could not find valid price for '%s' option" % option) - - -def _get_bandwidth_price_id(items, - hourly=True, - no_public=False, - location=None): - """Choose a valid price id for bandwidth.""" +def _get_bandwidth_key(items, hourly=True, no_public=False, location=None): + """Picks a valid Bandwidth Item, returns the KeyName""" + keyName = None # Prefer pay-for-use data transfer with hourly for item in items: capacity = float(item.get('capacity', 0)) # Hourly and private only do pay-as-you-go bandwidth - if any([utils.lookup(item, - 'itemCategory', - 'categoryCode') != 'bandwidth', + if any([utils.lookup(item, 'itemCategory', 'categoryCode') != 'bandwidth', (hourly or no_public) and capacity != 0.0, not (hourly or no_public) and capacity == 0.0]): continue + keyName = item['keyName'] for price in item['prices']: if not _matches_billing(price, hourly): continue if not _matches_location(price, location): continue + return keyName - return price['id'] - - raise SoftLayerError( - "Could not find valid price for bandwidth option") - - -def _get_os_price_id(items, os, location): - """Returns the price id matching.""" - - for item in items: - if any([utils.lookup(item, 'itemCategory', 'categoryCode') != 'os', - utils.lookup(item, 'keyName') != os]): - continue - - for price in item['prices']: - if not _matches_location(price, location): - continue - - return price['id'] + raise SoftLayerError("Could not find valid price for bandwidth option") - raise SoftLayerError("Could not find valid price for os: '%s'" % os) - -def _get_port_speed_price_id(items, port_speed, no_public, location): +def _get_port_speed_key(items, port_speed, no_public, location): """Choose a valid price id for port speed.""" + keyName = None for item in items: if utils.lookup(item, 'itemCategory', 'categoryCode') != 'port_speed': continue @@ -826,12 +786,12 @@ def _get_port_speed_price_id(items, port_speed, no_public, location): _is_private_port_speed_item(item) != no_public, not _is_bonded(item)]): continue - + keyName = item['keyName'] for price in item['prices']: if not _matches_location(price, location): continue - return price['id'] + return keyName raise SoftLayerError( "Could not find valid price for port speed: '%s'" % port_speed) @@ -883,12 +843,3 @@ def _get_location(package, location): return region raise SoftLayerError("Could not find valid location for: '%s'" % location) - - -def _get_preset_id(package, size): - """Get the preset id given the keyName of the preset.""" - for preset in package['activePresets'] + package['accountRestrictedActivePresets']: - if preset['keyName'] == size or preset['id'] == size: - return preset['id'] - - raise SoftLayerError("Could not find valid size for: '%s'" % size) diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index 605bd32bf..d016c7730 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -355,19 +355,10 @@ def test_create_options(self): result = self.run_command(['server', 'create-options']) self.assert_no_fail(result) - expected = [ - [{'datacenter': 'Washington 1', 'value': 'wdc01'}], - [{'size': 'Single Xeon 1270, 8GB Ram, 2x1TB SATA disks, Non-RAID', - 'value': 'S1270_8GB_2X1TBSATA_NORAID'}, - {'size': 'Dual Xeon Gold, 384GB Ram, 4x960GB SSD, RAID 10', - 'value': 'DGOLD_6140_384GB_4X960GB_SSD_SED_RAID_10'}], - [{'operating_system': 'Ubuntu / 14.04-64', - 'value': 'OS_UBUNTU_14_04_LTS_TRUSTY_TAHR_64_BIT', - 'operatingSystemReferenceCode ': 'UBUNTU_14_64'}], - [{'port_speed': '10 Mbps Public & Private Network Uplinks', - 'value': '10'}], - [{'extras': '1 IPv6 Address', 'value': '1_IPV6_ADDRESS'}]] - self.assertEqual(json.loads(result.output), expected) + output = json.loads(result.output) + self.assertEqual(output[0][0]['Value'], 'wdc01') + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects') + @mock.patch('SoftLayer.HardwareManager.place_order') def test_create_server(self, order_mock): @@ -391,21 +382,6 @@ def test_create_server(self, order_mock): self.assertEqual(json.loads(result.output), {'id': 98765, 'created': '2013-08-02 15:23:47'}) - def test_create_server_missing_required(self): - - # This is missing a required argument - result = self.run_command(['server', 'create', - # Note: no chassis id - '--hostname=test', - '--domain=example.com', - '--datacenter=TEST00', - '--network=100', - '--os=UBUNTU_12_64_MINIMAL', - ]) - - self.assertEqual(result.exit_code, 2) - self.assertIsInstance(result.exception, SystemExit) - @mock.patch('SoftLayer.CLI.template.export_to_template') def test_create_server_with_export(self, export_mock): if (sys.platform.startswith("win")): diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index f6a2445d1..2e3d7b3a5 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -7,6 +7,7 @@ import copy import mock +from pprint import pprint as pp import SoftLayer from SoftLayer import fixtures @@ -117,29 +118,29 @@ def test_reload(self): def test_get_create_options(self): options = self.hardware.get_create_options() - expected = { - 'extras': [{'key': '1_IPV6_ADDRESS', 'name': '1 IPv6 Address'}], - 'locations': [{'key': 'wdc01', 'name': 'Washington 1'}], - 'operating_systems': [{'key': 'OS_UBUNTU_14_04_LTS_TRUSTY_TAHR_64_BIT', - 'name': 'Ubuntu / 14.04-64', - 'referenceCode': 'UBUNTU_14_64'}], - 'port_speeds': [{ - 'key': '10', - 'name': '10 Mbps Public & Private Network Uplinks' - }], - 'sizes': [ - { - 'key': 'S1270_8GB_2X1TBSATA_NORAID', - 'name': 'Single Xeon 1270, 8GB Ram, 2x1TB SATA disks, Non-RAID' - }, - { - 'key': 'DGOLD_6140_384GB_4X960GB_SSD_SED_RAID_10', - 'name': 'Dual Xeon Gold, 384GB Ram, 4x960GB SSD, RAID 10' - } - ] + extras = {'key': '1_IPV6_ADDRESS', 'name': '1 IPv6 Address'} + locations = {'key': 'wdc01', 'name': 'Washington 1'} + operating_systems = { + 'key': 'OS_UBUNTU_14_04_LTS_TRUSTY_TAHR_64_BIT', + 'name': 'Ubuntu / 14.04-64', + 'referenceCode': 'UBUNTU_14_64' + } + + port_speeds = { + 'key': '10', + 'name': '10 Mbps Public & Private Network Uplinks' + } + sizes = { + 'key': 'S1270_8GB_2X1TBSATA_NORAID', + 'name': 'Single Xeon 1270, 8GB Ram, 2x1TB SATA disks, Non-RAID' } - self.assertEqual(options, expected) + self.assertEqual(options['extras'][0], extras) + self.assertEqual(options['locations'][0], locations) + self.assertEqual(options['operating_systems'][0], operating_systems) + self.assertEqual(options['port_speeds'][0]['name'], port_speeds['name']) + self.assertEqual(options['sizes'][0], sizes) + def test_get_create_options_package_missing(self): packages = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') @@ -148,18 +149,6 @@ def test_get_create_options_package_missing(self): ex = self.assertRaises(SoftLayer.SoftLayerError, self.hardware.get_create_options) self.assertEqual("Package BARE_METAL_SERVER does not exist", str(ex)) - def test_generate_create_dict_no_items(self): - packages = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - packages_copy = copy.deepcopy( - fixtures.SoftLayer_Product_Package.getAllObjects) - packages_copy[0]['items'] = [] - packages.return_value = packages_copy - - ex = self.assertRaises(SoftLayer.SoftLayerError, - self.hardware._generate_create_dict, - location="wdc01") - self.assertIn("Could not find valid price", str(ex)) - def test_generate_create_dict_no_regions(self): packages = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') packages_copy = copy.deepcopy( @@ -172,20 +161,6 @@ def test_generate_create_dict_no_regions(self): **MINIMAL_TEST_CREATE_ARGS) self.assertIn("Could not find valid location for: 'wdc01'", str(ex)) - def test_generate_create_dict_invalid_size(self): - args = { - 'size': 'UNKNOWN_SIZE', - 'hostname': 'unicorn', - 'domain': 'giggles.woo', - 'location': 'wdc01', - 'os': 'OS_UBUNTU_14_04_LTS_TRUSTY_TAHR_64_BIT', - 'port_speed': 10, - } - - ex = self.assertRaises(SoftLayer.SoftLayerError, - self.hardware._generate_create_dict, **args) - self.assertIn("Could not find valid size for: 'UNKNOWN_SIZE'", str(ex)) - def test_generate_create_dict(self): args = { 'size': 'S1270_8GB_2X1TBSATA_NORAID', @@ -199,51 +174,59 @@ def test_generate_create_dict(self): 'post_uri': 'http://example.com/script.php', 'ssh_keys': [10], } - - expected = { + + package = 'BARE_METAL_SERVER' + location = 'wdc01' + item_keynames = [ + '1_IP_ADDRESS', + 'UNLIMITED_SSL_VPN_USERS_1_PPTP_VPN_USER_PER_ACCOUNT', + 'REBOOT_KVM_OVER_IP', + 'OS_UBUNTU_14_04_LTS_TRUSTY_TAHR_64_BIT', + 'BANDWIDTH_0_GB_2', + '10_MBPS_PUBLIC_PRIVATE_NETWORK_UPLINKS', + '1_IPV6_ADDRESS' + ] + hourly = True + preset_keyname = 'S1270_8GB_2X1TBSATA_NORAID' + extras = { 'hardware': [{ 'domain': 'giggles.woo', 'hostname': 'unicorn', }], - 'location': 'WASHINGTON_DC', - 'packageId': 200, - 'presetId': 64, - 'prices': [{'id': 21}, - {'id': 420}, - {'id': 906}, - {'id': 37650}, - {'id': 1800}, - {'id': 272}, - {'id': 17129}], - 'useHourlyPricing': True, 'provisionScripts': ['http://example.com/script.php'], - 'sshKeys': [{'sshKeyIds': [10]}], + 'sshKeys' : [{'sshKeyIds': [10]}] } data = self.hardware._generate_create_dict(**args) - self.assertEqual(expected, data) + self.assertEqual(package, data['package_keyname']) + self.assertEqual(location, data['location']) + for keyname in item_keynames: + self.assertIn(keyname, data['item_keynames']) + self.assertEqual(extras, data['extras']) - @mock.patch('SoftLayer.managers.hardware.HardwareManager' - '._generate_create_dict') - def test_verify_order(self, create_dict): + + @mock.patch('SoftLayer.managers.ordering.OrderingManager.verify_order') + @mock.patch('SoftLayer.managers.hardware.HardwareManager._generate_create_dict') + def test_verify_order(self, create_dict, verify_order): create_dict.return_value = {'test': 1, 'verify': 1} + verify_order.return_value = {'test': 2} - self.hardware.verify_order(test=1, verify=1) + result = self.hardware.verify_order(test=1, verify=1) + self.assertEqual(2, result['test']) create_dict.assert_called_once_with(test=1, verify=1) - self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder', - args=({'test': 1, 'verify': 1},)) + verify_order.assert_called_once_with(test=1, verify=1) - @mock.patch('SoftLayer.managers.hardware.HardwareManager' - '._generate_create_dict') - def test_place_order(self, create_dict): + @mock.patch('SoftLayer.managers.ordering.OrderingManager.place_order') + @mock.patch('SoftLayer.managers.hardware.HardwareManager._generate_create_dict') + def test_place_order(self, create_dict, place_order): create_dict.return_value = {'test': 1, 'verify': 1} - self.hardware.place_order(test=1, verify=1) - + place_order.return_value = {'test': 1} + result = self.hardware.place_order(test=1, verify=1) + self.assertEqual(1, result['test']) create_dict.assert_called_once_with(test=1, verify=1) - self.assert_called_with('SoftLayer_Product_Order', 'placeOrder', - args=({'test': 1, 'verify': 1},)) + place_order.assert_called_once_with(test=1, verify=1) def test_cancel_hardware_without_reason(self): mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') @@ -629,162 +612,6 @@ def test_get_hard_drive_empty(self): class HardwareHelperTests(testing.TestCase): - def test_get_extra_price_id_no_items(self): - ex = self.assertRaises(SoftLayer.SoftLayerError, - managers.hardware._get_extra_price_id, - [], 'test', True, None) - self.assertEqual("Could not find valid price for extra option, 'test'", str(ex)) - - def test_get_extra_price_mismatched(self): - items = [ - {'keyName': 'TEST', 'prices': [{'id': 1, 'locationGroupId': None, 'recurringFee': 99}]}, - {'keyName': 'TEST', 'prices': [{'id': 2, 'locationGroupId': 55, 'hourlyRecurringFee': 99}]}, - {'keyName': 'TEST', 'prices': [{'id': 3, 'locationGroupId': None, 'hourlyRecurringFee': 99}]}, - ] - location = { - 'location': { - 'location': { - 'priceGroups': [ - {'id': 50}, - {'id': 51} - ] - } - } - } - result = managers.hardware._get_extra_price_id(items, 'TEST', True, location) - self.assertEqual(3, result) - - def test_get_bandwidth_price_mismatched(self): - items = [ - {'itemCategory': {'categoryCode': 'bandwidth'}, - 'capacity': 100, - 'prices': [{'id': 1, 'locationGroupId': None, 'hourlyRecurringFee': 99}] - }, - {'itemCategory': {'categoryCode': 'bandwidth'}, - 'capacity': 100, - 'prices': [{'id': 2, 'locationGroupId': 55, 'recurringFee': 99}] - }, - {'itemCategory': {'categoryCode': 'bandwidth'}, - 'capacity': 100, - 'prices': [{'id': 3, 'locationGroupId': None, 'recurringFee': 99}] - }, - ] - location = { - 'location': { - 'location': { - 'priceGroups': [ - {'id': 50}, - {'id': 51} - ] - } - } - } - result = managers.hardware._get_bandwidth_price_id(items, False, False, location) - self.assertEqual(3, result) - - def test_get_os_price_mismatched(self): - items = [ - {'itemCategory': {'categoryCode': 'os'}, - 'keyName': 'OS_TEST', - 'prices': [{'id': 2, 'locationGroupId': 55, 'recurringFee': 99}] - }, - {'itemCategory': {'categoryCode': 'os'}, - 'keyName': 'OS_TEST', - 'prices': [{'id': 3, 'locationGroupId': None, 'recurringFee': 99}] - }, - ] - location = { - 'location': { - 'location': { - 'priceGroups': [ - {'id': 50}, - {'id': 51} - ] - } - } - } - result = managers.hardware._get_os_price_id(items, 'OS_TEST', location) - self.assertEqual(3, result) - - def test_get_default_price_id_item_not_first(self): - items = [{ - 'itemCategory': {'categoryCode': 'unknown', 'id': 325}, - 'keyName': 'UNKNOWN', - 'prices': [{'accountRestrictions': [], - 'currentPriceFlag': '', - 'hourlyRecurringFee': '10.0', - 'id': 1245172, - 'recurringFee': '1.0'}], - }] - ex = self.assertRaises(SoftLayer.SoftLayerError, - managers.hardware._get_default_price_id, - items, 'unknown', True, None) - self.assertEqual("Could not find valid price for 'unknown' option", str(ex)) - - def test_get_default_price_id_no_items(self): - ex = self.assertRaises(SoftLayer.SoftLayerError, - managers.hardware._get_default_price_id, - [], 'test', True, None) - self.assertEqual("Could not find valid price for 'test' option", str(ex)) - - def test_get_bandwidth_price_id_no_items(self): - ex = self.assertRaises(SoftLayer.SoftLayerError, - managers.hardware._get_bandwidth_price_id, - [], hourly=True, no_public=False) - self.assertEqual("Could not find valid price for bandwidth option", str(ex)) - - def test_get_os_price_id_no_items(self): - ex = self.assertRaises(SoftLayer.SoftLayerError, - managers.hardware._get_os_price_id, - [], 'UBUNTU_14_64', None) - self.assertEqual("Could not find valid price for os: 'UBUNTU_14_64'", str(ex)) - - def test_get_port_speed_price_id_no_items(self): - ex = self.assertRaises(SoftLayer.SoftLayerError, - managers.hardware._get_port_speed_price_id, - [], 10, True, None) - self.assertEqual("Could not find valid price for port speed: '10'", str(ex)) - - def test_get_port_speed_price_id_mismatch(self): - items = [ - {'itemCategory': {'categoryCode': 'port_speed'}, - 'capacity': 101, - 'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}], - 'prices': [{'id': 1, 'locationGroupId': None, 'recurringFee': 99}] - }, - {'itemCategory': {'categoryCode': 'port_speed'}, - 'capacity': 100, - 'attributes': [{'attributeTypeKeyName': 'IS_NOT_PRIVATE_NETWORK_ONLY'}], - 'prices': [{'id': 2, 'locationGroupId': 55, 'recurringFee': 99}] - }, - {'itemCategory': {'categoryCode': 'port_speed'}, - 'capacity': 100, - 'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}, {'attributeTypeKeyName': 'NON_LACP'}], - 'prices': [{'id': 3, 'locationGroupId': 55, 'recurringFee': 99}] - }, - {'itemCategory': {'categoryCode': 'port_speed'}, - 'capacity': 100, - 'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}], - 'prices': [{'id': 4, 'locationGroupId': 12, 'recurringFee': 99}] - }, - {'itemCategory': {'categoryCode': 'port_speed'}, - 'capacity': 100, - 'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}], - 'prices': [{'id': 5, 'locationGroupId': None, 'recurringFee': 99}] - }, - ] - location = { - 'location': { - 'location': { - 'priceGroups': [ - {'id': 50}, - {'id': 51} - ] - } - } - } - result = managers.hardware._get_port_speed_price_id(items, 100, True, location) - self.assertEqual(5, result) def test_matches_location(self): price = {'id': 1, 'locationGroupId': 51, 'recurringFee': 99} From b6933ebc0365a7ddb559e6989d916d0978fe5832 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 9 Jul 2020 17:26:12 -0500 Subject: [PATCH 02/46] #828 full unit test coverage --- SoftLayer/CLI/hardware/create.py | 47 ++++------- SoftLayer/managers/hardware.py | 27 +++--- tests/CLI/modules/server_tests.py | 25 +++++- tests/managers/hardware_tests.py | 132 ++++++++++++++++++++++++++---- 4 files changed, 168 insertions(+), 63 deletions(-) diff --git a/SoftLayer/CLI/hardware/create.py b/SoftLayer/CLI/hardware/create.py index eb83e6664..40fa871bc 100644 --- a/SoftLayer/CLI/hardware/create.py +++ b/SoftLayer/CLI/hardware/create.py @@ -12,38 +12,25 @@ @click.command(epilog="See 'slcli server create-options' for valid options.") -@click.option('--hostname', '-H', required=True, prompt=True, - help="Host portion of the FQDN") -@click.option('--domain', '-D', required=True, prompt=True, - help="Domain portion of the FQDN") -@click.option('--size', '-s', required=True, prompt=True, - help="Hardware size") -@click.option('--os', '-o', required=True, prompt=True, - help="OS Key value") -@click.option('--datacenter', '-d', required=True, prompt=True, - help="Datacenter shortname") -@click.option('--port-speed', type=click.INT, required=True, prompt=True, - help="Port speeds. DEPRECATED, use --network") -@click.option('--no-public', is_flag=True, - help="Private network only. DEPRECATED, use --network.") -@click.option('--network', - help="Network Option Key.") -@click.option('--billing', default='hourly', show_default=True, - type=click.Choice(['hourly', 'monthly']), +@click.option('--hostname', '-H', required=True, prompt=True, help="Host portion of the FQDN") +@click.option('--domain', '-D', required=True, prompt=True, help="Domain portion of the FQDN") +@click.option('--size', '-s', required=True, prompt=True, help="Hardware size") +@click.option('--os', '-o', required=True, prompt=True, help="OS Key value") +@click.option('--datacenter', '-d', required=True, prompt=True, help="Datacenter shortname") +@click.option('--port-speed', type=click.INT, help="Port speeds. DEPRECATED, use --network") +@click.option('--no-public', is_flag=True, help="Private network only. DEPRECATED, use --network.") +@click.option('--network', help="Network Option Key. Use instead of port-speed option") +@click.option('--billing', default='hourly', show_default=True, type=click.Choice(['hourly', 'monthly']), help="Billing rate") -@click.option('--postinstall', '-i', - help="Post-install script. Should be a HTTPS URL.") -@click.option('--test', is_flag=True, - help="Do not actually create the server") -@click.option('--template', '-t', is_eager=True, +@click.option('--postinstall', '-i', help="Post-install script. Should be a HTTPS URL.") +@click.option('--test', is_flag=True, help="Do not actually create the server") +@click.option('--template', '-t', is_eager=True, type=click.Path(exists=True, readable=True, resolve_path=True), callback=template.TemplateCallback(list_args=['key']), - help="A template file that defaults the command-line options", - type=click.Path(exists=True, readable=True, resolve_path=True)) + help="A template file that defaults the command-line options") @click.option('--export', type=click.Path(writable=True, resolve_path=True), help="Exports options to a template file") @click.option('--wait', type=click.INT, - help="Wait until the server is finished provisioning for up to " - "X seconds before returning") + help="Wait until the server is finished provisioning for up to X seconds before returning") @helpers.multi_option('--key', '-k', help="SSH keys to add to the root user") @helpers.multi_option('--extra', '-e', help="Extra option Key Names") @environment.pass_env @@ -101,15 +88,13 @@ def cli(env, **args): if args['export']: export_file = args.pop('export') - template.export_to_template(export_file, args, - exclude=['wait', 'test']) + template.export_to_template(export_file, args, exclude=['wait', 'test']) env.fout('Successfully exported options to a template file.') return if do_create: if not (env.skip_confirmations or formatting.confirm( - "This action will incur charges on your account. " - "Continue?")): + "This action will incur charges on your account. Continue?")): raise exceptions.CLIAbort('Aborting dedicated server order.') result = mgr.place_order(**order) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index cccf6b29f..136b22c0a 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -729,23 +729,23 @@ def get_hard_drives(self, instance_id): return self.hardware.getHardDrives(id=instance_id) -def _get_extra_price_id(items, key_name, hourly, location): - """Returns a price id attached to item with the given key_name.""" +# def _get_extra_price_id(items, key_name, hourly, location): +# """Returns a price id attached to item with the given key_name.""" - for item in items: - if utils.lookup(item, 'keyName') != key_name: - continue +# for item in items: +# if utils.lookup(item, 'keyName') != key_name: +# continue - for price in item['prices']: - if not _matches_billing(price, hourly): - continue +# for price in item['prices']: +# if not _matches_billing(price, hourly): +# continue - if not _matches_location(price, location): - continue +# if not _matches_location(price, location): +# continue - return price['id'] +# return price['id'] - raise SoftLayerError("Could not find valid price for extra option, '%s'" % key_name) +# raise SoftLayerError("Could not find valid price for extra option, '%s'" % key_name) def _get_bandwidth_key(items, hourly=True, no_public=False, location=None): @@ -793,8 +793,7 @@ def _get_port_speed_key(items, port_speed, no_public, location): return keyName - raise SoftLayerError( - "Could not find valid price for port speed: '%s'" % port_speed) + raise SoftLayerError("Could not find valid price for port speed: '%s'" % port_speed) def _matches_billing(price, hourly): diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index d016c7730..388fc310d 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -359,7 +359,6 @@ def test_create_options(self): self.assertEqual(output[0][0]['Value'], 'wdc01') self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects') - @mock.patch('SoftLayer.HardwareManager.place_order') def test_create_server(self, order_mock): order_mock.return_value = { @@ -860,3 +859,27 @@ def test_billing(self): } self.assert_no_fail(result) self.assertEqual(json.loads(result.output), billing_json) + + def test_create_hw_export(self): + if(sys.platform.startswith("win")): + self.skipTest("Temp files do not work properly in Windows.") + with tempfile.NamedTemporaryFile() as config_file: + result = self.run_command(['hw', 'create', '--hostname=test', '--export', config_file.name, + '--domain=example.com', '--datacenter=TEST00', + '--network=TEST_NETWORK', '--os=UBUNTU_12_64', + '--size=S1270_8GB_2X1TBSATA_NORAID']) + self.assert_no_fail(result) + self.assertTrue('Successfully exported options to a template file.' in result.output) + contents = config_file.read().decode("utf-8") + self.assertIn('hostname=TEST', contents) + self.assertIn('size=S1270_8GB_2X1TBSATA_NORAID', contents) + + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_create_hw_no_confirm(self, confirm_mock): + confirm_mock.return_value = False + + result = self.run_command(['hw', 'create', '--hostname=test', '--size=S1270_8GB_2X1TBSATA_NORAID', + '--domain=example.com', '--datacenter=TEST00', + '--network=TEST_NETWORK', '--os=UBUNTU_12_64']) + + self.assertEqual(result.exit_code, 2) diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index 2e3d7b3a5..a61aeece0 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -7,7 +7,6 @@ import copy import mock -from pprint import pprint as pp import SoftLayer from SoftLayer import fixtures @@ -118,7 +117,7 @@ def test_reload(self): def test_get_create_options(self): options = self.hardware.get_create_options() - extras = {'key': '1_IPV6_ADDRESS', 'name': '1 IPv6 Address'} + extras = {'key': '1_IPV6_ADDRESS', 'name': '1 IPv6 Address'} locations = {'key': 'wdc01', 'name': 'Washington 1'} operating_systems = { 'key': 'OS_UBUNTU_14_04_LTS_TRUSTY_TAHR_64_BIT', @@ -141,7 +140,6 @@ def test_get_create_options(self): self.assertEqual(options['port_speeds'][0]['name'], port_speeds['name']) self.assertEqual(options['sizes'][0], sizes) - def test_get_create_options_package_missing(self): packages = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') packages.return_value = [] @@ -174,7 +172,7 @@ def test_generate_create_dict(self): 'post_uri': 'http://example.com/script.php', 'ssh_keys': [10], } - + package = 'BARE_METAL_SERVER' location = 'wdc01' item_keynames = [ @@ -194,7 +192,7 @@ def test_generate_create_dict(self): 'hostname': 'unicorn', }], 'provisionScripts': ['http://example.com/script.php'], - 'sshKeys' : [{'sshKeyIds': [10]}] + 'sshKeys': [{'sshKeyIds': [10]}] } data = self.hardware._generate_create_dict(**args) @@ -204,7 +202,25 @@ def test_generate_create_dict(self): for keyname in item_keynames: self.assertIn(keyname, data['item_keynames']) self.assertEqual(extras, data['extras']) + self.assertEqual(preset_keyname, data['preset_keyname']) + self.assertEqual(hourly, data['hourly']) + def test_generate_create_dict_network_key(self): + args = { + 'size': 'S1270_8GB_2X1TBSATA_NORAID', + 'hostname': 'test1', + 'domain': 'test.com', + 'location': 'wdc01', + 'os': 'OS_UBUNTU_14_04_LTS_TRUSTY_TAHR_64_BIT', + 'network': 'NETWORKING', + 'hourly': True, + 'extras': ['1_IPV6_ADDRESS'], + 'post_uri': 'http://example.com/script.php', + 'ssh_keys': [10], + } + + data = self.hardware._generate_create_dict(**args) + self.assertIn('NETWORKING', data['item_keynames']) @mock.patch('SoftLayer.managers.ordering.OrderingManager.verify_order') @mock.patch('SoftLayer.managers.hardware.HardwareManager._generate_create_dict') @@ -613,17 +629,99 @@ def test_get_hard_drive_empty(self): class HardwareHelperTests(testing.TestCase): + def set_up(self): + self.items = [ + { + "itemCategory": {"categoryCode": "port_speed"}, + "capacity": 100, + "attributes": [ + {'attributeTypeKeyName': 'NON_LACP'}, + {'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'} + ], + "keyName": "ITEM_1", + "prices": [{"id": 1, "locationGroupId": 100}] + }, + { + "itemCategory": {"categoryCode": "port_speed"}, + "capacity": 200, + "attributes": [ + {'attributeTypeKeyName': 'YES_LACP'}, + {'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'} + ], + "keyName": "ITEM_2", + "prices": [{"id": 1, "locationGroupId": 151}] + }, + { + "itemCategory": {"categoryCode": "port_speed"}, + "capacity": 200, + "attributes": [ + {'attributeTypeKeyName': 'YES_LACP'}, + {'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'} + ], + "keyName": "ITEM_3", + "prices": [{"id": 1, "locationGroupId": 51}] + }, + { + "itemCategory": {"categoryCode": "bandwidth"}, + "capacity": 0.0, + "attributes": [], + "keyName": "HOURLY_BANDWIDTH_1", + "prices": [{"id": 1, "locationGroupId": 51, "hourlyRecurringFee": 1.0, "recurringFee": 1.0}] + }, + { + "itemCategory": {"categoryCode": "bandwidth"}, + "capacity": 10.0, + "attributes": [], + "keyName": "MONTHLY_BANDWIDTH_1", + "prices": [{"id": 1, "locationGroupId": 151, "recurringFee": 1.0}] + }, + { + "itemCategory": {"categoryCode": "bandwidth"}, + "capacity": 10.0, + "attributes": [], + "keyName": "MONTHLY_BANDWIDTH_2", + "prices": [{"id": 1, "locationGroupId": 51, "recurringFee": 1.0}] + }, + ] + self.location = {'location': {'location': {'priceGroups': [{'id': 50}, {'id': 51}]}}} + + def test_bandwidth_key(self): + result = managers.hardware._get_bandwidth_key(self.items, True, False, self.location) + self.assertEqual('HOURLY_BANDWIDTH_1', result) + result = managers.hardware._get_bandwidth_key(self.items, False, True, self.location) + self.assertEqual('HOURLY_BANDWIDTH_1', result) + result = managers.hardware._get_bandwidth_key(self.items, False, False, self.location) + self.assertEqual('MONTHLY_BANDWIDTH_2', result) + ex = self.assertRaises(SoftLayer.SoftLayerError, + managers.hardware._get_bandwidth_key, [], True, False, self.location) + self.assertEqual("Could not find valid price for bandwidth option", str(ex)) + + def test_port_speed_key(self): + result = managers.hardware._get_port_speed_key(self.items, 200, True, self.location) + self.assertEqual("ITEM_3", result) + + def test_port_speed_key_exception(self): + items = [] + location = {} + ex = self.assertRaises(SoftLayer.SoftLayerError, + managers.hardware._get_port_speed_key, items, 999, False, location) + self.assertEqual("Could not find valid price for port speed: '999'", str(ex)) + def test_matches_location(self): price = {'id': 1, 'locationGroupId': 51, 'recurringFee': 99} - location = { - 'location': { - 'location': { - 'priceGroups': [ - {'id': 50}, - {'id': 51} - ] - } - } - } - result = managers.hardware._matches_location(price, location) - self.assertTrue(result) + + self.assertTrue(managers.hardware._matches_location(price, self.location)) + price['locationGroupId'] = 99999 + self.assertFalse(managers.hardware._matches_location(price, self.location)) + + def test_is_bonded(self): + item_non_lacp = {'attributes': [{'attributeTypeKeyName': 'NON_LACP'}]} + item_lacp = {'attributes': [{'attributeTypeKeyName': 'YES_LACP'}]} + self.assertFalse(managers.hardware._is_bonded(item_non_lacp)) + self.assertTrue(managers.hardware._is_bonded(item_lacp)) + + def test_is_private(self): + item_private = {'attributes': [{'attributeTypeKeyName': 'IS_PRIVATE_NETWORK_ONLY'}]} + item_public = {'attributes': [{'attributeTypeKeyName': 'NOT_PRIVATE_NETWORK_ONLY'}]} + self.assertTrue(managers.hardware._is_private_port_speed_item(item_private)) + self.assertFalse(managers.hardware._is_private_port_speed_item(item_public)) From 6dd7041e24ae4cc15631443ae6dd471680e018c5 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 10 Jul 2020 16:26:30 -0500 Subject: [PATCH 03/46] #828 code cleanup --- SoftLayer/managers/hardware.py | 19 ------------------- docs/cli/hardware.rst | 2 ++ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 136b22c0a..956d33e3a 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -729,25 +729,6 @@ def get_hard_drives(self, instance_id): return self.hardware.getHardDrives(id=instance_id) -# def _get_extra_price_id(items, key_name, hourly, location): -# """Returns a price id attached to item with the given key_name.""" - -# for item in items: -# if utils.lookup(item, 'keyName') != key_name: -# continue - -# for price in item['prices']: -# if not _matches_billing(price, hourly): -# continue - -# if not _matches_location(price, location): -# continue - -# return price['id'] - -# raise SoftLayerError("Could not find valid price for extra option, '%s'" % key_name) - - def _get_bandwidth_key(items, hourly=True, no_public=False, location=None): """Picks a valid Bandwidth Item, returns the KeyName""" diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst index 0cce23042..5ca325cb7 100644 --- a/docs/cli/hardware.rst +++ b/docs/cli/hardware.rst @@ -27,6 +27,8 @@ Interacting with Hardware Provides some basic functionality to order a server. `slcli order` has a more full featured method of ordering servers. This command only supports the FAST_PROVISION type. +As of v5.9.0 please use the `--network` option for specifying port speed, as that allows a bit more granularity for choosing your networking type. + .. click:: SoftLayer.CLI.hardware.credentials:cli :prog: hardware credentials :show-nested: From f4c256593ca8066b6f58040c7a542d96751d3ee6 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 10 Jul 2020 16:38:11 -0500 Subject: [PATCH 04/46] fixed a unit test --- tests/CLI/modules/server_tests.py | 37 +++---------------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index 388fc310d..f11d9d0a6 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -383,8 +383,7 @@ def test_create_server(self, order_mock): @mock.patch('SoftLayer.CLI.template.export_to_template') def test_create_server_with_export(self, export_mock): - if (sys.platform.startswith("win")): - self.skipTest("Test doesn't work in Windows") + result = self.run_command(['--really', 'server', 'create', '--size=S1270_8GB_2X1TBSATA_NORAID', '--hostname=test', @@ -397,24 +396,8 @@ def test_create_server_with_export(self, export_mock): fmt='raw') self.assert_no_fail(result) - self.assertIn("Successfully exported options to a template file.", - result.output) - export_mock.assert_called_with('/path/to/test_file.txt', - {'billing': 'hourly', - 'datacenter': 'TEST00', - 'domain': 'example.com', - 'extra': (), - 'hostname': 'test', - 'key': (), - 'os': 'UBUNTU_12_64', - 'port_speed': 100, - 'postinstall': None, - 'size': 'S1270_8GB_2X1TBSATA_NORAID', - 'test': False, - 'no_public': True, - 'wait': None, - 'template': None}, - exclude=['wait', 'test']) + self.assertIn("Successfully exported options to a template file.", result.output) + export_mock.assert_called_once() def test_edit_server_userdata_and_file(self): # Test both userdata and userfile at once @@ -860,20 +843,6 @@ def test_billing(self): self.assert_no_fail(result) self.assertEqual(json.loads(result.output), billing_json) - def test_create_hw_export(self): - if(sys.platform.startswith("win")): - self.skipTest("Temp files do not work properly in Windows.") - with tempfile.NamedTemporaryFile() as config_file: - result = self.run_command(['hw', 'create', '--hostname=test', '--export', config_file.name, - '--domain=example.com', '--datacenter=TEST00', - '--network=TEST_NETWORK', '--os=UBUNTU_12_64', - '--size=S1270_8GB_2X1TBSATA_NORAID']) - self.assert_no_fail(result) - self.assertTrue('Successfully exported options to a template file.' in result.output) - contents = config_file.read().decode("utf-8") - self.assertIn('hostname=TEST', contents) - self.assertIn('size=S1270_8GB_2X1TBSATA_NORAID', contents) - @mock.patch('SoftLayer.CLI.formatting.confirm') def test_create_hw_no_confirm(self, confirm_mock): confirm_mock.return_value = False From d3871cf8aca3f6e92aebed4955344c3948069fdb Mon Sep 17 00:00:00 2001 From: ATGE Date: Fri, 10 Jul 2020 22:12:50 -0400 Subject: [PATCH 05/46] add user notifications --- SoftLayer/CLI/routes.py | 2 + SoftLayer/CLI/user/edit_notifications.py | 34 ++++++++++++ SoftLayer/CLI/user/notifications.py | 34 ++++++++++++ SoftLayer/managers/user.py | 68 ++++++++++++++++++++++++ 4 files changed, 138 insertions(+) create mode 100644 SoftLayer/CLI/user/edit_notifications.py create mode 100644 SoftLayer/CLI/user/notifications.py diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 48b0af834..049b43c3b 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -316,6 +316,8 @@ ('user:detail', 'SoftLayer.CLI.user.detail:cli'), ('user:permissions', 'SoftLayer.CLI.user.permissions:cli'), ('user:edit-permissions', 'SoftLayer.CLI.user.edit_permissions:cli'), + ('user:notifications', 'SoftLayer.CLI.user.notifications:cli'), + ('user:edit-notifications', 'SoftLayer.CLI.user.edit_notifications:cli'), ('user:edit-details', 'SoftLayer.CLI.user.edit_details:cli'), ('user:create', 'SoftLayer.CLI.user.create:cli'), ('user:delete', 'SoftLayer.CLI.user.delete:cli'), diff --git a/SoftLayer/CLI/user/edit_notifications.py b/SoftLayer/CLI/user/edit_notifications.py new file mode 100644 index 000000000..b01272b36 --- /dev/null +++ b/SoftLayer/CLI/user/edit_notifications.py @@ -0,0 +1,34 @@ +"""Enable or Disable specific noticication for the current user""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment + + +@click.command() +@click.option('--enable/--disable', default=True, + help="Enable (DEFAULT) or Disable selected notification") +@click.argument('notification', nargs=-1, required=True) +@environment.pass_env +def cli(env, enable, notification): + """Enable or Disable specific notifications. + + Example:: + + slcli user edit-notifications --enable 'Order Approved' 'Reload Complete' + + """ + + mgr = SoftLayer.UserManager(env.client) + + if enable: + result = mgr.enable_notifications(notification) + else: + result = mgr.disable_notifications(notification) + + if result: + click.secho("Notifications updated successfully: %s" % ", ".join(notification), fg='green') + else: + click.secho("Failed to update notifications: %s" % ", ".join(notification), fg='red') diff --git a/SoftLayer/CLI/user/notifications.py b/SoftLayer/CLI/user/notifications.py new file mode 100644 index 000000000..ddb8019e9 --- /dev/null +++ b/SoftLayer/CLI/user/notifications.py @@ -0,0 +1,34 @@ +"""List user notifications""" +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + + +@click.command() +@environment.pass_env +def cli(env): + """User Notifications.""" + + mgr = SoftLayer.UserManager(env.client) + + all_notifications = mgr.get_all_notifications() + + env.fout(notification_table(all_notifications)) + + +def notification_table(all_notifications): + """Creates a table of available notifications""" + + table = formatting.Table(['Id', 'Name', 'Description', 'Enabled']) + table.align['Id'] = 'l' + table.align['Name'] = 'l' + table.align['Description'] = 'l' + table.align['Enabled'] = 'l' + for notification in all_notifications: + table.add_row([notification['id'], + notification['name'], + notification['description'], + notification['enabled']]) + return table diff --git a/SoftLayer/managers/user.py b/SoftLayer/managers/user.py index 5875d76a8..283208baf 100644 --- a/SoftLayer/managers/user.py +++ b/SoftLayer/managers/user.py @@ -36,6 +36,7 @@ def __init__(self, client): self.user_service = self.client['SoftLayer_User_Customer'] self.override_service = self.client['Network_Service_Vpn_Overrides'] self.account_service = self.client['SoftLayer_Account'] + self.subscription_service = self.client['SoftLayer_Email_Subscription'] self.resolvers = [self._get_id_from_username] self.all_permissions = None @@ -85,6 +86,56 @@ def get_all_permissions(self): self.all_permissions = sorted(permissions, key=itemgetter('keyName')) return self.all_permissions + def get_all_notifications(self): + """Calls SoftLayer_Email_Subscription::getAllObjects + + Stores the result in self.all_permissions + :returns: A list of dictionaries that contains all valid permissions + """ + return self.subscription_service.getAllObjects(mask='mask[enabled]') + + def enable_notifications(self, notifications_names): + """Enables a list of notifications for the current a user profile. + + :param list notifications_names: List of notifications names to enable + :returns: True on success + + Example:: + enable_notifications(['Order Approved','Reload Complete']) + """ + + result = False + notifications = self.gather_notifications(notifications_names) + for notification in notifications: + notification_id = notification.get('id') + result = self.subscription_service.enable(id=notification_id) + if result: + continue + else: + return False + return result + + def disable_notifications(self, notifications_names): + """Disable a list of notifications for the current a user profile. + + :param list notifications_names: List of notifications names to disable + :returns: True on success + + Example:: + disable_notifications(['Order Approved','Reload Complete']) + """ + + result = False + notifications = self.gather_notifications(notifications_names) + for notification in notifications: + notification_id = notification.get('id') + result = self.subscription_service.disable(id=notification_id) + if result: + continue + else: + return False + return result + def add_permissions(self, user_id, permissions): """Enables a list of permissions for a user @@ -237,6 +288,23 @@ def format_permission_object(self, permissions): raise exceptions.SoftLayerError("'%s' is not a valid permission" % permission) return pretty_permissions + def gather_notifications(self, notifications_names): + """Gets a list of notifications. + + :param list notifications_names: A list of notifications names. + :returns: list of notifications. + """ + notifications = [] + available_notifications = self.get_all_notifications() + for notification in notifications_names: + result = next((item for item in available_notifications + if item.get('name') == notification), None) + if result: + notifications.append(result) + else: + raise exceptions.SoftLayerError("{} is not a valid notification name".format(notification)) + return notifications + def create_user(self, user_object, password): """Blindly sends user_object to SoftLayer_User_Customer::createObject From 2e4618acc47a33682ee72a430588f771e3357e35 Mon Sep 17 00:00:00 2001 From: ATGE Date: Fri, 10 Jul 2020 22:13:58 -0400 Subject: [PATCH 06/46] add user notifications tests --- .../fixtures/SoftLayer_Email_Subscription.py | 22 +++++++++ tests/CLI/modules/user_tests.py | 32 +++++++++++++ tests/managers/user_tests.py | 48 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 SoftLayer/fixtures/SoftLayer_Email_Subscription.py diff --git a/SoftLayer/fixtures/SoftLayer_Email_Subscription.py b/SoftLayer/fixtures/SoftLayer_Email_Subscription.py new file mode 100644 index 000000000..bc3104b16 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Email_Subscription.py @@ -0,0 +1,22 @@ +getAllObjects = [ + {'description': 'Email about your order.', + 'enabled': True, + 'id': 1, + 'name': 'Order Being Reviewed' + }, + {'description': 'Maintenances that will or are likely to cause service ' + 'outages and disruptions', + 'enabled': True, + 'id': 8, + 'name': 'High Impact' + }, + {'description': 'Testing description.', + 'enabled': True, + 'id': 111, + 'name': 'Test notification' + } +] + +enable = True + +disable = True diff --git a/tests/CLI/modules/user_tests.py b/tests/CLI/modules/user_tests.py index 6f58c14a0..f16ef1843 100644 --- a/tests/CLI/modules/user_tests.py +++ b/tests/CLI/modules/user_tests.py @@ -305,3 +305,35 @@ def test_vpn_subnet_remove(self, click): result = self.run_command(['user', 'vpn-subnet', '12345', '--remove', '1234']) click.secho.assert_called_with('12345 updated successfully', fg='green') self.assert_no_fail(result) + + """User notification tests""" + + def test_notificacions_list(self): + result = self.run_command(['user', 'notifications']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Email_Subscription', 'getAllObjects', mask='mask[enabled]') + + """User edit-notification tests""" + + def test_edit_notification_on(self): + result = self.run_command(['user', 'edit-notifications', '--enable', 'Test notification']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Email_Subscription', 'enable', identifier=111) + + def test_edit_notification_on_bad(self): + result = self.run_command(['user', 'edit-notifications', '--enable', 'Test not exist']) + self.assertEqual(result.exit_code, 1) + + def test_edit_notifications_off(self): + result = self.run_command(['user', 'edit-notifications', '--disable', 'Test notification']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Email_Subscription', 'disable', identifier=111) + + @mock.patch('SoftLayer.CLI.user.edit_notifications.click') + def test_edit_notification_off_failure(self, click): + notification = self.set_mock('SoftLayer_Email_Subscription', 'disable') + notification.return_value = False + result = self.run_command(['user', 'edit-notifications', '--disable', 'Test notification']) + click.secho.assert_called_with('Failed to update notifications: Test notification', fg='red') + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Email_Subscription', 'disable', identifier=111) diff --git a/tests/managers/user_tests.py b/tests/managers/user_tests.py index b0ab015f9..61f5b4d0a 100644 --- a/tests/managers/user_tests.py +++ b/tests/managers/user_tests.py @@ -4,7 +4,9 @@ """ import datetime + import mock + import SoftLayer from SoftLayer import exceptions from SoftLayer import testing @@ -246,3 +248,49 @@ def test_vpn_subnet_remove(self): self.manager.vpn_subnet_remove(user_id, [subnet_id]) self.assert_called_with('SoftLayer_Network_Service_Vpn_Overrides', 'deleteObjects', args=expected_args) self.assert_called_with('SoftLayer_User_Customer', 'updateVpnUser', identifier=user_id) + + def test_get_all_notifications(self): + self.manager.get_all_notifications() + self.assert_called_with('SoftLayer_Email_Subscription', 'getAllObjects') + + def test_enable_notifications(self): + self.manager.enable_notifications(['Test notification']) + self.assert_called_with('SoftLayer_Email_Subscription', 'enable', identifier=111) + + def test_disable_notifications(self): + self.manager.disable_notifications(['Test notification']) + self.assert_called_with('SoftLayer_Email_Subscription', 'disable', identifier=111) + + def test_enable_notifications_fail(self): + notification = self.set_mock('SoftLayer_Email_Subscription', 'enable') + notification.return_value = False + result = self.manager.enable_notifications(['Test notification']) + self.assert_called_with('SoftLayer_Email_Subscription', 'enable', identifier=111) + self.assertFalse(result) + + def test_disable_notifications_fail(self): + notification = self.set_mock('SoftLayer_Email_Subscription', 'disable') + notification.return_value = False + result = self.manager.disable_notifications(['Test notification']) + self.assert_called_with('SoftLayer_Email_Subscription', 'disable', identifier=111) + self.assertFalse(result) + + def test_gather_notifications(self): + expected_result = [ + {'description': 'Testing description.', + 'enabled': True, + 'id': 111, + 'name': 'Test notification' + } + ] + result = self.manager.gather_notifications(['Test notification']) + self.assert_called_with('SoftLayer_Email_Subscription', + 'getAllObjects', + mask='mask[enabled]') + self.assertEqual(result, expected_result) + + def test_gather_notifications_fail(self): + ex = self.assertRaises(SoftLayer.SoftLayerError, + self.manager.gather_notifications, + ['Test not exit']) + self.assertEqual("Test not exit is not a valid notification name", str(ex)) From eb6631697f6de79323c3440eb633169dd542a656 Mon Sep 17 00:00:00 2001 From: ATGE Date: Fri, 10 Jul 2020 22:14:42 -0400 Subject: [PATCH 07/46] add user notifications docs --- docs/cli/users.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/cli/users.rst b/docs/cli/users.rst index feb94e352..5195a7788 100644 --- a/docs/cli/users.rst +++ b/docs/cli/users.rst @@ -12,10 +12,18 @@ Version 5.6.0 introduces the ability to interact with user accounts from the cli :prog: user detail :show-nested: +.. click:: SoftLayer.CLI.user.notifications:cli + :prog: user notifications + :show-nested: + .. click:: SoftLayer.CLI.user.permissions:cli :prog: user permissions :show-nested: +.. click:: SoftLayer.CLI.user.edit_notifications:cli + :prog: user edit-notifications + :show-nested: + .. click:: SoftLayer.CLI.user.edit_permissions:cli :prog: user edit-permissions :show-nested: From e423d6b83f99c0d2bd34b7408211eb72c3485b4d Mon Sep 17 00:00:00 2001 From: ATGE Date: Mon, 13 Jul 2020 15:24:22 -0400 Subject: [PATCH 08/46] fix tox analysis --- SoftLayer/managers/user.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/SoftLayer/managers/user.py b/SoftLayer/managers/user.py index 283208baf..0948df8b3 100644 --- a/SoftLayer/managers/user.py +++ b/SoftLayer/managers/user.py @@ -109,9 +109,7 @@ def enable_notifications(self, notifications_names): for notification in notifications: notification_id = notification.get('id') result = self.subscription_service.enable(id=notification_id) - if result: - continue - else: + if not result: return False return result @@ -130,9 +128,7 @@ def disable_notifications(self, notifications_names): for notification in notifications: notification_id = notification.get('id') result = self.subscription_service.disable(id=notification_id) - if result: - continue - else: + if not result: return False return result From 744213a800ca827fbd1a790110ebb6063a914bfa Mon Sep 17 00:00:00 2001 From: ATGE Date: Tue, 14 Jul 2020 15:55:24 -0400 Subject: [PATCH 09/46] improve user notifications docs --- SoftLayer/CLI/user/edit_notifications.py | 3 ++- SoftLayer/CLI/user/notifications.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/user/edit_notifications.py b/SoftLayer/CLI/user/edit_notifications.py index b01272b36..acef3380b 100644 --- a/SoftLayer/CLI/user/edit_notifications.py +++ b/SoftLayer/CLI/user/edit_notifications.py @@ -13,7 +13,8 @@ @click.argument('notification', nargs=-1, required=True) @environment.pass_env def cli(env, enable, notification): - """Enable or Disable specific notifications. + """Enable or Disable specific notifications for the active user. + Notification names should be enclosed in quotation marks. Example:: diff --git a/SoftLayer/CLI/user/notifications.py b/SoftLayer/CLI/user/notifications.py index ddb8019e9..deffd951e 100644 --- a/SoftLayer/CLI/user/notifications.py +++ b/SoftLayer/CLI/user/notifications.py @@ -9,7 +9,7 @@ @click.command() @environment.pass_env def cli(env): - """User Notifications.""" + """My Notifications.""" mgr = SoftLayer.UserManager(env.client) From 9c940744471f4a3bc2482a1c1fdfa086b55ee9c4 Mon Sep 17 00:00:00 2001 From: ATGE Date: Tue, 14 Jul 2020 16:06:48 -0400 Subject: [PATCH 10/46] clean code --- SoftLayer/CLI/user/edit_notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/user/edit_notifications.py b/SoftLayer/CLI/user/edit_notifications.py index acef3380b..1be7527c2 100644 --- a/SoftLayer/CLI/user/edit_notifications.py +++ b/SoftLayer/CLI/user/edit_notifications.py @@ -14,8 +14,8 @@ @environment.pass_env def cli(env, enable, notification): """Enable or Disable specific notifications for the active user. - Notification names should be enclosed in quotation marks. + Notification names should be enclosed in quotation marks. Example:: slcli user edit-notifications --enable 'Order Approved' 'Reload Complete' From 47fb7896c3c004ea1abb7aa669c897e92b5d0163 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 16 Jul 2020 15:10:35 -0500 Subject: [PATCH 11/46] #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 12/46] 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 cda861a2fc1ef5420136c9d1606953c6b462bf49 Mon Sep 17 00:00:00 2001 From: ATGE Date: Thu, 16 Jul 2020 18:31:54 -0400 Subject: [PATCH 13/46] #1298 refactor get local type disks --- SoftLayer/CLI/virt/detail.py | 13 +------------ SoftLayer/CLI/virt/storage.py | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index d17e489f2..c07ef657c 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -9,6 +9,7 @@ from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers +from SoftLayer.CLI.virt.storage import get_local_type from SoftLayer import utils LOGGER = logging.getLogger(__name__) @@ -200,15 +201,3 @@ def _get_security_table(result): return secgroup_table else: return None - - -def get_local_type(disks): - """Returns the virtual server local disk type. - - :param disks: virtual serve local disks. - """ - disk_type = 'System' - if 'SWAP' in disks['diskImage']['description']: - disk_type = 'Swap' - - return disk_type diff --git a/SoftLayer/CLI/virt/storage.py b/SoftLayer/CLI/virt/storage.py index 8d1b65854..802ae32d9 100644 --- a/SoftLayer/CLI/virt/storage.py +++ b/SoftLayer/CLI/virt/storage.py @@ -67,7 +67,7 @@ def get_local_type(disks): :param disks: virtual serve local disks. """ disk_type = 'System' - if 'SWAP' in disks['diskImage']['description']: + if 'SWAP' in disks.get('diskImage', {}).get('description', []): disk_type = 'Swap' return disk_type From 3f8361834f46f17e56d6662922d3e5d466a0d175 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 17 Jul 2020 16:42:01 -0500 Subject: [PATCH 14/46] 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 15/46] 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 beace276a551a79cdf3ea1b9130b0dfbd40b116f Mon Sep 17 00:00:00 2001 From: Fernando Ojeda Date: Wed, 22 Jul 2020 12:42:14 -0400 Subject: [PATCH 16/46] List hardware vs associated. Add vs list hw vs associated. --- SoftLayer/CLI/hardware/guests.py | 38 ++++++++++++ SoftLayer/CLI/routes.py | 1 + SoftLayer/CLI/virt/list.py | 22 ++++++- .../fixtures/SoftLayer_Hardware_Server.py | 20 +++++++ SoftLayer/fixtures/SoftLayer_Virtual_Host.py | 40 +++++++++++++ SoftLayer/managers/hardware.py | 14 ++++- SoftLayer/managers/vs.py | 7 +++ tests/CLI/modules/server_tests.py | 12 ++++ tests/managers/hardware_tests.py | 48 +++++++++++++++ tests/managers/vs/vs_tests.py | 58 +++++++++++++++++++ 10 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 SoftLayer/CLI/hardware/guests.py create mode 100644 SoftLayer/fixtures/SoftLayer_Virtual_Host.py diff --git a/SoftLayer/CLI/hardware/guests.py b/SoftLayer/CLI/hardware/guests.py new file mode 100644 index 000000000..0bb6a0501 --- /dev/null +++ b/SoftLayer/CLI/hardware/guests.py @@ -0,0 +1,38 @@ +"""List the Hardware server associated virtual guests.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer import utils +from SoftLayer.CLI import environment, formatting, exceptions +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@environment.pass_env +def cli(env, identifier): + """List the Hardware server associated virtual guests.""" + + mgr = SoftLayer.HardwareManager(env.client) + hw_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'hardware') + hw_guests = mgr.get_hardware_guests(hw_id) + + if not hw_guests: + raise exceptions.CLIAbort("The hardware server does not has associated virtual guests.") + + table = formatting.Table(['id', 'hostname', 'CPU', 'Memory', 'Start Date', 'Status', 'powerState']) + table.sortby = 'hostname' + for guest in hw_guests: + table.add_row([ + guest['id'], + guest['hostname'], + '%i %s' % (guest['maxCpu'], guest['maxCpuUnits']), + guest['maxMemory'], + utils.clean_time(guest['createDate']), + guest['status']['keyName'], + guest['powerState']['keyName'] + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 049b43c3b..a84de0157 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -236,6 +236,7 @@ ('hardware:detail', 'SoftLayer.CLI.hardware.detail:cli'), ('hardware:billing', 'SoftLayer.CLI.hardware.billing:cli'), ('hardware:edit', 'SoftLayer.CLI.hardware.edit:cli'), + ('hardware:guests', 'SoftLayer.CLI.hardware.guests:cli'), ('hardware:list', 'SoftLayer.CLI.hardware.list:cli'), ('hardware:power-cycle', 'SoftLayer.CLI.hardware.power:power_cycle'), ('hardware:power-off', 'SoftLayer.CLI.hardware.power:power_off'), diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 6bf9e6bb6..3a24ffe9d 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -4,12 +4,12 @@ import click import SoftLayer +from SoftLayer import utils from SoftLayer.CLI import columns as column_helper from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers - # pylint: disable=unnecessary-lambda COLUMNS = [ @@ -93,3 +93,23 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, for value in columns.row(guest)]) env.fout(table) + + hardware_guests = vsi.get_hardware_guests() + for hardware in hardware_guests: + if 'virtualHost' in hardware and hardware['virtualHost']['guests']: + table_hardware_guest = formatting.Table(['id', 'hostname', 'CPU', 'Memory', 'Start Date', 'Status', + 'powerState'], title="Hardware(id = {hardwareId}) guests " + "associated".format(hardwareId=hardware['id']) + ) + table_hardware_guest.sortby = 'hostname' + for guest in hardware['virtualHost']['guests']: + table_hardware_guest.add_row([ + guest['id'], + guest['hostname'], + '%i %s' % (guest['maxCpu'], guest['maxCpuUnits']), + guest['maxMemory'], + utils.clean_time(guest['createDate']), + guest['status']['keyName'], + guest['powerState']['keyName'] + ]) + env.fout(table_hardware_guest) diff --git a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py index e90288753..5c26d20da 100644 --- a/SoftLayer/fixtures/SoftLayer_Hardware_Server.py +++ b/SoftLayer/fixtures/SoftLayer_Hardware_Server.py @@ -242,3 +242,23 @@ } } ] + +getVirtualHost = { + "accountId": 11111, + "createDate": "2018-10-08T10:54:48-06:00", + "description": "host16.vmware.chechu.com", + "hardwareId": 22222, + "id": 33333, + "name": "host16.vmware.chechu.com", + "uuid": "00000000-0000-0000-0000-0cc11111", + "hardware": { + "accountId": 11111, + "domain": "chechu.com", + "hostname": "host16.vmware", + "id": 22222, + "hardwareStatus": { + "id": 5, + "status": "ACTIVE" + } + } +} diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Host.py b/SoftLayer/fixtures/SoftLayer_Virtual_Host.py new file mode 100644 index 000000000..c5b20a34b --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Virtual_Host.py @@ -0,0 +1,40 @@ +getGuests = [ + { + "accountId": 11111, + "createDate": "2019-09-05T17:03:42-06:00", + "fullyQualifiedDomainName": "NSX-T Manager", + "hostname": "NSX-T Manager", + "id": 22222, + "maxCpu": 16, + "maxCpuUnits": "CORE", + "maxMemory": 49152, + "startCpus": 16, + "powerState": { + "keyName": "RUNNING", + "name": "Running" + }, + "status": { + "keyName": "ACTIVE", + "name": "Active" + } + }, + { + "accountId": 11111, + "createDate": "2019-09-23T06:00:53-06:00", + "hostname": "NSX-T Manager2", + "id": 33333, + "maxCpu": 12, + "maxCpuUnits": "CORE", + "maxMemory": 49152, + "startCpus": 12, + "statusId": 1001, + "powerState": { + "keyName": "RUNNING", + "name": "Running" + }, + "status": { + "keyName": "ACTIVE", + "name": "Active" + } + } +] diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 956d33e3a..2214ee8c2 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -9,11 +9,11 @@ import socket import time +from SoftLayer import utils from SoftLayer.decoration import retry from SoftLayer.exceptions import SoftLayerError from SoftLayer.managers import ordering from SoftLayer.managers.ticket import TicketManager -from SoftLayer import utils LOGGER = logging.getLogger(__name__) @@ -728,6 +728,18 @@ def get_hard_drives(self, instance_id): """ return self.hardware.getHardDrives(id=instance_id) + def get_hardware_guests(self, instance_id): + """Returns the hardware server guests. + + :param int instance_id: Id of the hardware server. + """ + mask = "mask[id]" + virtual_host = self.hardware.getVirtualHost(mask=mask, id=instance_id) + if virtual_host: + return self.client.call('SoftLayer_Virtual_Host', 'getGuests', mask='mask[powerState]', + id=virtual_host['id']) + return virtual_host + def _get_bandwidth_key(items, hourly=True, no_public=False, location=None): """Picks a valid Bandwidth Item, returns the KeyName""" diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index e63d7a80f..9426fb53f 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -1158,3 +1158,10 @@ def get_local_disks(self, instance_id): """ mask = 'mask[diskImage]' return self.guest.getBlockDevices(mask=mask, id=instance_id) + + def get_hardware_guests(self): + """Returns the hardware virtual server associated. + """ + object_filter = {"hardware": {"networkGatewayMemberFlag": {"operation": 0}}} + mask = "mask[networkGatewayMemberFlag,virtualHost[guests[powerState]]]" + return self.client.call('SoftLayer_Account', 'getHardware', mask=mask, filter=object_filter) diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index f11d9d0a6..4ef1a7354 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -852,3 +852,15 @@ def test_create_hw_no_confirm(self, confirm_mock): '--network=TEST_NETWORK', '--os=UBUNTU_12_64']) self.assertEqual(result.exit_code, 2) + + def test_get_hardware_guests(self): + result = self.run_command(['hw', 'guests', '123456']) + self.assert_no_fail(result) + + def test_hardware_guests_empty(self): + mock = self.set_mock('SoftLayer_Virtual_Host', 'getGuests') + mock.return_value = None + + result = self.run_command(['hw', 'guests', '123456']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.CLIAbort) diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index a61aeece0..a7824dd61 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -626,6 +626,54 @@ def test_get_hard_drive_empty(self): self.assertEqual([], result) + def test_get_hardware_guests_empty_virtualHost(self): + mock = self.set_mock('SoftLayer_Hardware_Server', 'getVirtualHost') + mock.return_value = None + + result = self.hardware.get_hardware_guests(1234) + + self.assertEqual(None, result) + + def test_get_hardware_guests(self): + mock = self.set_mock('SoftLayer_Virtual_Host', 'getGuests') + mock.return_value = [ + { + "accountId": 11111, + "hostname": "NSX-T Manager", + "id": 22222, + "maxCpu": 16, + "maxCpuUnits": "CORE", + "maxMemory": 49152, + "powerState": { + "keyName": "RUNNING", + "name": "Running" + }, + "status": { + "keyName": "ACTIVE", + "name": "Active" + } + }] + + result = self.hardware.get_hardware_guests(1234) + + self.assertEqual([ + { + "accountId": 11111, + "hostname": "NSX-T Manager", + "id": 22222, + "maxCpu": 16, + "maxCpuUnits": "CORE", + "maxMemory": 49152, + "powerState": { + "keyName": "RUNNING", + "name": "Running" + }, + "status": { + "keyName": "ACTIVE", + "name": "Active" + } + }], result) + class HardwareHelperTests(testing.TestCase): diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py index 40fb3063f..fcee57be5 100644 --- a/tests/managers/vs/vs_tests.py +++ b/tests/managers/vs/vs_tests.py @@ -1132,3 +1132,61 @@ def test_get_local_disks_swap(self): } } ], result) + + def test_get_hardware_guests(self): + mock = self.set_mock('SoftLayer_Account', 'getHardware') + mock.return_value = [{ + "accountId": 11111, + "domain": "vmware.chechu.com", + "hostname": "host14", + "id": 22222, + "virtualHost": { + "accountId": 11111, + "id": 33333, + "name": "host14.vmware.chechu.com", + "guests": [ + { + "accountId": 11111, + "hostname": "NSX-T Manager", + "id": 44444, + "maxCpu": 16, + "maxCpuUnits": "CORE", + "maxMemory": 49152, + "powerState": { + "keyName": "RUNNING", + "name": "Running" + }, + "status": { + "keyName": "ACTIVE", + "name": "Active" + } + }]}}] + + result = self.vs.get_hardware_guests() + + self.assertEqual([{ + "accountId": 11111, + "domain": "vmware.chechu.com", + "hostname": "host14", + "id": 22222, + "virtualHost": { + "accountId": 11111, + "id": 33333, + "name": "host14.vmware.chechu.com", + "guests": [ + { + "accountId": 11111, + "hostname": "NSX-T Manager", + "id": 44444, + "maxCpu": 16, + "maxCpuUnits": "CORE", + "maxMemory": 49152, + "powerState": { + "keyName": "RUNNING", + "name": "Running" + }, + "status": { + "keyName": "ACTIVE", + "name": "Active" + } + }]}}], result) From a3d6ef5a29c33ba3e2d537898d7b29363266201a Mon Sep 17 00:00:00 2001 From: Fernando Ojeda Date: Wed, 22 Jul 2020 12:50:26 -0400 Subject: [PATCH 17/46] Add hardware guests documentation. --- docs/cli/hardware.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst index 5ca325cb7..f7691e700 100644 --- a/docs/cli/hardware.rst +++ b/docs/cli/hardware.rst @@ -115,3 +115,7 @@ This function updates the firmware of a server. If already at the latest version .. click:: SoftLayer.CLI.hardware.storage:cli :prog: hardware storage :show-nested: + +.. click:: SoftLayer.CLI.hardware.guests:cli + :prog: hardware guests + :show-nested: From 69c90adbf8db457a3888769662f14af741659748 Mon Sep 17 00:00:00 2001 From: Fernando Ojeda Date: Wed, 22 Jul 2020 13:12:36 -0400 Subject: [PATCH 18/46] Fi tox analysis. --- SoftLayer/CLI/hardware/guests.py | 6 ++++-- SoftLayer/CLI/virt/list.py | 2 +- SoftLayer/managers/hardware.py | 2 +- SoftLayer/managers/vs.py | 4 +++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/hardware/guests.py b/SoftLayer/CLI/hardware/guests.py index 0bb6a0501..418cf9d6e 100644 --- a/SoftLayer/CLI/hardware/guests.py +++ b/SoftLayer/CLI/hardware/guests.py @@ -4,9 +4,11 @@ import click import SoftLayer -from SoftLayer import utils -from SoftLayer.CLI import environment, formatting, exceptions +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers +from SoftLayer import utils @click.command() diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 3a24ffe9d..925f33d2a 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -4,11 +4,11 @@ import click import SoftLayer -from SoftLayer import utils from SoftLayer.CLI import columns as column_helper from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers +from SoftLayer import utils # pylint: disable=unnecessary-lambda diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 2214ee8c2..1c2a097f1 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -9,11 +9,11 @@ import socket import time -from SoftLayer import utils from SoftLayer.decoration import retry from SoftLayer.exceptions import SoftLayerError from SoftLayer.managers import ordering from SoftLayer.managers.ticket import TicketManager +from SoftLayer import utils LOGGER = logging.getLogger(__name__) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 9426fb53f..6c4d67786 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -1160,7 +1160,9 @@ def get_local_disks(self, instance_id): return self.guest.getBlockDevices(mask=mask, id=instance_id) def get_hardware_guests(self): - """Returns the hardware virtual server associated. + """Returns the hardware server vs associated. + + :return SoftLayer_Hardware[]. """ object_filter = {"hardware": {"networkGatewayMemberFlag": {"operation": 0}}} mask = "mask[networkGatewayMemberFlag,virtualHost[guests[powerState]]]" From fb59874ea7eee7fb30411d56d80066abdbb44f31 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Wed, 22 Jul 2020 15:30:07 -0500 Subject: [PATCH 19/46] #875 added option to reload bare metal servers with LVM enabled --- SoftLayer/CLI/hardware/reload.py | 13 ++++++------- SoftLayer/managers/hardware.py | 12 +++++++----- tests/CLI/modules/server_tests.py | 10 +++++++--- tests/managers/hardware_tests.py | 12 +++++++----- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/SoftLayer/CLI/hardware/reload.py b/SoftLayer/CLI/hardware/reload.py index 11286d0d9..798e01968 100644 --- a/SoftLayer/CLI/hardware/reload.py +++ b/SoftLayer/CLI/hardware/reload.py @@ -13,17 +13,16 @@ @click.command() @click.argument('identifier') @click.option('--postinstall', '-i', - help=("Post-install script to download " - "(Only HTTPS executes, HTTP leaves file in /root")) + help=("Post-install script to download (Only HTTPS executes, HTTP leaves file in /root")) @helpers.multi_option('--key', '-k', help="SSH keys to add to the root user") +@click.option('--lvm', '-l', is_flag=True, default=False, show_default=True, + help="A flag indicating that the provision should use LVM for all logical drives.") @environment.pass_env -def cli(env, identifier, postinstall, key): +def cli(env, identifier, postinstall, key, lvm): """Reload operating system on a server.""" hardware = SoftLayer.HardwareManager(env.client) - hardware_id = helpers.resolve_id(hardware.resolve_ids, - identifier, - 'hardware') + hardware_id = helpers.resolve_id(hardware.resolve_ids, identifier, 'hardware') key_list = [] if key: for single_key in key: @@ -33,4 +32,4 @@ def cli(env, identifier, postinstall, key): if not (env.skip_confirmations or formatting.no_going_back(hardware_id)): raise exceptions.CLIAbort('Aborted') - hardware.reload(hardware_id, postinstall, key_list) + hardware.reload(hardware_id, postinstall, key_list, lvm) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 956d33e3a..2db54ed25 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -269,13 +269,14 @@ def get_hardware(self, hardware_id, **kwargs): return self.hardware.getObject(id=hardware_id, **kwargs) - def reload(self, hardware_id, post_uri=None, ssh_keys=None): + def reload(self, hardware_id, post_uri=None, ssh_keys=None, lvm=False): """Perform an OS reload of a server with its current configuration. + https://sldn.softlayer.com/reference/datatypes/SoftLayer_Container_Hardware_Server_Configuration/ :param integer hardware_id: the instance ID to reload - :param string post_uri: The URI of the post-install script to run - after reload + :param string post_uri: The URI of the post-install script to run after reload :param list ssh_keys: The SSH keys to add to the root user + :param bool lvm: A flag indicating that the provision should use LVM for all logical drives. """ config = {} @@ -285,9 +286,10 @@ def reload(self, hardware_id, post_uri=None, ssh_keys=None): if ssh_keys: config['sshKeyIds'] = list(ssh_keys) + if lvm: + config['lvmFlag'] = lvm - return self.hardware.reloadOperatingSystem('FORCE', config, - id=hardware_id) + return self.hardware.reloadOperatingSystem('FORCE', config, id=hardware_id) def rescue(self, hardware_id): """Reboot a server into the a recsue kernel. diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index f11d9d0a6..1f539c84e 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -219,11 +219,15 @@ def test_server_reload(self, reload_mock, ngb_mock): ngb_mock.return_value = False # Check the positive case - result = self.run_command(['--really', 'server', 'reload', '12345', - '--key=4567']) + result = self.run_command(['--really', 'server', 'reload', '12345', '--key=4567']) self.assert_no_fail(result) - reload_mock.assert_called_with(12345, None, [4567]) + reload_mock.assert_called_with(12345, None, [4567], False) + + # LVM switch + result = self.run_command(['--really', 'server', 'reload', '12345', '--lvm']) + self.assert_no_fail(result) + reload_mock.assert_called_with(12345, None, [], True) # Now check to make sure we properly call CLIAbort in the negative case result = self.run_command(['server', 'reload', '12345']) diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index a61aeece0..d7516aec0 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -107,13 +107,15 @@ def test_reload(self): result = self.hardware.reload(1, post_uri=post_uri, ssh_keys=[1701]) self.assertEqual(result, 'OK') - self.assert_called_with('SoftLayer_Hardware_Server', - 'reloadOperatingSystem', - args=('FORCE', - {'customProvisionScriptUri': post_uri, - 'sshKeyIds': [1701]}), + self.assert_called_with('SoftLayer_Hardware_Server', 'reloadOperatingSystem', + args=('FORCE', {'customProvisionScriptUri': post_uri, 'sshKeyIds': [1701]}), identifier=1) + result = self.hardware.reload(100, lvm=True) + self.assertEqual(result, 'OK') + self.assert_called_with('SoftLayer_Hardware_Server', 'reloadOperatingSystem', + args=('FORCE', {'lvmFlag': True}), identifier=100) + def test_get_create_options(self): options = self.hardware.get_create_options() From 3863aaf5270ff8d7848371c7a00a3ddd9a620b8d Mon Sep 17 00:00:00 2001 From: Fernando Ojeda Date: Thu, 23 Jul 2020 10:51:07 -0400 Subject: [PATCH 20/46] Refactored Code. --- SoftLayer/CLI/hardware/guests.py | 6 +++--- SoftLayer/CLI/virt/list.py | 7 +++---- SoftLayer/managers/vs.py | 6 +++--- tests/managers/hardware_tests.py | 18 +----------------- tests/managers/vs/vs_tests.py | 27 +-------------------------- 5 files changed, 11 insertions(+), 53 deletions(-) diff --git a/SoftLayer/CLI/hardware/guests.py b/SoftLayer/CLI/hardware/guests.py index 418cf9d6e..238897e84 100644 --- a/SoftLayer/CLI/hardware/guests.py +++ b/SoftLayer/CLI/hardware/guests.py @@ -1,4 +1,4 @@ -"""List the Hardware server associated virtual guests.""" +"""Lists the Virtual Guests running on this server.""" # :license: MIT, see LICENSE for more details. import click @@ -15,14 +15,14 @@ @click.argument('identifier') @environment.pass_env def cli(env, identifier): - """List the Hardware server associated virtual guests.""" + """Lists the Virtual Guests running on this server.""" mgr = SoftLayer.HardwareManager(env.client) hw_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'hardware') hw_guests = mgr.get_hardware_guests(hw_id) if not hw_guests: - raise exceptions.CLIAbort("The hardware server does not has associated virtual guests.") + raise exceptions.CLIAbort("No Virtual Guests found.") table = formatting.Table(['id', 'hostname', 'CPU', 'Memory', 'Start Date', 'Status', 'powerState']) table.sortby = 'hostname' diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 925f33d2a..1a3c4d545 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -96,11 +96,10 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, hardware_guests = vsi.get_hardware_guests() for hardware in hardware_guests: - if 'virtualHost' in hardware and hardware['virtualHost']['guests']: + if hardware['virtualHost']['guests']: + title = "Hardware(id = {hardwareId}) guests associated".format(hardwareId=hardware['id']) table_hardware_guest = formatting.Table(['id', 'hostname', 'CPU', 'Memory', 'Start Date', 'Status', - 'powerState'], title="Hardware(id = {hardwareId}) guests " - "associated".format(hardwareId=hardware['id']) - ) + 'powerState'], title=title) table_hardware_guest.sortby = 'hostname' for guest in hardware['virtualHost']['guests']: table_hardware_guest.add_row([ diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 6c4d67786..a322c78a1 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -1160,10 +1160,10 @@ def get_local_disks(self, instance_id): return self.guest.getBlockDevices(mask=mask, id=instance_id) def get_hardware_guests(self): - """Returns the hardware server vs associated. + """Returns all virtualHost capable hardware objects and their guests. :return SoftLayer_Hardware[]. """ - object_filter = {"hardware": {"networkGatewayMemberFlag": {"operation": 0}}} - mask = "mask[networkGatewayMemberFlag,virtualHost[guests[powerState]]]" + object_filter = {"hardware": {"virtualHost": {"id": {"operation": "not null"}}}} + mask = "mask[virtualHost[guests[powerState]]]" return self.client.call('SoftLayer_Account', 'getHardware', mask=mask, filter=object_filter) diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index a7824dd61..e98c57492 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -656,23 +656,7 @@ def test_get_hardware_guests(self): result = self.hardware.get_hardware_guests(1234) - self.assertEqual([ - { - "accountId": 11111, - "hostname": "NSX-T Manager", - "id": 22222, - "maxCpu": 16, - "maxCpuUnits": "CORE", - "maxMemory": 49152, - "powerState": { - "keyName": "RUNNING", - "name": "Running" - }, - "status": { - "keyName": "ACTIVE", - "name": "Active" - } - }], result) + self.assertEqual("NSX-T Manager", result[0]['hostname']) class HardwareHelperTests(testing.TestCase): diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py index fcee57be5..34e07dd06 100644 --- a/tests/managers/vs/vs_tests.py +++ b/tests/managers/vs/vs_tests.py @@ -1164,29 +1164,4 @@ def test_get_hardware_guests(self): result = self.vs.get_hardware_guests() - self.assertEqual([{ - "accountId": 11111, - "domain": "vmware.chechu.com", - "hostname": "host14", - "id": 22222, - "virtualHost": { - "accountId": 11111, - "id": 33333, - "name": "host14.vmware.chechu.com", - "guests": [ - { - "accountId": 11111, - "hostname": "NSX-T Manager", - "id": 44444, - "maxCpu": 16, - "maxCpuUnits": "CORE", - "maxMemory": 49152, - "powerState": { - "keyName": "RUNNING", - "name": "Running" - }, - "status": { - "keyName": "ACTIVE", - "name": "Active" - } - }]}}], result) + self.assertEqual("NSX-T Manager", result[0]['virtualHost']['guests'][0]['hostname']) From d1c59f925179f8b3109802eda16103ee41eead7e Mon Sep 17 00:00:00 2001 From: Fernando Ojeda Date: Thu, 23 Jul 2020 12:11:16 -0400 Subject: [PATCH 21/46] Fixed unit test issues. --- SoftLayer/fixtures/SoftLayer_Account.py | 44 +++++++++++++++++++++++-- tests/CLI/modules/vs/vs_tests.py | 13 -------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index 95692ef00..dcb6d32f0 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -146,6 +146,28 @@ 'id': 6660 } }, + "virtualHost": { + "accountId": 11111, + "id": 22222, + "name": "vmware.chechu.com", + "guests": [ + { + "accountId": 11111, + "createDate": "2019-09-05T17:03:42-06:00", + "hostname": "NSX-T Manager", + "id": 33333, + "maxCpu": 16, + "maxCpuUnits": "CORE", + "maxMemory": 49152, + "powerState": { + "keyName": "RUNNING", + "name": "Running" + }, + "status": { + "keyName": "ACTIVE", + "name": "Active" + } + }]} }, { 'id': 1001, 'metricTrackingObject': {'id': 4}, @@ -190,7 +212,13 @@ 'vlanNumber': 3672, 'id': 19082 }, - ] + ], + "virtualHost": { + "accountId": 11111, + "id": 22222, + "name": "host14.vmware.chechu.com", + "guests": [] + } }, { 'id': 1002, 'metricTrackingObject': {'id': 5}, @@ -234,9 +262,21 @@ 'vlanNumber': 3672, 'id': 19082 }, - ] + ], + "virtualHost": { + "accountId": 11111, + "id": 22222, + "name": "host14.vmware.chechu.com", + "guests": [] + } }, { 'id': 1003, + "virtualHost": { + "accountId": 11111, + "id": 22222, + "name": "host14.vmware.chechu.com", + "guests": [] + } }] getDomains = [{'name': 'example.com', 'id': 12345, diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index a4bf28509..783b52743 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -143,19 +143,6 @@ def test_list_vs(self): result = self.run_command(['vs', 'list', '--tag=tag']) self.assert_no_fail(result) - self.assertEqual(json.loads(result.output), - [{'datacenter': 'TEST00', - 'primary_ip': '172.16.240.2', - 'hostname': 'vs-test1', - 'action': None, - 'id': 100, - 'backend_ip': '10.45.19.37'}, - {'datacenter': 'TEST00', - 'primary_ip': '172.16.240.7', - 'hostname': 'vs-test2', - 'action': None, - 'id': 104, - 'backend_ip': '10.45.19.35'}]) @mock.patch('SoftLayer.utils.lookup') def test_detail_vs_empty_billing(self, mock_lookup): From d0ebf61672f3f32230535a599b407a021e958c99 Mon Sep 17 00:00:00 2001 From: caberos Date: Fri, 24 Jul 2020 09:20:04 -0400 Subject: [PATCH 22/46] vs upgrade disk and add new disk --- SoftLayer/CLI/virt/upgrade.py | 12 +++-- SoftLayer/fixtures/SoftLayer_Virtual_Guest.py | 46 ++++++++++++++++--- SoftLayer/managers/vs.py | 43 ++++++++++++++--- tests/CLI/modules/vs/vs_tests.py | 24 ++++++++++ 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/SoftLayer/CLI/virt/upgrade.py b/SoftLayer/CLI/virt/upgrade.py index 463fc077e..23e521863 100644 --- a/SoftLayer/CLI/virt/upgrade.py +++ b/SoftLayer/CLI/virt/upgrade.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -20,15 +21,17 @@ help="CPU core will be on a dedicated host server.") @click.option('--memory', type=virt.MEM_TYPE, help="Memory in megabytes") @click.option('--network', type=click.INT, help="Network port speed in Mbps") +@click.option('--add', type=click.INT, required=False, help="add Hard disk in GB") +@click.option('--disk', nargs=1, help="update the number and capacity in GB Hard disk, E.G {'number':2,'capacity':100}") @click.option('--flavor', type=click.STRING, help="Flavor keyName\nDo not use --memory, --cpu or --private, if you are using flavors") @environment.pass_env -def cli(env, identifier, cpu, private, memory, network, flavor): +def cli(env, identifier, cpu, private, memory, network, flavor, disk, add): """Upgrade a virtual server.""" vsi = SoftLayer.VSManager(env.client) - if not any([cpu, memory, network, flavor]): + if not any([cpu, memory, network, flavor, disk, add]): raise exceptions.ArgumentError("Must provide [--cpu], [--memory], [--network], or [--flavor] to upgrade") if private and not cpu: @@ -40,6 +43,9 @@ def cli(env, identifier, cpu, private, memory, network, flavor): if memory: memory = int(memory / 1024) + if disk is not None: + disk = json.loads(disk) - if not vsi.upgrade(vs_id, cpus=cpu, memory=memory, nic_speed=network, public=not private, preset=flavor): + if not vsi.upgrade(vs_id, cpus=cpu, memory=memory, nic_speed=network, public=not private, preset=flavor, + disk=disk, add=add): raise exceptions.CLIAbort('VS Upgrade Failed') diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py index c42963c8e..47eebe424 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py @@ -8,11 +8,16 @@ 'id': 6327, 'nextInvoiceTotalRecurringAmount': 1.54, 'children': [ - {'nextInvoiceTotalRecurringAmount': 1}, - {'nextInvoiceTotalRecurringAmount': 1}, - {'nextInvoiceTotalRecurringAmount': 1}, - {'nextInvoiceTotalRecurringAmount': 1}, - {'nextInvoiceTotalRecurringAmount': 1}, + {'categoryCode': 'port_speed', + 'nextInvoiceTotalRecurringAmount': 1}, + {'categoryCode': 'guest_core', + 'nextInvoiceTotalRecurringAmount': 1}, + {'categoryCode': 'ram', + 'nextInvoiceTotalRecurringAmount': 1}, + {'categoryCode': 'guest_core', + 'nextInvoiceTotalRecurringAmount': 1}, + {'categoryCode': 'guest_disk1', + 'nextInvoiceTotalRecurringAmount': 1}, ], 'package': { "id": 835, @@ -622,7 +627,35 @@ 'capacity': '2', 'description': 'RAM', } - }, + }, { + "id": 2255, + "categories": [ + { + "categoryCode": "guest_disk1", + "id": 82, + "name": "Second Disk" + }, + { + "categoryCode": "guest_disk2", + "id": 92, + "name": "Third Disk" + }, + { + "categoryCode": "guest_disk3", + "id": 93, + "name": "Fourth Disk" + }, + { + "categoryCode": "guest_disk4", + "id": 116, + "name": "Fifth Disk" + } + ], + "item": { + "capacity": "10", + "description": "10 GB (SAN)" + } + } ] DEDICATED_GET_UPGRADE_ITEM_PRICES = [ @@ -641,7 +674,6 @@ getMetricTrackingObjectId = 1000 - getBandwidthAllotmentDetail = { 'allocationId': 25465663, 'bandwidthAllotmentId': 138442, diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index e63d7a80f..23f51b138 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -18,6 +18,7 @@ LOGGER = logging.getLogger(__name__) + # pylint: disable=no-self-use,too-many-lines @@ -818,7 +819,8 @@ def capture(self, instance_id, name, additional_disks=False, notes=None): return self.guest.createArchiveTransaction( name, disks_to_capture, notes, id=instance_id) - def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=True, preset=None): + def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=True, preset=None, + disk=None, add=None): """Upgrades a VS instance. Example:: @@ -851,7 +853,10 @@ def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=Tr if memory is not None and preset is not None: raise ValueError("Do not use memory, private or cpu if you are using flavors") data['memory'] = memory - + if disk is not None: + data['disk'] = disk.get('capacity') + elif add is not None: + data['disk'] = add maintenance_window = datetime.datetime.now(utils.UTC()) order = { 'complexType': 'SoftLayer_Container_Product_Order_Virtual_Guest_Upgrade', @@ -874,9 +879,30 @@ def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=Tr raise exceptions.SoftLayerError( "Unable to find %s option with value %s" % (option, value)) - prices.append({'id': price_id}) - order['prices'] = prices - + if disk is not None: + category = {'categories': [{ + 'categoryCode': 'guest_disk' + str(disk.get('number')), + 'complexType': "SoftLayer_Product_Item_Category" + }], 'complexType': 'SoftLayer_Product_Item_Price'} + prices.append(category) + prices[0]['id'] = price_id + elif add: + vsi_disk = self.get_instance(instance_id) + disk_number = 0 + for item in vsi_disk.get('billingItem').get('children'): + if item.get('categoryCode').__contains__('guest_disk'): + if disk_number < int("".join(filter(str.isdigit, item.get('categoryCode')))): + disk_number = int("".join(filter(str.isdigit, item.get('categoryCode')))) + category = {'categories': [{ + 'categoryCode': 'guest_disk' + str(disk_number + 1), + 'complexType': "SoftLayer_Product_Item_Category" + }], 'complexType': 'SoftLayer_Product_Item_Price'} + prices.append(category) + prices[0]['id'] = price_id + else: + prices.append({'id': price_id}) + + order['prices'] = prices if preset is not None: vs_object = self.get_instance(instance_id)['billingItem']['package'] order['presetId'] = self.ordering_manager.get_preset_by_key(vs_object['keyName'], preset)['id'] @@ -994,7 +1020,8 @@ def _get_price_id_for_upgrade_option(self, upgrade_prices, option, value, public option_category = { 'memory': 'ram', 'cpus': 'guest_core', - 'nic_speed': 'port_speed' + 'nic_speed': 'port_speed', + 'disk': 'guest_disk' } category_code = option_category.get(option) for price in upgrade_prices: @@ -1006,7 +1033,7 @@ def _get_price_id_for_upgrade_option(self, upgrade_prices, option, value, public or product.get('units') == 'DEDICATED_CORE') for category in price.get('categories'): - if not (category.get('categoryCode') == category_code + if not (category_code == (''.join([i for i in category.get('categoryCode') if not i.isdigit()])) and str(product.get('capacity')) == str(value)): continue @@ -1020,6 +1047,8 @@ def _get_price_id_for_upgrade_option(self, upgrade_prices, option, value, public elif option == 'nic_speed': if 'Public' in product.get('description'): return price.get('id') + elif option == 'disk': + return price.get('id') else: return price.get('id') diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index a4bf28509..a90811fe8 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -570,6 +570,19 @@ def test_upgrade(self, confirm_mock): self.assertIn({'id': 1122}, order_container['prices']) self.assertEqual(order_container['virtualGuests'], [{'id': 100}]) + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_upgrade_disk(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'upgrade', '100', '--flavor=M1_64X512X100', + '--disk={"number":1,"capacity":10}']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + call = self.calls('SoftLayer_Product_Order', 'placeOrder')[0] + order_container = call.args[0] + self.assertEqual(799, order_container['presetId']) + self.assertIn({'id': 100}, order_container['virtualGuests']) + self.assertEqual(order_container['virtualGuests'], [{'id': 100}]) + @mock.patch('SoftLayer.CLI.formatting.confirm') def test_upgrade_with_flavor(self, confirm_mock): confirm_mock.return_value = True @@ -582,6 +595,17 @@ def test_upgrade_with_flavor(self, confirm_mock): self.assertIn({'id': 100}, order_container['virtualGuests']) self.assertEqual(order_container['virtualGuests'], [{'id': 100}]) + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_upgrade_with_add_disk(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'upgrade', '100', '--add=10']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + call = self.calls('SoftLayer_Product_Order', 'placeOrder')[0] + order_container = call.args[0] + self.assertIn({'id': 100}, order_container['virtualGuests']) + self.assertEqual(order_container['virtualGuests'], [{'id': 100}]) + @mock.patch('SoftLayer.CLI.formatting.confirm') def test_upgrade_with_cpu_memory_and_flavor(self, confirm_mock): confirm_mock.return_value = True From bb7b220b6f3fcc6c97f53e3c761166960a43ae51 Mon Sep 17 00:00:00 2001 From: caberos Date: Fri, 24 Jul 2020 11:14:16 -0400 Subject: [PATCH 23/46] fix tox tool --- SoftLayer/managers/vs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 23f51b138..ffc67db7d 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -1033,7 +1033,12 @@ def _get_price_id_for_upgrade_option(self, upgrade_prices, option, value, public or product.get('units') == 'DEDICATED_CORE') for category in price.get('categories'): - if not (category_code == (''.join([i for i in category.get('categoryCode') if not i.isdigit()])) + if option == 'disk': + if not (category_code == (''.join([i for i in category.get('categoryCode') if not i.isdigit()])) + and str(product.get('capacity')) == str(value)): + return price.get('id') + + if not (category.get('categoryCode') == category_code and str(product.get('capacity')) == str(value)): continue From f1abcfc226095be4c8949c09b2c9c8897626ed37 Mon Sep 17 00:00:00 2001 From: caberos Date: Fri, 24 Jul 2020 11:57:41 -0400 Subject: [PATCH 24/46] fix tox tool --- SoftLayer/CLI/virt/upgrade.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/upgrade.py b/SoftLayer/CLI/virt/upgrade.py index 23e521863..ef4477d86 100644 --- a/SoftLayer/CLI/virt/upgrade.py +++ b/SoftLayer/CLI/virt/upgrade.py @@ -1,9 +1,10 @@ """Upgrade a virtual server.""" # :license: MIT, see LICENSE for more details. -import click import json +import click + import SoftLayer from SoftLayer.CLI import environment from SoftLayer.CLI import exceptions From 81bd07df42060b1d2db3848e091878b87a441305 Mon Sep 17 00:00:00 2001 From: caberos Date: Fri, 24 Jul 2020 16:57:24 -0400 Subject: [PATCH 25/46] fix the empty lines --- SoftLayer/CLI/order/preset_list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SoftLayer/CLI/order/preset_list.py b/SoftLayer/CLI/order/preset_list.py index 7397f9428..412d95ee7 100644 --- a/SoftLayer/CLI/order/preset_list.py +++ b/SoftLayer/CLI/order/preset_list.py @@ -43,8 +43,8 @@ def cli(env, package_keyname, keyword): for preset in presets: table.add_row([ - preset['name'], - preset['keyName'], - preset['description'] + str(preset['name']).strip(), + str(preset['keyName']).strip(), + str(preset['description']).strip() ]) env.fout(table) From 730ed0fc48b8c0f2257f3aab0bfc0efe1c58e27b Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 24 Jul 2020 16:59:16 -0500 Subject: [PATCH 26/46] 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 27/46] 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 28/46] 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) From 60fbbbad396f33dad52fd48d72b9ac98d09179b7 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Sun, 26 Jul 2020 12:30:57 -0500 Subject: [PATCH 29/46] fixed vs_tests --- tests/CLI/modules/vs/vs_tests.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index 8278ab23f..94b913923 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -842,9 +842,6 @@ def test_vs_migrate_all_empty(self): 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']) From ded403848217ff778006c15be6f92fdc5033a62c Mon Sep 17 00:00:00 2001 From: ATGE Date: Fri, 24 Jul 2020 16:42:22 -0400 Subject: [PATCH 30/46] #1302 fix lots of whitespace slcli vs create-options --- SoftLayer/CLI/virt/create_options.py | 189 ++++++++++++++++----------- tests/CLI/modules/vs/vs_tests.py | 93 +++++++------ 2 files changed, 170 insertions(+), 112 deletions(-) diff --git a/SoftLayer/CLI/virt/create_options.py b/SoftLayer/CLI/virt/create_options.py index 601c0f3ac..7e671d028 100644 --- a/SoftLayer/CLI/virt/create_options.py +++ b/SoftLayer/CLI/virt/create_options.py @@ -18,56 +18,121 @@ def cli(env): """Virtual server order options.""" vsi = SoftLayer.VSManager(env.client) - result = vsi.get_create_options() + options = vsi.get_create_options() - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' + tables = [ + _get_datacenter_table(options), + _get_flavors_table(options), + _get_cpu_table(options), + _get_memory_table(options), + _get_os_table(options), + _get_disk_table(options), + _get_network_table(options), + ] - # Datacenters + env.fout(formatting.listing(tables, separator='\n')) + + +def _get_datacenter_table(create_options): datacenters = [dc['template']['datacenter']['name'] - for dc in result['datacenters']] + for dc in create_options['datacenters']] + datacenters = sorted(datacenters) - table.add_row(['datacenter', - formatting.listing(datacenters, separator='\n')]) + dc_table = formatting.Table(['datacenter'], title='Datacenters') + dc_table.sortby = 'datacenter' + dc_table.align = 'l' + for datacenter in datacenters: + dc_table.add_row([datacenter]) + return dc_table - _add_flavors_to_table(result, table) - # CPUs - standard_cpus = [int(x['template']['startCpus']) for x in result['processors'] +def _get_flavors_table(create_options): + flavor_table = formatting.Table(['flavor', 'value'], title='Flavors') + flavor_table.sortby = 'flavor' + flavor_table.align = 'l' + grouping = { + 'balanced': {'key_starts_with': 'B1', 'flavors': []}, + 'balanced local - hdd': {'key_starts_with': 'BL1', 'flavors': []}, + 'balanced local - ssd': {'key_starts_with': 'BL2', 'flavors': []}, + 'compute': {'key_starts_with': 'C1', 'flavors': []}, + 'memory': {'key_starts_with': 'M1', 'flavors': []}, + 'GPU': {'key_starts_with': 'AC', 'flavors': []}, + 'transient': {'transient': True, 'flavors': []}, + } + + if create_options.get('flavors', None) is None: + return + + for flavor_option in create_options['flavors']: + flavor_key_name = utils.lookup(flavor_option, 'flavor', 'keyName') + + for name, group in grouping.items(): + if utils.lookup(flavor_option, 'template', 'transientGuestFlag') is True: + if utils.lookup(group, 'transient') is True: + group['flavors'].append(flavor_key_name) + break + + elif utils.lookup(group, 'key_starts_with') is not None \ + and flavor_key_name.startswith(group['key_starts_with']): + group['flavors'].append(flavor_key_name) + break + + for name, group in grouping.items(): + if len(group['flavors']) > 0: + flavor_table.add_row(['{}'.format(name), + formatting.listing(group['flavors'], + separator='\n')]) + return flavor_table + + +def _get_cpu_table(create_options): + cpu_table = formatting.Table(['cpu', 'value'], title='CPUs') + cpu_table.sortby = 'cpu' + cpu_table.align = 'l' + standard_cpus = [int(x['template']['startCpus']) for x in create_options['processors'] if not x['template'].get('dedicatedAccountHostOnlyFlag', False) and not x['template'].get('dedicatedHost', None)] - ded_cpus = [int(x['template']['startCpus']) for x in result['processors'] + ded_cpus = [int(x['template']['startCpus']) for x in create_options['processors'] if x['template'].get('dedicatedAccountHostOnlyFlag', False)] - ded_host_cpus = [int(x['template']['startCpus']) for x in result['processors'] + ded_host_cpus = [int(x['template']['startCpus']) for x in create_options['processors'] if x['template'].get('dedicatedHost', None)] standard_cpus = sorted(standard_cpus) - table.add_row(['cpus (standard)', formatting.listing(standard_cpus, separator=',')]) + cpu_table.add_row(['standard', formatting.listing(standard_cpus, separator=',')]) ded_cpus = sorted(ded_cpus) - table.add_row(['cpus (dedicated)', formatting.listing(ded_cpus, separator=',')]) + cpu_table.add_row(['dedicated', formatting.listing(ded_cpus, separator=',')]) ded_host_cpus = sorted(ded_host_cpus) - table.add_row(['cpus (dedicated host)', formatting.listing(ded_host_cpus, separator=',')]) + cpu_table.add_row(['dedicated host', formatting.listing(ded_host_cpus, separator=',')]) + return cpu_table + - # Memory - memory = [int(m['template']['maxMemory']) for m in result['memory'] +def _get_memory_table(create_options): + memory_table = formatting.Table(['memory', 'value'], title='Memories') + memory_table.sortby = 'memory' + memory_table.align = 'l' + memory = [int(m['template']['maxMemory']) for m in create_options['memory'] if not m['itemPrice'].get('dedicatedHostInstanceFlag', False)] - ded_host_memory = [int(m['template']['maxMemory']) for m in result['memory'] + ded_host_memory = [int(m['template']['maxMemory']) for m in create_options['memory'] if m['itemPrice'].get('dedicatedHostInstanceFlag', False)] memory = sorted(memory) - table.add_row(['memory', - formatting.listing(memory, separator=',')]) + memory_table.add_row(['standard', + formatting.listing(memory, separator=',')]) ded_host_memory = sorted(ded_host_memory) - table.add_row(['memory (dedicated host)', - formatting.listing(ded_host_memory, separator=',')]) + memory_table.add_row(['dedicated host', + formatting.listing(ded_host_memory, separator=',')]) + return memory_table - # Operating Systems + +def _get_os_table(create_options): + os_table = formatting.Table(['os', 'value'], title='Operating Systems') + os_table.sortby = 'os' + os_table.align = 'l' op_sys = [o['template']['operatingSystemReferenceCode'] for o in - result['operatingSystems']] + create_options['operatingSystems']] op_sys = sorted(op_sys) os_summary = set() @@ -76,24 +141,29 @@ def cli(env): os_summary.add(operating_system[0:operating_system.find('_')]) for summary in sorted(os_summary): - table.add_row([ - 'os (%s)' % summary, + os_table.add_row([ + summary, os.linesep.join(sorted([x for x in op_sys if x[0:len(summary)] == summary])) ]) + return os_table + - # Disk - local_disks = [x for x in result['blockDevices'] +def _get_disk_table(create_options): + disk_table = formatting.Table(['disk', 'value'], title='Disks') + disk_table.sortby = 'disk' + disk_table.align = 'l' + local_disks = [x for x in create_options['blockDevices'] if x['template'].get('localDiskFlag', False) and not x['itemPrice'].get('dedicatedHostInstanceFlag', False)] - ded_host_local_disks = [x for x in result['blockDevices'] + ded_host_local_disks = [x for x in create_options['blockDevices'] if x['template'].get('localDiskFlag', False) and x['itemPrice'].get('dedicatedHostInstanceFlag', False)] - san_disks = [x for x in result['blockDevices'] + san_disks = [x for x in create_options['blockDevices'] if not x['template'].get('localDiskFlag', False)] def add_block_rows(disks, name): @@ -109,18 +179,23 @@ def add_block_rows(disks, name): simple[bid].append(str(block['diskImage']['capacity'])) for label in sorted(simple): - table.add_row(['%s disk(%s)' % (name, label), - formatting.listing(simple[label], - separator=',')]) + disk_table.add_row(['%s disk(%s)' % (name, label), + formatting.listing(simple[label], + separator=',')]) add_block_rows(san_disks, 'san') add_block_rows(local_disks, 'local') add_block_rows(ded_host_local_disks, 'local (dedicated host)') + return disk_table + - # Network +def _get_network_table(create_options): + network_table = formatting.Table(['network', 'value'], title='Network') + network_table.sortby = 'network' + network_table.align = 'l' speeds = [] ded_host_speeds = [] - for option in result['networkComponents']: + for option in create_options['networkComponents']: template = option.get('template', None) price = option.get('itemPrice', None) @@ -140,43 +215,9 @@ def add_block_rows(disks, name): speeds.append(max_speed) speeds = sorted(speeds) - table.add_row(['nic', formatting.listing(speeds, separator=',')]) + network_table.add_row(['nic', formatting.listing(speeds, separator=',')]) ded_host_speeds = sorted(ded_host_speeds) - table.add_row(['nic (dedicated host)', - formatting.listing(ded_host_speeds, separator=',')]) - - env.fout(table) - - -def _add_flavors_to_table(result, table): - grouping = { - 'balanced': {'key_starts_with': 'B1', 'flavors': []}, - 'balanced local - hdd': {'key_starts_with': 'BL1', 'flavors': []}, - 'balanced local - ssd': {'key_starts_with': 'BL2', 'flavors': []}, - 'compute': {'key_starts_with': 'C1', 'flavors': []}, - 'memory': {'key_starts_with': 'M1', 'flavors': []}, - 'GPU': {'key_starts_with': 'AC', 'flavors': []}, - 'transient': {'transient': True, 'flavors': []}, - } - - if result.get('flavors', None) is None: - return - - for flavor_option in result['flavors']: - flavor_key_name = utils.lookup(flavor_option, 'flavor', 'keyName') - - for name, group in grouping.items(): - if utils.lookup(flavor_option, 'template', 'transientGuestFlag') is True: - if utils.lookup(group, 'transient') is True: - group['flavors'].append(flavor_key_name) - break - - elif utils.lookup(group, 'key_starts_with') is not None \ - and flavor_key_name.startswith(group['key_starts_with']): - group['flavors'].append(flavor_key_name) - break - - for name, group in grouping.items(): - if len(group['flavors']) > 0: - table.add_row(['flavors (%s)' % name, formatting.listing(group['flavors'], separator='\n')]) + network_table.add_row(['nic (dedicated host)', + formatting.listing(ded_host_speeds, separator=',')]) + return network_table diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index 94b913923..6494f0c40 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -315,28 +315,45 @@ def test_detail_vs_ptr_error(self): def test_create_options(self): result = self.run_command(['vs', 'create-options']) + expected_json_result = [ + [ + {"Datacenter": "ams01"}, + {"Datacenter": "dal05"} + ], + [ + {"flavor": "balanced", "value": ["B1_1X2X25", "B1_1X2X100"]}, + {"flavor": "balanced local - hdd", "value": ["BL1_1X2X100"]}, + {"flavor": "balanced local - ssd", "value": ["BL2_1X2X100"]}, + {"flavor": "compute", "value": ["C1_1X2X25"]}, + {"flavor": "memory", "value": ["M1_1X2X100"]}, + {"flavor": "GPU", "value": ["AC1_1X2X100", "ACL1_1X2X100"]}, + {"flavor": "transient", "value": ["B1_1X2X25_TRANSIENT"]} + ], + [ + {"cpu": "standard", "value": [1, 2, 3, 4]}, + {"cpu": "dedicated", "value": [1]}, + {"cpu": "dedicated host", "value": [4, 56]} + ], + [ + {"memory": "standard", "value": [1024, 2048, 3072, 4096]}, + {"memory": "dedicated host", "value": [8192, 65536]} + ], + [ + {"os": "CENTOS", "value": "CENTOS_6_64"}, + {"os": "DEBIAN", "value": "DEBIAN_7_64"}, + {"os": "UBUNTU", "value": "UBUNTU_12_64"} + ], + [ + {"disk": "local disk(0)", "value": ["25", "100"]} + ], + [ + {"network": "nic", "value": ["10", "100", "1000"]}, + {"network": "nic (dedicated host)", "value": ["1000"]} + ] + ] self.assert_no_fail(result) - self.assertEqual({'cpus (dedicated host)': [4, 56], - 'cpus (dedicated)': [1], - 'cpus (standard)': [1, 2, 3, 4], - 'datacenter': ['ams01', 'dal05'], - 'flavors (balanced)': ['B1_1X2X25', 'B1_1X2X100'], - 'flavors (balanced local - hdd)': ['BL1_1X2X100'], - 'flavors (balanced local - ssd)': ['BL2_1X2X100'], - 'flavors (compute)': ['C1_1X2X25'], - 'flavors (memory)': ['M1_1X2X100'], - 'flavors (GPU)': ['AC1_1X2X100', 'ACL1_1X2X100'], - 'flavors (transient)': ['B1_1X2X25_TRANSIENT'], - 'local disk(0)': ['25', '100'], - 'memory': [1024, 2048, 3072, 4096], - 'memory (dedicated host)': [8192, 65536], - 'nic': ['10', '100', '1000'], - 'nic (dedicated host)': ['1000'], - 'os (CENTOS)': 'CENTOS_6_64', - 'os (DEBIAN)': 'DEBIAN_7_64', - 'os (UBUNTU)': 'UBUNTU_12_64'}, - json.loads(result.output)) + self.assertEqual(expected_json_result, json.loads(result.output)) @mock.patch('SoftLayer.CLI.formatting.confirm') def test_dns_sync_both(self, confirm_mock): @@ -357,19 +374,19 @@ def test_dns_sync_both(self, confirm_mock): 'getResourceRecords') getResourceRecords.return_value = [] createAargs = ({ - 'type': 'a', - 'host': 'vs-test1', - 'domainId': 12345, # from SoftLayer_Account::getDomains - 'data': '172.16.240.2', - 'ttl': 7200 - },) + 'type': 'a', + 'host': 'vs-test1', + 'domainId': 12345, # from SoftLayer_Account::getDomains + 'data': '172.16.240.2', + 'ttl': 7200 + },) createPTRargs = ({ - 'type': 'ptr', - 'host': '2', - 'domainId': 123456, - 'data': 'vs-test1.test.sftlyr.ws', - 'ttl': 7200 - },) + 'type': 'ptr', + 'host': '2', + 'domainId': 123456, + 'data': 'vs-test1.test.sftlyr.ws', + 'ttl': 7200 + },) result = self.run_command(['vs', 'dns-sync', '100']) @@ -412,12 +429,12 @@ def test_dns_sync_v6(self, confirm_mock): } } createV6args = ({ - 'type': 'aaaa', - 'host': 'vs-test1', - 'domainId': 12345, - 'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004', - 'ttl': 7200 - },) + 'type': 'aaaa', + 'host': 'vs-test1', + 'domainId': 12345, + 'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004', + 'ttl': 7200 + },) guest.return_value = test_guest result = self.run_command(['vs', 'dns-sync', '--aaaa-record', '100']) self.assert_no_fail(result) From 6f873b04971268effc5c365badac3d098504f868 Mon Sep 17 00:00:00 2001 From: ATGE Date: Fri, 24 Jul 2020 16:56:57 -0400 Subject: [PATCH 31/46] fix vs tests create options --- tests/CLI/modules/vs/vs_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index 6494f0c40..bf0221170 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -317,8 +317,8 @@ def test_create_options(self): result = self.run_command(['vs', 'create-options']) expected_json_result = [ [ - {"Datacenter": "ams01"}, - {"Datacenter": "dal05"} + {"datacenter": "ams01"}, + {"datacenter": "dal05"} ], [ {"flavor": "balanced", "value": ["B1_1X2X25", "B1_1X2X100"]}, From b8d56f38161b356c2f2c8b8920f7c42c1708b282 Mon Sep 17 00:00:00 2001 From: ATGE Date: Fri, 24 Jul 2020 19:04:32 -0400 Subject: [PATCH 32/46] fix tox issues --- SoftLayer/CLI/virt/create_options.py | 2 +- tests/CLI/modules/vs/vs_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/virt/create_options.py b/SoftLayer/CLI/virt/create_options.py index 7e671d028..85139b8ba 100644 --- a/SoftLayer/CLI/virt/create_options.py +++ b/SoftLayer/CLI/virt/create_options.py @@ -62,7 +62,7 @@ def _get_flavors_table(create_options): } if create_options.get('flavors', None) is None: - return + return flavor_table for flavor_option in create_options['flavors']: flavor_key_name = utils.lookup(flavor_option, 'flavor', 'keyName') diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index bf0221170..629b6e14f 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -351,7 +351,7 @@ def test_create_options(self): {"network": "nic (dedicated host)", "value": ["1000"]} ] ] - + self.maxDiff = None self.assert_no_fail(result) self.assertEqual(expected_json_result, json.loads(result.output)) From 452922eb17c906ba0a644d25ed5cd2feada46f1b Mon Sep 17 00:00:00 2001 From: ATGE Date: Fri, 24 Jul 2020 19:56:29 -0400 Subject: [PATCH 33/46] fix the VirtTests.test_create_options test --- tests/CLI/modules/vs/vs_tests.py | 44 +++++--------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index 629b6e14f..06d3147ac 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -315,45 +315,13 @@ def test_detail_vs_ptr_error(self): def test_create_options(self): result = self.run_command(['vs', 'create-options']) - expected_json_result = [ - [ - {"datacenter": "ams01"}, - {"datacenter": "dal05"} - ], - [ - {"flavor": "balanced", "value": ["B1_1X2X25", "B1_1X2X100"]}, - {"flavor": "balanced local - hdd", "value": ["BL1_1X2X100"]}, - {"flavor": "balanced local - ssd", "value": ["BL2_1X2X100"]}, - {"flavor": "compute", "value": ["C1_1X2X25"]}, - {"flavor": "memory", "value": ["M1_1X2X100"]}, - {"flavor": "GPU", "value": ["AC1_1X2X100", "ACL1_1X2X100"]}, - {"flavor": "transient", "value": ["B1_1X2X25_TRANSIENT"]} - ], - [ - {"cpu": "standard", "value": [1, 2, 3, 4]}, - {"cpu": "dedicated", "value": [1]}, - {"cpu": "dedicated host", "value": [4, 56]} - ], - [ - {"memory": "standard", "value": [1024, 2048, 3072, 4096]}, - {"memory": "dedicated host", "value": [8192, 65536]} - ], - [ - {"os": "CENTOS", "value": "CENTOS_6_64"}, - {"os": "DEBIAN", "value": "DEBIAN_7_64"}, - {"os": "UBUNTU", "value": "UBUNTU_12_64"} - ], - [ - {"disk": "local disk(0)", "value": ["25", "100"]} - ], - [ - {"network": "nic", "value": ["10", "100", "1000"]}, - {"network": "nic (dedicated host)", "value": ["1000"]} - ] - ] - self.maxDiff = None self.assert_no_fail(result) - self.assertEqual(expected_json_result, json.loads(result.output)) + self.assertIn('datacenter', result.output) + self.assertIn('flavor', result.output) + self.assertIn('memory', result.output) + self.assertIn('cpu', result.output) + self.assertIn('OS', result.output) + self.assertIn('network', result.output) @mock.patch('SoftLayer.CLI.formatting.confirm') def test_dns_sync_both(self, confirm_mock): From 9ff03c0f3f2d8b8a4b1c7bd6a7236c732a97d3a8 Mon Sep 17 00:00:00 2001 From: ATGE Date: Fri, 24 Jul 2020 19:57:24 -0400 Subject: [PATCH 34/46] fix tox analysis issue --- SoftLayer/CLI/virt/create_options.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/SoftLayer/CLI/virt/create_options.py b/SoftLayer/CLI/virt/create_options.py index 85139b8ba..693de74a2 100644 --- a/SoftLayer/CLI/virt/create_options.py +++ b/SoftLayer/CLI/virt/create_options.py @@ -1,9 +1,6 @@ """Virtual server order options.""" # :license: MIT, see LICENSE for more details. # pylint: disable=too-many-statements -import os -import os.path - import click import SoftLayer @@ -128,23 +125,21 @@ def _get_memory_table(create_options): def _get_os_table(create_options): - os_table = formatting.Table(['os', 'value'], title='Operating Systems') - os_table.sortby = 'os' + os_table = formatting.Table(['KeyName', 'Description'], title='Operating Systems') + os_table.sortby = 'KeyName' os_table.align = 'l' - op_sys = [o['template']['operatingSystemReferenceCode'] for o in - create_options['operatingSystems']] - - op_sys = sorted(op_sys) - os_summary = set() + op_sys = [] + for operating_system in create_options['operatingSystems']: + os_option = { + 'referenceCode': operating_system['template']['operatingSystemReferenceCode'], + 'description': operating_system['itemPrice']['item']['description'] + } + op_sys.append(os_option) for operating_system in op_sys: - os_summary.add(operating_system[0:operating_system.find('_')]) - - for summary in sorted(os_summary): os_table.add_row([ - summary, - os.linesep.join(sorted([x for x in op_sys - if x[0:len(summary)] == summary])) + operating_system['referenceCode'], + operating_system['description'] ]) return os_table From 1d25ad81141989c871c70170e5ed2841a31fdb79 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Mon, 27 Jul 2020 16:56:44 -0500 Subject: [PATCH 35/46] added support for filteredMask --- SoftLayer/transports.py | 3 ++- tests/transport_tests.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/SoftLayer/transports.py b/SoftLayer/transports.py index 02cd7a214..5722e1867 100644 --- a/SoftLayer/transports.py +++ b/SoftLayer/transports.py @@ -564,7 +564,8 @@ def _format_object_mask(objectmask): objectmask = objectmask.strip() if (not objectmask.startswith('mask') and - not objectmask.startswith('[')): + not objectmask.startswith('[') and + not objectmask.startswith('filteredMask')): objectmask = "mask[%s]" % objectmask return objectmask diff --git a/tests/transport_tests.py b/tests/transport_tests.py index 6e6c793fd..a2e500bd8 100644 --- a/tests/transport_tests.py +++ b/tests/transport_tests.py @@ -228,6 +228,22 @@ def test_mask_call_v2(self, request): "mask[something[nested]]", kwargs['data']) + @mock.patch('SoftLayer.transports.requests.Session.request') + def test_mask_call_filteredMask(self, request): + request.return_value = self.response + + req = transports.Request() + req.endpoint = "http://something.com" + req.service = "SoftLayer_Service" + req.method = "getObject" + req.mask = "filteredMask[something[nested]]" + self.transport(req) + + args, kwargs = request.call_args + self.assertIn( + "filteredMask[something[nested]]", + kwargs['data']) + @mock.patch('SoftLayer.transports.requests.Session.request') def test_mask_call_v2_dot(self, request): request.return_value = self.response From 73ea275c20d196c3f9a31d4af3287d1818b692f2 Mon Sep 17 00:00:00 2001 From: ATGE Date: Tue, 28 Jul 2020 18:37:06 -0400 Subject: [PATCH 36/46] #1305 update the old Bluemix URLs to the IBM Cloud Docs URL --- CHANGELOG.md | 2 +- SoftLayer/CLI/block/order.py | 2 +- SoftLayer/CLI/file/order.py | 2 +- SoftLayer/CLI/image/export.py | 6 ++++-- SoftLayer/CLI/image/import.py | 10 +++++----- SoftLayer/managers/image.py | 2 +- SoftLayer/managers/vs_capacity.py | 2 +- docs/cli/vs/reserved_capacity.rst | 4 ++-- 8 files changed, 16 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b6b28c4e..de7ae2c03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -228,7 +228,7 @@ https://github.com/softlayer/softlayer-python/compare/v5.8.2...v5.8.3 ## [5.6.0] - 2018-10-16 - Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.3...v5.6.0 -+ #1026 Support for [Reserved Capacity](https://console.bluemix.net/docs/vsi/vsi_about_reserved.html#about-reserved-virtual-servers) ++ #1026 Support for [Reserved Capacity](https://cloud.ibm.com/docs/virtual-servers?topic=virtual-servers-about-reserved-virtual-servers) * `slcli vs capacity create` * `slcli vs capacity create-guest` * `slcli vs capacity create-options` diff --git a/SoftLayer/CLI/block/order.py b/SoftLayer/CLI/block/order.py index 302b45cc8..738080fd0 100644 --- a/SoftLayer/CLI/block/order.py +++ b/SoftLayer/CLI/block/order.py @@ -63,7 +63,7 @@ def cli(env, storage_type, size, iops, tier, os_type, """Order a block storage volume. Valid size and iops options can be found here: - https://console.bluemix.net/docs/infrastructure/BlockStorage/index.html#provisioning + https://cloud.ibm.com/docs/BlockStorage/index.html#provisioning-considerations """ block_manager = SoftLayer.BlockStorageManager(env.client) storage_type = storage_type.lower() diff --git a/SoftLayer/CLI/file/order.py b/SoftLayer/CLI/file/order.py index 9e1c9cd29..e665a088b 100644 --- a/SoftLayer/CLI/file/order.py +++ b/SoftLayer/CLI/file/order.py @@ -52,7 +52,7 @@ def cli(env, storage_type, size, iops, tier, """Order a file storage volume. Valid size and iops options can be found here: - https://console.bluemix.net/docs/infrastructure/FileStorage/index.html#provisioning + https://cloud.ibm.com/docs/FileStorage/index.html#provisioning-considerations """ file_manager = SoftLayer.FileStorageManager(env.client) storage_type = storage_type.lower() diff --git a/SoftLayer/CLI/image/export.py b/SoftLayer/CLI/image/export.py index eb9081ac7..c8215c28c 100644 --- a/SoftLayer/CLI/image/export.py +++ b/SoftLayer/CLI/image/export.py @@ -16,8 +16,10 @@ default=None, help="The IBM Cloud API Key with access to IBM Cloud Object " "Storage instance. For help creating this key see " - "https://console.bluemix.net/docs/services/cloud-object-" - "storage/iam/users-serviceids.html#serviceidapikeys") + "https://cloud.ibm.com/docs/cloud-object-storage?" + "topic=cloud-object-storage-iam-overview#iam-overview" + "-service-id-api-key " + ) @environment.pass_env def cli(env, identifier, uri, ibm_api_key): """Export an image to object storage. diff --git a/SoftLayer/CLI/image/import.py b/SoftLayer/CLI/image/import.py index 53082c9ac..83d2abcf2 100644 --- a/SoftLayer/CLI/image/import.py +++ b/SoftLayer/CLI/image/import.py @@ -22,17 +22,17 @@ default=None, help="The IBM Cloud API Key with access to IBM Cloud Object " "Storage instance and IBM KeyProtect instance. For help " - "creating this key see https://console.bluemix.net/docs/" - "services/cloud-object-storage/iam/users-serviceids.html" - "#serviceidapikeys") + "creating this key see https://cloud.ibm.com/docs/" + "cloud-object-storage?topic=cloud-object-storage" + "-iam-overview#iam-overview-service-id-api-key") @click.option('--root-key-crn', default=None, help="CRN of the root key in your KMS instance") @click.option('--wrapped-dek', default=None, help="Wrapped Data Encryption Key provided by IBM KeyProtect. " - "For more info see https://console.bluemix.net/docs/" - "services/key-protect/wrap-keys.html#wrap-keys") + "For more info see " + "https://cloud.ibm.com/docs/key-protect?topic=key-protect-wrap-keys") @click.option('--cloud-init', is_flag=True, help="Specifies if image is cloud-init") diff --git a/SoftLayer/managers/image.py b/SoftLayer/managers/image.py index d30b05305..b76a959a6 100644 --- a/SoftLayer/managers/image.py +++ b/SoftLayer/managers/image.py @@ -17,7 +17,7 @@ class ImageManager(utils.IdentifierMixin, object): """Manages SoftLayer server images. See product information here: - https://console.bluemix.net/docs/infrastructure/image-templates/image_index.html + https://cloud.ibm.com/docs/image-templates :param SoftLayer.API.BaseClient client: the client instance """ diff --git a/SoftLayer/managers/vs_capacity.py b/SoftLayer/managers/vs_capacity.py index 3f6574f12..813e2d565 100644 --- a/SoftLayer/managers/vs_capacity.py +++ b/SoftLayer/managers/vs_capacity.py @@ -24,7 +24,7 @@ class CapacityManager(utils.IdentifierMixin, object): Product Information - - https://console.bluemix.net/docs/vsi/vsi_about_reserved.html + - https://cloud.ibm.com/docs/virtual-servers?topic=virtual-servers-about-reserved-virtual-servers - https://softlayer.github.io/reference/services/SoftLayer_Virtual_ReservedCapacityGroup/ - https://softlayer.github.io/reference/services/SoftLayer_Virtual_ReservedCapacityGroup_Instance/ diff --git a/docs/cli/vs/reserved_capacity.rst b/docs/cli/vs/reserved_capacity.rst index 3193febff..37e74000c 100644 --- a/docs/cli/vs/reserved_capacity.rst +++ b/docs/cli/vs/reserved_capacity.rst @@ -5,8 +5,8 @@ Working with Reserved Capacity There are two main concepts for Reserved Capacity. The `Reserved Capacity Group `_ and the `Reserved Capacity Instance `_ The Reserved Capacity Group, is a set block of capacity set aside for you at the time of the order. It will contain a set number of Instances which are all the same size. Instances can be ordered like normal VSIs, with the exception that you need to include the reservedCapacityGroupId, and it must be the same size as the group you are ordering the instance in. -- `About Reserved Capacity `_ -- `Reserved Capacity FAQ `_ +- `About Reserved Capacity `_ +- `Reserved Capacity FAQ `_ The SLCLI supports some basic Reserved Capacity Features. From 95ad3be5be757c53ecf1b0732237eda4acaf54f8 Mon Sep 17 00:00:00 2001 From: ATGE Date: Wed, 29 Jul 2020 19:13:11 -0400 Subject: [PATCH 37/46] 1305 update softlayer.com urls to ibm.com/cloud urls --- SoftLayer/managers/block.py | 2 +- SoftLayer/managers/dns.py | 2 +- SoftLayer/managers/firewall.py | 2 +- SoftLayer/managers/hardware.py | 2 +- SoftLayer/managers/network.py | 2 +- SoftLayer/managers/object_storage.py | 2 +- SoftLayer/managers/ssl.py | 2 +- SoftLayer/managers/ticket.py | 2 +- SoftLayer/managers/vs.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py index 4d129d07c..a16500919 100644 --- a/SoftLayer/managers/block.py +++ b/SoftLayer/managers/block.py @@ -15,7 +15,7 @@ class BlockStorageManager(StorageManager): """Manages SoftLayer Block Storage volumes. - See product information here: http://www.softlayer.com/block-storage + See product information here: https://www.ibm.com/cloud/block-storage """ def list_block_volume_limit(self): diff --git a/SoftLayer/managers/dns.py b/SoftLayer/managers/dns.py index 3a2ea9147..1e89ec9cf 100644 --- a/SoftLayer/managers/dns.py +++ b/SoftLayer/managers/dns.py @@ -14,7 +14,7 @@ class DNSManager(utils.IdentifierMixin, object): """Manage SoftLayer DNS. - See product information here: http://www.softlayer.com/DOMAIN-SERVICES + See product information here: https://www.ibm.com/cloud/dns :param SoftLayer.API.BaseClient client: the client instance diff --git a/SoftLayer/managers/firewall.py b/SoftLayer/managers/firewall.py index 2b1d8e452..34b197521 100644 --- a/SoftLayer/managers/firewall.py +++ b/SoftLayer/managers/firewall.py @@ -32,7 +32,7 @@ def has_firewall(vlan): class FirewallManager(utils.IdentifierMixin, object): """Manages SoftLayer firewalls - See product information here: http://www.softlayer.com/firewalls + See product information here: https://www.ibm.com/cloud/network-security :param SoftLayer.API.BaseClient client: the client instance diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 8ac8d7edd..3a7acbfbd 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -41,7 +41,7 @@ class HardwareManager(utils.IdentifierMixin, object): client = SoftLayer.Client() mgr = SoftLayer.HardwareManager(client) - See product information here: http://www.softlayer.com/bare-metal-servers + See product information here: https://www.ibm.com/cloud/bare-metal-servers :param SoftLayer.API.BaseClient client: the client instance :param SoftLayer.managers.OrderingManager ordering_manager: an optional diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index f9c5f4af0..1e9296cac 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -57,7 +57,7 @@ class NetworkManager(object): """Manage SoftLayer network objects: VLANs, subnets, IPs and rwhois - See product information here: http://www.softlayer.com/networking + See product information here: https://www.ibm.com/cloud/network :param SoftLayer.API.BaseClient client: the client instance diff --git a/SoftLayer/managers/object_storage.py b/SoftLayer/managers/object_storage.py index 2560d26c8..a3a84414b 100644 --- a/SoftLayer/managers/object_storage.py +++ b/SoftLayer/managers/object_storage.py @@ -18,7 +18,7 @@ class ObjectStorageManager(object): """Manager for SoftLayer Object Storage accounts. - See product information here: http://www.softlayer.com/object-storage + See product information here: https://www.ibm.com/cloud/object-storage :param SoftLayer.API.BaseClient client: the client instance diff --git a/SoftLayer/managers/ssl.py b/SoftLayer/managers/ssl.py index 3fa2ac1dd..5474d9cb3 100644 --- a/SoftLayer/managers/ssl.py +++ b/SoftLayer/managers/ssl.py @@ -10,7 +10,7 @@ class SSLManager(object): """Manages SSL certificates in SoftLayer. - See product information here: http://www.softlayer.com/ssl-certificates + See product information here: https://www.ibm.com/cloud/ssl-certificates Example:: diff --git a/SoftLayer/managers/ticket.py b/SoftLayer/managers/ticket.py index 9ff361d4b..bbd2eddd2 100644 --- a/SoftLayer/managers/ticket.py +++ b/SoftLayer/managers/ticket.py @@ -11,7 +11,7 @@ class TicketManager(utils.IdentifierMixin, object): """Manages SoftLayer support tickets. - See product information here: http://www.softlayer.com/support + See product information here: https://www.ibm.com/cloud/support :param SoftLayer.API.BaseClient client: the client instance diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 0e27c4cd3..9995e7ec2 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -24,7 +24,7 @@ class VSManager(utils.IdentifierMixin, object): """Manages SoftLayer Virtual Servers. - See product information here: http://www.softlayer.com/virtual-servers + See product information here: https://www.ibm.com/cloud/virtual-servers Example:: From 194723993b0db8dc7346f9f32b5ed143fad9a8e6 Mon Sep 17 00:00:00 2001 From: caberos Date: Mon, 3 Aug 2020 19:11:38 -0400 Subject: [PATCH 38/46] fix and improve the new code --- SoftLayer/CLI/virt/upgrade.py | 29 +++++++++----- SoftLayer/managers/vs.py | 65 +++++++++++++++++--------------- tests/CLI/modules/vs/vs_tests.py | 40 ++++++++++---------- 3 files changed, 73 insertions(+), 61 deletions(-) diff --git a/SoftLayer/CLI/virt/upgrade.py b/SoftLayer/CLI/virt/upgrade.py index ef4477d86..4afeb8be6 100644 --- a/SoftLayer/CLI/virt/upgrade.py +++ b/SoftLayer/CLI/virt/upgrade.py @@ -1,8 +1,6 @@ """Upgrade a virtual server.""" # :license: MIT, see LICENSE for more details. -import json - import click import SoftLayer @@ -22,18 +20,20 @@ help="CPU core will be on a dedicated host server.") @click.option('--memory', type=virt.MEM_TYPE, help="Memory in megabytes") @click.option('--network', type=click.INT, help="Network port speed in Mbps") -@click.option('--add', type=click.INT, required=False, help="add Hard disk in GB") -@click.option('--disk', nargs=1, help="update the number and capacity in GB Hard disk, E.G {'number':2,'capacity':100}") +@click.option('--add-disk', type=click.INT, multiple=True, required=False, help="add Hard disk in GB") +@click.option('--resize-disk', nargs=2, multiple=True, type=(int, int), + help="Update disk number to size in GB. --resize-disk 250 2 ") @click.option('--flavor', type=click.STRING, help="Flavor keyName\nDo not use --memory, --cpu or --private, if you are using flavors") @environment.pass_env -def cli(env, identifier, cpu, private, memory, network, flavor, disk, add): +def cli(env, identifier, cpu, private, memory, network, flavor, add_disk, resize_disk): """Upgrade a virtual server.""" vsi = SoftLayer.VSManager(env.client) - if not any([cpu, memory, network, flavor, disk, add]): - raise exceptions.ArgumentError("Must provide [--cpu], [--memory], [--network], or [--flavor] to upgrade") + if not any([cpu, memory, network, flavor, resize_disk, add_disk]): + raise exceptions.ArgumentError("Must provide [--cpu]," + " [--memory], [--network], [--flavor], [--resize-disk], or [--add] to upgrade") if private and not cpu: raise exceptions.ArgumentError("Must specify [--cpu] when using [--private]") @@ -44,9 +44,18 @@ def cli(env, identifier, cpu, private, memory, network, flavor, disk, add): if memory: memory = int(memory / 1024) - if disk is not None: - disk = json.loads(disk) + if resize_disk: + disk_json = list() + for guest_disk in resize_disk: + disks = {'capacity': guest_disk[0], 'number': guest_disk[1]} + disk_json.append(disks) + + elif add_disk: + disk_json = list() + for guest_disk in add_disk: + disks = {'capacity': guest_disk, 'number': -1} + disk_json.append(disks) if not vsi.upgrade(vs_id, cpus=cpu, memory=memory, nic_speed=network, public=not private, preset=flavor, - disk=disk, add=add): + disk=disk_json): raise exceptions.CLIAbort('VS Upgrade Failed') diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index ffc67db7d..672a74701 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -819,8 +819,7 @@ def capture(self, instance_id, name, additional_disks=False, notes=None): return self.guest.createArchiveTransaction( name, disks_to_capture, notes, id=instance_id) - def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=True, preset=None, - disk=None, add=None): + def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=True, preset=None, disk=None): """Upgrades a VS instance. Example:: @@ -853,10 +852,7 @@ def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=Tr if memory is not None and preset is not None: raise ValueError("Do not use memory, private or cpu if you are using flavors") data['memory'] = memory - if disk is not None: - data['disk'] = disk.get('capacity') - elif add is not None: - data['disk'] = add + maintenance_window = datetime.datetime.now(utils.UTC()) order = { 'complexType': 'SoftLayer_Container_Product_Order_Virtual_Guest_Upgrade', @@ -867,6 +863,35 @@ def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=Tr 'virtualGuests': [{'id': int(instance_id)}], } + if disk: + disk_number = 0 + vsi_disk = self.get_instance(instance_id) + for item in vsi_disk.get('billingItem').get('children'): + if item.get('categoryCode').__contains__('guest_disk'): + if disk_number < int("".join(filter(str.isdigit, item.get('categoryCode')))): + disk_number = int("".join(filter(str.isdigit, item.get('categoryCode')))) + for disk_guest in disk: + if disk_guest.get('number') > 0: + price_id = self._get_price_id_for_upgrade_option(upgrade_prices, 'disk', + disk_guest.get('capacity'), + public) + disk_number = disk_guest.get('number') + + else: + price_id = self._get_price_id_for_upgrade_option(upgrade_prices, 'disk', + disk_guest.get('capacity'), + public) + disk_number = disk_number + 1 + + category = {'categories': [{ + 'categoryCode': 'guest_disk' + str(disk_number), + 'complexType': "SoftLayer_Product_Item_Category"}], + 'complexType': 'SoftLayer_Product_Item_Price', + 'id': price_id} + + prices.append(category) + order['prices'] = prices + for option, value in data.items(): if not value: continue @@ -879,28 +904,7 @@ def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=Tr raise exceptions.SoftLayerError( "Unable to find %s option with value %s" % (option, value)) - if disk is not None: - category = {'categories': [{ - 'categoryCode': 'guest_disk' + str(disk.get('number')), - 'complexType': "SoftLayer_Product_Item_Category" - }], 'complexType': 'SoftLayer_Product_Item_Price'} - prices.append(category) - prices[0]['id'] = price_id - elif add: - vsi_disk = self.get_instance(instance_id) - disk_number = 0 - for item in vsi_disk.get('billingItem').get('children'): - if item.get('categoryCode').__contains__('guest_disk'): - if disk_number < int("".join(filter(str.isdigit, item.get('categoryCode')))): - disk_number = int("".join(filter(str.isdigit, item.get('categoryCode')))) - category = {'categories': [{ - 'categoryCode': 'guest_disk' + str(disk_number + 1), - 'complexType': "SoftLayer_Product_Item_Category" - }], 'complexType': 'SoftLayer_Product_Item_Price'} - prices.append(category) - prices[0]['id'] = price_id - else: - prices.append({'id': price_id}) + prices.append({'id': price_id}) order['prices'] = prices if preset is not None: @@ -1024,6 +1028,7 @@ def _get_price_id_for_upgrade_option(self, upgrade_prices, option, value, public 'disk': 'guest_disk' } category_code = option_category.get(option) + for price in upgrade_prices: if price.get('categories') is None or price.get('item') is None: continue @@ -1034,7 +1039,7 @@ def _get_price_id_for_upgrade_option(self, upgrade_prices, option, value, public for category in price.get('categories'): if option == 'disk': - if not (category_code == (''.join([i for i in category.get('categoryCode') if not i.isdigit()])) + if (category_code == (''.join([i for i in category.get('categoryCode') if not i.isdigit()])) and str(product.get('capacity')) == str(value)): return price.get('id') @@ -1052,8 +1057,6 @@ def _get_price_id_for_upgrade_option(self, upgrade_prices, option, value, public elif option == 'nic_speed': if 'Public' in product.get('description'): return price.get('id') - elif option == 'disk': - return price.get('id') else: return price.get('id') diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index a90811fe8..138a9f832 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -370,19 +370,19 @@ def test_dns_sync_both(self, confirm_mock): 'getResourceRecords') getResourceRecords.return_value = [] createAargs = ({ - 'type': 'a', - 'host': 'vs-test1', - 'domainId': 12345, # from SoftLayer_Account::getDomains - 'data': '172.16.240.2', - 'ttl': 7200 - },) + 'type': 'a', + 'host': 'vs-test1', + 'domainId': 12345, # from SoftLayer_Account::getDomains + 'data': '172.16.240.2', + 'ttl': 7200 + },) createPTRargs = ({ - 'type': 'ptr', - 'host': '2', - 'domainId': 123456, - 'data': 'vs-test1.test.sftlyr.ws', - 'ttl': 7200 - },) + 'type': 'ptr', + 'host': '2', + 'domainId': 123456, + 'data': 'vs-test1.test.sftlyr.ws', + 'ttl': 7200 + },) result = self.run_command(['vs', 'dns-sync', '100']) @@ -425,12 +425,12 @@ def test_dns_sync_v6(self, confirm_mock): } } createV6args = ({ - 'type': 'aaaa', - 'host': 'vs-test1', - 'domainId': 12345, - 'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004', - 'ttl': 7200 - },) + 'type': 'aaaa', + 'host': 'vs-test1', + 'domainId': 12345, + 'data': '2607:f0d0:1b01:0023:0000:0000:0000:0004', + 'ttl': 7200 + },) guest.return_value = test_guest result = self.run_command(['vs', 'dns-sync', '--aaaa-record', '100']) self.assert_no_fail(result) @@ -574,7 +574,7 @@ def test_upgrade(self, confirm_mock): def test_upgrade_disk(self, confirm_mock): confirm_mock.return_value = True result = self.run_command(['vs', 'upgrade', '100', '--flavor=M1_64X512X100', - '--disk={"number":1,"capacity":10}']) + '--resize-disk=10', '1', '--resize-disk=10', '2']) self.assert_no_fail(result) self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') call = self.calls('SoftLayer_Product_Order', 'placeOrder')[0] @@ -598,7 +598,7 @@ def test_upgrade_with_flavor(self, confirm_mock): @mock.patch('SoftLayer.CLI.formatting.confirm') def test_upgrade_with_add_disk(self, confirm_mock): confirm_mock.return_value = True - result = self.run_command(['vs', 'upgrade', '100', '--add=10']) + result = self.run_command(['vs', 'upgrade', '100', '--add-disk=10', '--add-disk=10']) self.assert_no_fail(result) self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') call = self.calls('SoftLayer_Product_Order', 'placeOrder')[0] From d149472298a5f0c8195ffd598ecb6e25ef3684c8 Mon Sep 17 00:00:00 2001 From: caberos Date: Tue, 4 Aug 2020 09:29:58 -0400 Subject: [PATCH 39/46] fix tox tool --- SoftLayer/CLI/virt/upgrade.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SoftLayer/CLI/virt/upgrade.py b/SoftLayer/CLI/virt/upgrade.py index 4afeb8be6..fdfa37822 100644 --- a/SoftLayer/CLI/virt/upgrade.py +++ b/SoftLayer/CLI/virt/upgrade.py @@ -42,16 +42,15 @@ def cli(env, identifier, cpu, private, memory, network, flavor, add_disk, resize if not (env.skip_confirmations or formatting.confirm("This action will incur charges on your account. Continue?")): raise exceptions.CLIAbort('Aborted') + disk_json = list() if memory: memory = int(memory / 1024) if resize_disk: - disk_json = list() for guest_disk in resize_disk: disks = {'capacity': guest_disk[0], 'number': guest_disk[1]} disk_json.append(disks) elif add_disk: - disk_json = list() for guest_disk in add_disk: disks = {'capacity': guest_disk, 'number': -1} disk_json.append(disks) From d6583f3c5f346a527a087c4e2c9fdb38ae6cc747 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 4 Aug 2020 15:04:50 -0500 Subject: [PATCH 40/46] 5.9.0 changelog entry --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de7ae2c03..92939ef75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Change Log +## [5.9.0] - 2020-08-03 +https://github.com/softlayer/softlayer-python/compare/v5.8.9...v5.9.0 + +- #1280 Notification Management + + slcli user notifications + + slcli user edit-notifications +- #828 Added networking options to slcli hw create-options + + Refactored slcli hw create to use the ordering manager + + Added --network option to slcli hw create for more granular network choices. + + Deprecated --port-speed and --no-public . They still work for now, but will be removed in a future release. +- #1298 Fix Unhandled exception in CLI - vs detail +- #1309 Fix the empty lines in slcli vs create-options +- #1301 Ability to list VirtualHost capable guests + + slcli hardware guests + + slcli vs list will show guests on VirtualHost servers +- #875 added option to reload bare metal servers with LVM enabled +- #874 Added Migrate command +- #1313 Added support for filteredMask +- #1305 Update docs links +- #1302 Fix lots of whitespace slcli vs create-options ## [5.8.9] - 2020-07-06 https://github.com/softlayer/softlayer-python/compare/v5.8.8...v5.8.9 From 2193048843cac26950bb20efb7286b33b7cbda18 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 4 Aug 2020 17:36:16 -0500 Subject: [PATCH 41/46] Support for STDIN on creating and updating tickets. #900 --- SoftLayer/CLI/ticket/create.py | 29 +++++++++++++--- SoftLayer/CLI/ticket/update.py | 29 +++++++++++++--- docs/cli/tickets.rst | 6 ++++ tests/CLI/modules/ticket_tests.py | 55 +++++++++++++++++++++++++++---- 4 files changed, 103 insertions(+), 16 deletions(-) diff --git a/SoftLayer/CLI/ticket/create.py b/SoftLayer/CLI/ticket/create.py index 5ebad8cf4..17667d74f 100644 --- a/SoftLayer/CLI/ticket/create.py +++ b/SoftLayer/CLI/ticket/create.py @@ -12,8 +12,7 @@ @click.command() @click.option('--title', required=True, help="The title of the ticket") @click.option('--subject-id', type=int, required=True, - help="""The subject id to use for the ticket, - issue 'slcli ticket subjects' to get the list""") + help="""The subject id to use for the ticket, run 'slcli ticket subjects' to get the list""") @click.option('--body', help="The ticket body") @click.option('--hardware', 'hardware_identifier', help="The identifier for hardware to attach") @@ -24,11 +23,31 @@ Only settable with Advanced and Premium support. See https://www.ibm.com/cloud/support""") @environment.pass_env def cli(env, title, subject_id, body, hardware_identifier, virtual_identifier, priority): - """Create a support ticket.""" - ticket_mgr = SoftLayer.TicketManager(env.client) + """Create a Infrastructure support ticket. + + Example:: + + Will create the ticket with `Some text`. + + slcli ticket create --body="Some text" --subject-id 1522 --hardware 12345 --title "My New Ticket" + Will create the ticket with text from STDIN + + cat sometfile.txt | slcli ticket create --subject-id 1003 --virtual 111111 --title "Reboot Me" + + Will open the default text editor, and once closed, use that text to create the ticket + + slcli ticket create --subject-id 1482 --title "Vyatta Questions..." + """ + ticket_mgr = SoftLayer.TicketManager(env.client) if body is None: - body = click.edit('\n\n' + ticket.TEMPLATE_MSG) + stdin = click.get_text_stream('stdin') + # Means there is text on the STDIN buffer, read it and add to the ticket + if not stdin.isatty(): + body = stdin.read() + # This is an interactive terminal, open a text editor + else: + body = click.edit('\n\n' + ticket.TEMPLATE_MSG) created_ticket = ticket_mgr.create_ticket( title=title, body=body, diff --git a/SoftLayer/CLI/ticket/update.py b/SoftLayer/CLI/ticket/update.py index 9c10971ad..f04d36f94 100644 --- a/SoftLayer/CLI/ticket/update.py +++ b/SoftLayer/CLI/ticket/update.py @@ -11,16 +11,35 @@ @click.command() @click.argument('identifier') -@click.option('--body', help="The entry that will be appended to the ticket") +@click.option('--body', help="Text to add to the ticket. STDIN or the default text editor will be used otherwise.") @environment.pass_env def cli(env, identifier, body): - """Adds an update to an existing ticket.""" + """Adds an update to an existing ticket. + + Example:: + + Will update the ticket with `Some text`. + + slcli ticket update 123456 --body="Some text" + + Will update the ticket with text from STDIN + + cat sometfile.txt | slcli ticket update 123456 + + Will open the default text editor, and once closed, use that text to update the ticket + + slcli ticket update 123456 + """ mgr = SoftLayer.TicketManager(env.client) ticket_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'ticket') - if body is None: - body = click.edit('\n\n' + ticket.TEMPLATE_MSG) - + stdin = click.get_text_stream('stdin') + # Means there is text on the STDIN buffer, read it and add to the ticket + if not stdin.isatty(): + body = stdin.read() + # This is an interactive terminal, open a text editor + else: + body = click.edit('\n\n' + ticket.TEMPLATE_MSG) mgr.update_ticket(ticket_id=ticket_id, body=body) env.fout("Ticket Updated!") diff --git a/docs/cli/tickets.rst b/docs/cli/tickets.rst index ad5f23428..71ef3192c 100644 --- a/docs/cli/tickets.rst +++ b/docs/cli/tickets.rst @@ -3,6 +3,12 @@ Support Tickets =============== +The SoftLayer ticket API is used to create "classic" or Infrastructure Support cases. +These tickets will still show up in your web portal, but for the more unified case management API, +see the `Case Management API `_ + +.. note:: Windows Git-Bash users might run into issues with `ticket create` and `ticket update` if --body isn't used, as it doesn't report that it is a real TTY to python, so the default editor can not be launched. + .. click:: SoftLayer.CLI.ticket.create:cli :prog: ticket create :show-nested: diff --git a/tests/CLI/modules/ticket_tests.py b/tests/CLI/modules/ticket_tests.py index 4f8db62a8..7b2363eab 100644 --- a/tests/CLI/modules/ticket_tests.py +++ b/tests/CLI/modules/ticket_tests.py @@ -13,6 +13,22 @@ from SoftLayer import testing +class FakeTTY(): + """A fake object to fake STD input""" + def __init__(self, isatty=False, read="Default Output"): + """Sets isatty and read""" + self._isatty = isatty + self._read = read + + def isatty(self): + """returns self.isatty""" + return self._isatty + + def read(self): + """returns self.read""" + return self._read + + class TicketTests(testing.TestCase): def test_list(self): @@ -99,18 +115,33 @@ def test_create_and_attach(self): identifier=100) @mock.patch('click.edit') - def test_create_no_body(self, edit_mock): + @mock.patch('click.get_text_stream') + def test_create_no_body(self, isatty_mock, edit_mock): + fake_tty = FakeTTY(True, "TEST") + isatty_mock.return_value = fake_tty edit_mock.return_value = 'ticket body' - result = self.run_command(['ticket', 'create', '--title=Test', - '--subject-id=1000']) + result = self.run_command(['ticket', 'create', '--title=Test', '--subject-id=1000']) self.assert_no_fail(result) args = ({'subjectId': 1000, 'assignedUserId': 12345, 'title': 'Test'}, 'ticket body') - self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket', - args=args) + self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket', args=args) + + @mock.patch('click.get_text_stream') + def test_create_no_body_stdin(self, isatty_mock): + fake_tty = FakeTTY(False, "TEST TICKET BODY") + isatty_mock.return_value = fake_tty + result = self.run_command(['ticket', 'create', '--title=Test', '--subject-id=1000']) + print(result.output) + self.assert_no_fail(result) + + args = ({'subjectId': 1000, + 'assignedUserId': 12345, + 'title': 'Test'}, 'TEST TICKET BODY') + + self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket', args=args) def test_subjects(self): list_expected_ids = [1001, 1002, 1003, 1004, 1005] @@ -294,12 +325,24 @@ def test_ticket_update(self): self.assert_called_with('SoftLayer_Ticket', 'addUpdate', args=({'entry': 'Testing'},), identifier=100) @mock.patch('click.edit') - def test_ticket_update_no_body(self, edit_mock): + @mock.patch('click.get_text_stream') + def test_ticket_update_no_body(self, isatty_mock, edit_mock): + fake_tty = FakeTTY(True, "TEST TICKET BODY") + isatty_mock.return_value = fake_tty edit_mock.return_value = 'Testing1' result = self.run_command(['ticket', 'update', '100']) self.assert_no_fail(result) self.assert_called_with('SoftLayer_Ticket', 'addUpdate', args=({'entry': 'Testing1'},), identifier=100) + @mock.patch('click.get_text_stream') + def test_ticket_update_no_body_stdin(self, isatty_mock): + fake_tty = FakeTTY(False, "TEST TICKET BODY") + isatty_mock.return_value = fake_tty + result = self.run_command(['ticket', 'update', '100']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Ticket', 'addUpdate', + args=({'entry': 'TEST TICKET BODY'},), identifier=100) + def test_ticket_json(self): result = self.run_command(['--format=json', 'ticket', 'detail', '1']) expected = {'Case_Number': 'CS123456', From 68f46e294edbaf093bdfa0ed003f8c57ee9bd63a Mon Sep 17 00:00:00 2001 From: caberos Date: Tue, 11 Aug 2020 09:39:30 -0400 Subject: [PATCH 42/46] fix tox Christopher commet code review --- SoftLayer/managers/vs.py | 5 +++++ tests/CLI/modules/vs/vs_tests.py | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 672a74701..8f799b1ac 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -883,6 +883,11 @@ def upgrade(self, instance_id, cpus=None, memory=None, nic_speed=None, public=Tr public) disk_number = disk_number + 1 + if price_id is None: + raise exceptions.SoftLayerAPIError(500, + 'Unable to find %s option with value %s' % ( + ('disk', disk_guest.get('capacity')))) + category = {'categories': [{ 'categoryCode': 'guest_disk' + str(disk_number), 'complexType': "SoftLayer_Product_Item_Category"}], diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index 138a9f832..0c8092e7f 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -583,6 +583,14 @@ def test_upgrade_disk(self, confirm_mock): self.assertIn({'id': 100}, order_container['virtualGuests']) self.assertEqual(order_container['virtualGuests'], [{'id': 100}]) + @mock.patch('SoftLayer.CLI.formatting.confirm') + def test_upgrade_disk_error(self, confirm_mock): + confirm_mock.return_value = True + result = self.run_command(['vs', 'upgrade', '100', '--flavor=M1_64X512X100', + '--resize-disk=1000', '1', '--resize-disk=10', '2']) + self.assertEqual(result.exit_code, 1) + self.assertIsInstance(result.exception, SoftLayerAPIError) + @mock.patch('SoftLayer.CLI.formatting.confirm') def test_upgrade_with_flavor(self, confirm_mock): confirm_mock.return_value = True From 2c66f4c368aed25e1bc12e04c9d79abd02da8d5b Mon Sep 17 00:00:00 2001 From: ATGE Date: Thu, 13 Aug 2020 12:45:01 -0400 Subject: [PATCH 43/46] #1318 add Drive number in guest drives details using the device number --- SoftLayer/CLI/virt/detail.py | 10 +++------- SoftLayer/CLI/virt/storage.py | 27 +++++++++++++++++++++------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index c07ef657c..94c0c7994 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -9,7 +9,7 @@ from SoftLayer.CLI import environment from SoftLayer.CLI import formatting from SoftLayer.CLI import helpers -from SoftLayer.CLI.virt.storage import get_local_type +from SoftLayer.CLI.virt.storage import get_local_storage_table from SoftLayer import utils LOGGER = logging.getLogger(__name__) @@ -35,11 +35,7 @@ def cli(env, identifier, passwords=False, price=False): result = utils.NestedDict(result) local_disks = vsi.get_local_disks(vs_id) - table_local_disks = formatting.Table(['Type', 'Name', 'Capacity']) - for disks in local_disks: - if 'diskImage' in disks: - table_local_disks.add_row([get_local_type(disks), disks['mountType'], - str(disks['diskImage']['capacity']) + " " + str(disks['diskImage']['units'])]) + table_local_disks = get_local_storage_table(local_disks) table.add_row(['id', result['id']]) table.add_row(['guid', result['globalIdentifier']]) @@ -173,7 +169,7 @@ def _get_owner_row(result): owner = utils.lookup(result, 'billingItem', 'orderItem', 'order', 'userRecord', 'username') else: owner = formatting.blank() - return(['owner', owner]) + return (['owner', owner]) def _get_vlan_table(result): diff --git a/SoftLayer/CLI/virt/storage.py b/SoftLayer/CLI/virt/storage.py index 802ae32d9..90252207f 100644 --- a/SoftLayer/CLI/virt/storage.py +++ b/SoftLayer/CLI/virt/storage.py @@ -48,11 +48,8 @@ def cli(env, identifier): nas['allowedVirtualGuests'][0]['datacenter']['longName'], nas.get('notes', None)]) - table_local_disks = formatting.Table(['Type', 'Name', 'Capacity'], title="Other storage details") - for disks in local_disks: - if 'diskImage' in disks: - table_local_disks.add_row([get_local_type(disks), disks['mountType'], - str(disks['diskImage']['capacity']) + " " + str(disks['diskImage']['units'])]) + table_local_disks = get_local_storage_table(local_disks) + table_local_disks.title = "Other storage details" env.fout(table_credentials) env.fout(table_iscsi) @@ -64,10 +61,28 @@ def cli(env, identifier): def get_local_type(disks): """Returns the virtual server local disk type. - :param disks: virtual serve local disks. + :param disks: virtual server local disks. """ disk_type = 'System' if 'SWAP' in disks.get('diskImage', {}).get('description', []): disk_type = 'Swap' return disk_type + + +def get_local_storage_table(local_disks): + """Returns a formatting local disk table + + :param local_disks: virtual server local disks. + """ + table_local_disks = formatting.Table(['Type', 'Name', 'Drive', 'Capacity']) + for disk in local_disks: + if 'diskImage' in disk: + table_local_disks.add_row([ + get_local_type(disk), + disk['mountType'], + disk['device'], + "{capacity} {unit}".format(capacity=disk['diskImage']['capacity'], + unit=disk['diskImage']['units']) + ]) + return table_local_disks From abb1da7e5a4840a822b6b1fb1de649d2522fa9ad Mon Sep 17 00:00:00 2001 From: caberos Date: Thu, 13 Aug 2020 14:49:33 -0400 Subject: [PATCH 44/46] add vs list hardware and all option --- SoftLayer/CLI/virt/list.py | 50 +++++++++++++++++--------------- tests/CLI/modules/vs/vs_tests.py | 4 +++ 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 1a3c4d545..e80417657 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -52,6 +52,8 @@ @click.option('--hourly', is_flag=True, help='Show only hourly instances') @click.option('--monthly', is_flag=True, help='Show only monthly instances') @click.option('--transient', help='Filter by transient instances', type=click.BOOL) +@click.option('--hardware', is_flag=True, default=False, help='Show the all VSI related to hardware') +@click.option('--all', is_flag=True, default=False, help='Show the all VSI and hardware VSIs') @helpers.multi_option('--tag', help='Filter by tags') @click.option('--sortby', help='Column to sort by', @@ -69,7 +71,7 @@ show_default=True) @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, - hourly, monthly, tag, columns, limit, transient): + hourly, monthly, tag, columns, limit, transient, all, hardware): """List virtual servers.""" vsi = SoftLayer.VSManager(env.client) @@ -88,27 +90,29 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, table = formatting.Table(columns.columns) table.sortby = sortby - for guest in guests: - table.add_row([value or formatting.blank() - for value in columns.row(guest)]) + if not hardware or all: + for guest in guests: + table.add_row([value or formatting.blank() + for value in columns.row(guest)]) - env.fout(table) + env.fout(table) - hardware_guests = vsi.get_hardware_guests() - for hardware in hardware_guests: - if hardware['virtualHost']['guests']: - title = "Hardware(id = {hardwareId}) guests associated".format(hardwareId=hardware['id']) - table_hardware_guest = formatting.Table(['id', 'hostname', 'CPU', 'Memory', 'Start Date', 'Status', - 'powerState'], title=title) - table_hardware_guest.sortby = 'hostname' - for guest in hardware['virtualHost']['guests']: - table_hardware_guest.add_row([ - guest['id'], - guest['hostname'], - '%i %s' % (guest['maxCpu'], guest['maxCpuUnits']), - guest['maxMemory'], - utils.clean_time(guest['createDate']), - guest['status']['keyName'], - guest['powerState']['keyName'] - ]) - env.fout(table_hardware_guest) + if hardware or all: + hardware_guests = vsi.get_hardware_guests() + for hardware in hardware_guests: + if hardware['virtualHost']['guests']: + title = "Hardware(id = {hardwareId}) guests associated".format(hardwareId=hardware['id']) + table_hardware_guest = formatting.Table(['id', 'hostname', 'CPU', 'Memory', 'Start Date', 'Status', + 'powerState'], title=title) + table_hardware_guest.sortby = 'hostname' + for guest in hardware['virtualHost']['guests']: + table_hardware_guest.add_row([ + guest['id'], + guest['hostname'], + '%i %s' % (guest['maxCpu'], guest['maxCpuUnits']), + guest['maxMemory'], + utils.clean_time(guest['createDate']), + guest['status']['keyName'], + guest['powerState']['keyName'] + ]) + env.fout(table_hardware_guest) diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index 06d3147ac..bce887178 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -846,3 +846,7 @@ def test_vs_migrate_exception(self): 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) + + def test_list_vsi(self): + result = self.run_command(['vs', 'list', '--hardware']) + self.assert_no_fail(result) From 487ae59238faf4addb1c031fd28af1ba890af355 Mon Sep 17 00:00:00 2001 From: caberos Date: Thu, 13 Aug 2020 17:22:53 -0400 Subject: [PATCH 45/46] fix tox tool --- SoftLayer/CLI/virt/list.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index e80417657..ffb9fd4a0 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -53,7 +53,7 @@ @click.option('--monthly', is_flag=True, help='Show only monthly instances') @click.option('--transient', help='Filter by transient instances', type=click.BOOL) @click.option('--hardware', is_flag=True, default=False, help='Show the all VSI related to hardware') -@click.option('--all', is_flag=True, default=False, help='Show the all VSI and hardware VSIs') +@click.option('--all-guests', is_flag=True, default=False, help='Show the all VSI and hardware VSIs') @helpers.multi_option('--tag', help='Filter by tags') @click.option('--sortby', help='Column to sort by', @@ -71,7 +71,7 @@ show_default=True) @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, - hourly, monthly, tag, columns, limit, transient, all, hardware): + hourly, monthly, tag, columns, limit, transient, hardware, all_guests): """List virtual servers.""" vsi = SoftLayer.VSManager(env.client) @@ -90,22 +90,22 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, table = formatting.Table(columns.columns) table.sortby = sortby - if not hardware or all: + if not hardware or all_guests: for guest in guests: table.add_row([value or formatting.blank() for value in columns.row(guest)]) env.fout(table) - if hardware or all: + if hardware or all_guests: hardware_guests = vsi.get_hardware_guests() - for hardware in hardware_guests: - if hardware['virtualHost']['guests']: - title = "Hardware(id = {hardwareId}) guests associated".format(hardwareId=hardware['id']) + for hd_guest in hardware_guests: + if hd_guest['virtualHost']['guests']: + title = "Hardware(id = {hardwareId}) guests associated".format(hardwareId=hd_guest['id']) table_hardware_guest = formatting.Table(['id', 'hostname', 'CPU', 'Memory', 'Start Date', 'Status', 'powerState'], title=title) table_hardware_guest.sortby = 'hostname' - for guest in hardware['virtualHost']['guests']: + for guest in hd_guest['virtualHost']['guests']: table_hardware_guest.add_row([ guest['id'], guest['hostname'], From 7a3d4d175e68e30e57be68b6df4046a26fea9565 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 18 Aug 2020 14:44:54 -0500 Subject: [PATCH 46/46] v5.9.0 changelog --- CHANGELOG.md | 3 +++ SoftLayer/consts.py | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92939ef75..6cb588bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ https://github.com/softlayer/softlayer-python/compare/v5.8.9...v5.9.0 - #1313 Added support for filteredMask - #1305 Update docs links - #1302 Fix lots of whitespace slcli vs create-options +- #900 Support for STDIN on creating and updating tickets. +- #1318 add Drive number in guest drives details using the device number +- #1323 add vs list hardware and all option ## [5.8.9] - 2020-07-06 https://github.com/softlayer/softlayer-python/compare/v5.8.8...v5.8.9 diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index cdcdf07ed..8dd619aa4 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v5.8.9' +VERSION = 'v5.9.0' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' diff --git a/setup.py b/setup.py index 8b8308901..33df75d76 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='SoftLayer', - version='5.8.9', + version='5.9.0', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='SoftLayer Technologies, Inc.',