diff --git a/SoftLayer/CLI/virt/create_options.py b/SoftLayer/CLI/virt/create_options.py index 4e73d55be..a3ee24314 100644 --- a/SoftLayer/CLI/virt/create_options.py +++ b/SoftLayer/CLI/virt/create_options.py @@ -9,16 +9,20 @@ @click.command(short_help="Get options to use for creating virtual servers.") +@click.argument('location', required=False) @click.option('--vsi-type', required=False, show_default=True, default='PUBLIC_CLOUD_SERVER', - type=click.Choice(['TRANSIENT_CLOUD_SERVER', 'SUSPEND_CLOUD_SERVER', 'PUBLIC_CLOUD_SERVER']), - help="Display options for a specific virtual server packages, for default is PUBLIC_CLOUD_SERVER, " - "choose between TRANSIENT_CLOUD_SERVER, SUSPEND_CLOUD_SERVER, PUBLIC_CLOUD_SERVER") + type=click.Choice(['PUBLIC_CLOUD_SERVER', 'TRANSIENT_CLOUD_SERVER', 'SUSPEND_CLOUD_SERVER', + 'CLOUD_SERVER']), + help="VS keyName type.") +@click.option('--prices', '-p', is_flag=True, + help='Use --prices to list the server item prices, and to list the Item Prices by location,' + 'add it to the --prices option using location short name, e.g. --prices dal13') @environment.pass_env -def cli(env, vsi_type): +def cli(env, vsi_type, prices, location=None): """Virtual server order options.""" vsi = SoftLayer.VSManager(env.client) - options = vsi.get_create_options(vsi_type) + options = vsi.get_create_options(vsi_type, location) tables = [] @@ -26,72 +30,246 @@ def cli(env, vsi_type): 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']]) + for location_info in options['locations']: + dc_table.add_row([location_info['name'], location_info['key']]) tables.append(dc_table) - # Operation system - os_table = formatting.Table(['OS', 'Key', 'Reference Code'], title="Operating Systems") - os_table.sortby = 'Key' - os_table.align = 'l' + if vsi_type == 'CLOUD_SERVER': + tables.append(guest_core_prices_table(options['guest_core'], prices)) + tables.append(ram_prices_table(options['ram'], prices)) + else: + tables.append(preset_prices_table(options['sizes'], prices)) + tables.append(os_prices_table(options['operating_systems'], prices)) + tables.append(port_speed_prices_table(options['port_speed'], prices)) + tables.append(database_prices_table(options['database'], prices)) + tables.append(guest_disk_prices_table(options['guest_disk'], prices)) + tables.append(extras_prices_table(options['extras'], prices)) + + env.fout(tables) - for operating_system in options['operating_systems']: - os_table.add_row([operating_system['name'], operating_system['key'], operating_system['referenceCode']]) - tables.append(os_table) - # Sizes +def preset_prices_table(sizes, prices=False): + """Shows Server Preset options prices. + + :param [] sizes: List of Hardware Server sizes. + :param prices: Include pricing information or not. + """ + preset_price_table = formatting.Table(['Size', 'Value', 'Hourly', 'Monthly'], title="Sizes Prices") + preset_price_table.sortby = 'Value' + preset_price_table.align = 'l' + preset_table = formatting.Table(['Size', 'Value'], title="Sizes") preset_table.sortby = 'Value' preset_table.align = 'l' - for size in options['sizes']: + for size in sizes: + if (size['hourlyRecurringFee'] > 0) or (size['recurringFee'] > 0): + preset_price_table.add_row([size['name'], size['key'], "%.4f" % size['hourlyRecurringFee'], + "%.4f" % size['recurringFee']]) preset_table.add_row([size['name'], size['key']]) - tables.append(preset_table) + if prices: + return preset_price_table + return preset_table + + +def os_prices_table(operating_systems, prices=False): + """Shows Server Operating Systems prices cost and capacity restriction. + + :param [] operating_systems: List of Hardware Server operating systems. + :param prices: Include pricing information or not. + """ + os_price_table = formatting.Table(['OS Key', 'Hourly', 'Monthly', 'Restriction'], + title="Operating Systems Prices") + os_price_table.sortby = 'OS Key' + os_price_table.align = 'l' + + os_table = formatting.Table(['OS', 'Key', 'Reference Code'], title="Operating Systems") + os_table.sortby = 'Key' + os_table.align = 'l' + + for operating_system in operating_systems: + for price in operating_system['prices']: + cr_max = _get_price_data(price, 'capacityRestrictionMaximum') + cr_min = _get_price_data(price, 'capacityRestrictionMinimum') + cr_type = _get_price_data(price, 'capacityRestrictionType') + os_price_table.add_row( + [operating_system['key'], + _get_price_data(price, 'hourlyRecurringFee'), + _get_price_data(price, 'recurringFee'), + "%s - %s %s" % (cr_min, cr_max, cr_type)]) + os_table.add_row([operating_system['name'], operating_system['key'], operating_system['referenceCode']]) + if prices: + return os_price_table + return os_table + + +def port_speed_prices_table(port_speeds, prices=False): + """Shows Server Port Speeds prices cost and capacity restriction. + + :param [] port_speeds: List of Hardware Server Port Speeds. + :param prices: Include pricing information or not. + """ + port_speed_price_table = formatting.Table(['Key', 'Speed', 'Hourly', 'Monthly'], title="Network Options Prices") + port_speed_price_table.sortby = 'Speed' + port_speed_price_table.align = 'l' + + port_speed_table = formatting.Table(['network', 'Key'], title="Network Options") + port_speed_table.sortby = 'Key' + port_speed_table.align = 'l' + + for speed in port_speeds: + for price in speed['prices']: + port_speed_price_table.add_row( + [speed['key'], speed['speed'], + _get_price_data(price, 'hourlyRecurringFee'), + _get_price_data(price, 'recurringFee')]) + port_speed_table.add_row([speed['name'], speed['key']]) + if prices: + return port_speed_price_table + return port_speed_table + + +def extras_prices_table(extras, prices=False): + """Shows Server extras prices cost and capacity restriction. + + :param [] extras: List of Hardware Server Extras. + :param prices: Include pricing information or not. + """ + extras_price_table = formatting.Table(['Extra Option Key', 'Hourly', 'Monthly'], title="Extras Prices") + extras_price_table.align = 'l' + + extras_table = formatting.Table(['Extra Option', 'Value'], title="Extras") + extras_table.sortby = 'Value' + extras_table.align = 'l' + + for extra in extras: + for price in extra['prices']: + extras_price_table.add_row( + [extra['key'], + _get_price_data(price, 'hourlyRecurringFee'), + _get_price_data(price, 'recurringFee')]) + extras_table.add_row([extra['name'], extra['key']]) + if prices: + return extras_price_table + return extras_table + + +def ram_prices_table(ram_list, prices=False): + """Shows Server Port Speeds prices cost and capacity restriction. + + :param [] ram_list: List of Virtual Server Ram. + :param prices: Include pricing information or not. + """ + ram_price_table = formatting.Table(['Key', 'Hourly', 'Monthly'], title="Ram Prices") + ram_price_table.sortby = 'Key' + ram_price_table.align = 'l' - # RAM ram_table = formatting.Table(['memory', 'Value'], title="RAM") ram_table.sortby = 'Value' ram_table.align = 'l' - for ram in options['ram']: + for ram in ram_list: + for price in ram['prices']: + ram_price_table.add_row( + [ram['key'], + _get_price_data(price, 'hourlyRecurringFee'), + _get_price_data(price, 'recurringFee')]) ram_table.add_row([ram['name'], ram['key']]) - tables.append(ram_table) + if prices: + return ram_price_table + return ram_table + + +def database_prices_table(database_list, prices=False): + """Shows Server Port Speeds prices cost and capacity restriction. + + :param [] database_list: List of Virtual Server database. + :param prices: Include pricing information or not. + """ + database_price_table = formatting.Table(['Key', 'Hourly', 'Monthly', 'Restriction'], title="Data Base Prices") + database_price_table.sortby = 'Key' + database_price_table.align = 'l' - # Data base database_table = formatting.Table(['database', 'Value'], title="Databases") database_table.sortby = 'Value' database_table.align = 'l' - for database in options['database']: + for database in database_list: + for price in database['prices']: + cr_max = _get_price_data(price, 'capacityRestrictionMaximum') + cr_min = _get_price_data(price, 'capacityRestrictionMinimum') + cr_type = _get_price_data(price, 'capacityRestrictionType') + database_price_table.add_row( + [database['key'], + _get_price_data(price, 'hourlyRecurringFee'), + _get_price_data(price, 'recurringFee'), + "%s - %s %s" % (cr_min, cr_max, cr_type)]) database_table.add_row([database['name'], database['key']]) - tables.append(database_table) + if prices: + return database_price_table + return database_table + + +def guest_core_prices_table(guest_core_list, prices=False): + """Shows Server Port Speeds prices cost and capacity restriction. + + :param [] guest_core_list: List of Virtual Server guest_core. + :param prices: Include pricing information or not. + """ + guest_core_price_table = formatting.Table(['Key', 'Hourly', 'Monthly'], title="Guest Core Prices") + guest_core_price_table.sortby = 'Key' + guest_core_price_table.align = 'l' - # Guest_core guest_core_table = formatting.Table(['cpu', 'Value', 'Capacity'], title="Guest_core") guest_core_table.sortby = 'Value' guest_core_table.align = 'l' - for guest_core in options['guest_core']: + for guest_core in guest_core_list: + for price in guest_core['prices']: + guest_core_price_table.add_row( + [guest_core['key'], + _get_price_data(price, 'hourlyRecurringFee'), + _get_price_data(price, 'recurringFee')]) guest_core_table.add_row([guest_core['name'], guest_core['key'], guest_core['capacity']]) - tables.append(guest_core_table) + if prices: + return guest_core_price_table + return guest_core_table + + +def guest_disk_prices_table(guest_disk_list, prices=False): + """Shows Server Port Speeds prices cost and capacity restriction. + + :param [] guest_disk_list: List of Virtual Server guest_disk. + :param prices: Include pricing information or not. + """ + guest_disk_price_table = formatting.Table(['Key', 'Hourly', 'Monthly'], title="Guest Disk Prices") + guest_disk_price_table.sortby = 'Key' + guest_disk_price_table.align = 'l' - # Guest_core guest_disk_table = formatting.Table(['guest_disk', 'Value', 'Capacity', 'Disk'], title="Guest_disks") guest_disk_table.sortby = 'Value' guest_disk_table.align = 'l' - for guest_disk in options['guest_disk']: - guest_disk_table.add_row([guest_disk['name'], guest_disk['key'], guest_disk['capacity'], guest_disk['disk']]) - tables.append(guest_disk_table) + for guest_disk in guest_disk_list: + for price in guest_disk['prices']: + guest_disk_price_table.add_row( + [guest_disk['key'], + _get_price_data(price, 'hourlyRecurringFee'), + _get_price_data(price, 'recurringFee')]) + guest_disk_table.add_row( + [guest_disk['name'], guest_disk['key'], guest_disk['capacity'], guest_disk['disk']]) + if prices: + return guest_disk_price_table + return guest_disk_table - # Port speed - port_speed_table = formatting.Table(['network', 'Key'], title="Network Options") - port_speed_table.sortby = 'Key' - port_speed_table.align = 'l' - for speed in options['port_speed']: - port_speed_table.add_row([speed['name'], speed['key']]) - tables.append(port_speed_table) +def _get_price_data(price, item): + """Get a specific data from HS price. - env.fout(formatting.listing(tables, separator='\n')) + :param price: Hardware Server price. + :param string item: Hardware Server price data. + """ + result = '-' + if item in price: + result = price[item] + return result diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 1ddbdc3e1..1a03db1c6 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -13,11 +13,20 @@ from SoftLayer.decoration import retry from SoftLayer import exceptions +from SoftLayer.exceptions import SoftLayerError +from SoftLayer.managers.hardware import _get_preset_cost +from SoftLayer.managers.hardware import get_item_price from SoftLayer.managers import ordering from SoftLayer import utils LOGGER = logging.getLogger(__name__) +EXTRA_CATEGORIES = ['pri_ipv6_addresses', + 'static_ipv6_addresses', + 'sec_ip_addresses', + 'trusted_platform_module', + 'software_guard_extensions'] + # pylint: disable=no-self-use,too-many-lines @@ -249,9 +258,11 @@ def get_instance(self, instance_id, **kwargs): return self.guest.getObject(id=instance_id, **kwargs) @retry(logger=LOGGER) - def get_create_options(self, vsi_type="PUBLIC_CLOUD_SERVER"): + def get_create_options(self, vsi_type="PUBLIC_CLOUD_SERVER", datacenter=None): """Retrieves the available options for creating a VS. + :param string vsi_type: vs keyName. + :param string datacenter: short name, like dal09 :returns: A dictionary of creation options. Example:: @@ -265,26 +276,45 @@ def get_create_options(self, vsi_type="PUBLIC_CLOUD_SERVER"): # SUSPEND_CLOUD_SERVER package = self._get_package(vsi_type) + location_group_id = None + if datacenter: + _filter = {"name": {"operation": datacenter}} + _mask = "mask[priceGroups]" + dc_details = self.client.call('SoftLayer_Location', 'getDatacenters', mask=_mask, filter=_filter, limit=1) + if not dc_details: + raise SoftLayerError("Unable to find a datacenter named {}".format(datacenter)) + # A DC will have several price groups, no good way to deal with this other than checking each. + # An item should only belong to one type of price group. + for group in dc_details[0].get('priceGroups', []): + # We only care about SOME of the priceGroups, which all SHOULD start with `Location Group X` + # Hopefully this never changes.... + if group.get('description').startswith('Location'): + location_group_id = group.get('id') + # Locations locations = [] for region in package['regions']: - locations.append({ - 'name': region['location']['location']['longName'], - 'key': region['location']['location']['name'], - }) + if datacenter is None or datacenter == region['location']['location']['name']: + locations.append({ + 'name': region['location']['location']['longName'], + 'key': region['location']['location']['name'], + }) operating_systems = [] database = [] port_speeds = [] guest_core = [] local_disk = [] + extras = [] ram = [] sizes = [] for preset in package['activePresets'] + package['accountRestrictedActivePresets']: sizes.append({ 'name': preset['description'], - 'key': preset['keyName'] + 'key': preset['keyName'], + 'hourlyRecurringFee': _get_preset_cost(preset, package['items'], 'hourly', location_group_id), + 'recurringFee': _get_preset_cost(preset, package['items'], 'monthly', location_group_id) }) for item in package['items']: @@ -294,33 +324,39 @@ def get_create_options(self, vsi_type="PUBLIC_CLOUD_SERVER"): operating_systems.append({ 'name': item['softwareDescription']['longDescription'], 'key': item['keyName'], - 'referenceCode': item['softwareDescription']['referenceCode'] + 'referenceCode': item['softwareDescription']['referenceCode'], + 'prices': get_item_price(item['prices'], location_group_id) }) # database elif category == 'database': database.append({ 'name': item['description'], - 'key': item['keyName'] + 'key': item['keyName'], + 'prices': get_item_price(item['prices'], location_group_id) }) elif category == 'port_speed': port_speeds.append({ 'name': item['description'], - 'key': item['keyName'] + 'speed': item['capacity'], + 'key': item['keyName'], + 'prices': get_item_price(item['prices'], location_group_id) }) elif category == 'guest_core': guest_core.append({ 'name': item['description'], 'capacity': item['capacity'], - 'key': item['keyName'] + 'key': item['keyName'], + 'prices': get_item_price(item['prices'], location_group_id) }) elif category == 'ram': ram.append({ 'name': item['description'], 'capacity': item['capacity'], - 'key': item['keyName'] + 'key': item['keyName'], + 'prices': get_item_price(item['prices'], location_group_id) }) elif category.__contains__('guest_disk'): @@ -328,9 +364,16 @@ def get_create_options(self, vsi_type="PUBLIC_CLOUD_SERVER"): 'name': item['description'], 'capacity': item['capacity'], 'key': item['keyName'], - 'disk': category + 'disk': category, + 'prices': get_item_price(item['prices'], location_group_id) }) # Extras + elif category in EXTRA_CATEGORIES: + extras.append({ + 'name': item['description'], + 'key': item['keyName'], + 'prices': get_item_price(item['prices'], location_group_id) + }) return { 'locations': locations, @@ -340,22 +383,34 @@ def get_create_options(self, vsi_type="PUBLIC_CLOUD_SERVER"): 'guest_core': guest_core, 'port_speed': port_speeds, 'guest_disk': local_disk, - 'sizes': sizes + 'sizes': sizes, + 'extras': extras, } @retry(logger=LOGGER) def _get_package(self, package_keyname): - """Get the package related to simple hardware ordering.""" - mask = ''' - activePresets, accountRestrictedActivePresets, - items[description, keyName, capacity, - attributes[id, attributeTypeKeyName], - itemCategory[id, categoryCode], - softwareDescription[id, referenceCode, longDescription], prices], - regions[location[location[priceGroups]]]''' - - package_id = self.ordering_manager.get_package_by_key(package_keyname, mask="mask[id]")['id'] - package = self.client.call('Product_Package', 'getObject', id=package_id, mask=mask) + """Get the package related to simple vs ordering. + + :param string package_keyname: Virtual Server package keyName. + """ + items_mask = 'mask[id,keyName,capacity,description,attributes[id,attributeTypeKeyName],' \ + 'itemCategory[id,categoryCode],softwareDescription[id,referenceCode,longDescription],' \ + 'prices[categories]]' + # The preset prices list will only have default prices. The prices->item->prices will have location specific + presets_mask = 'mask[prices]' + region_mask = 'location[location[priceGroups]]' + package = {'items': [], 'activePresets': [], 'accountRestrictedActivePresets': [], 'regions': []} + package_info = self.ordering_manager.get_package_by_key(package_keyname, mask="mask[id]") + + package['items'] = self.client.call('SoftLayer_Product_Package', 'getItems', + id=package_info.get('id'), mask=items_mask) + package['activePresets'] = self.client.call('SoftLayer_Product_Package', 'getActivePresets', + id=package_info.get('id'), mask=presets_mask) + package['accountRestrictedActivePresets'] = self.client.call('SoftLayer_Product_Package', + 'getAccountRestrictedActivePresets', + id=package_info.get('id'), mask=presets_mask) + package['regions'] = self.client.call('SoftLayer_Product_Package', 'getRegions', + id=package_info.get('id'), mask=region_mask) return package def cancel_instance(self, instance_id): diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py index a680e3c72..7fb23b97b 100644 --- a/tests/CLI/modules/vs/vs_tests.py +++ b/tests/CLI/modules/vs/vs_tests.py @@ -317,6 +317,14 @@ def test_create_options(self): result = self.run_command(['vs', 'create-options', '--vsi-type', 'TRANSIENT_CLOUD_SERVER']) self.assert_no_fail(result) + def test_create_options_prices(self): + result = self.run_command(['vs', 'create-options', '--prices', '--vsi-type', 'TRANSIENT_CLOUD_SERVER']) + self.assert_no_fail(result) + + def test_create_options_prices_location(self): + result = self.run_command(['vs', 'create-options', '--prices', 'dal13', '--vsi-type', 'TRANSIENT_CLOUD_SERVER']) + self.assert_no_fail(result) + @mock.patch('SoftLayer.CLI.formatting.confirm') def test_dns_sync_both(self, confirm_mock): confirm_mock.return_value = True diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index e5b532647..062cafc30 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -247,9 +247,6 @@ def test_get_create_options_prices_by_location(self): 'recurringFee': 0.0 } - print("---------") - print(options) - self.assertEqual(options['extras'][0]['prices'][0]['hourlyRecurringFee'], extras['prices'][0]['hourlyRecurringFee']) self.assertEqual(options['locations'][0], locations) diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py index cf701d21f..e858b2577 100644 --- a/tests/managers/vs/vs_tests.py +++ b/tests/managers/vs/vs_tests.py @@ -116,9 +116,88 @@ def test_get_instance(self): identifier=100) def test_get_create_options(self): - self.vs.get_create_options() - self.assert_called_with('SoftLayer_Product_Package', 'getObject', - identifier=200) + options = self.vs.get_create_options() + + extras = {'key': '1_IPV6_ADDRESS', 'name': '1 IPv6 Address'} + locations = {'key': 'wdc07', 'name': 'Washington 7'} + 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': 'M1_64X512X25', + 'name': 'M1.64x512x25', + 'hourlyRecurringFee': 0.0, + 'recurringFee': 0.0 + } + + self.assertEqual(options['extras'][0]['key'], extras['key']) + self.assertEqual(options['locations'][0], locations) + self.assertEqual(options['operating_systems'][0]['referenceCode'], + operating_systems['referenceCode']) + self.assertEqual(options['port_speed'][0]['name'], port_speeds['name']) + self.assertEqual(options['sizes'][0], sizes) + + def test_get_create_options_prices_by_location(self): + options = self.vs.get_create_options('wdc07') + + extras = {'key': '1_IPV6_ADDRESS', 'name': '1 IPv6 Address', + 'prices': [ + { + 'hourlyRecurringFee': '0', + 'id': 272, + 'locationGroupId': '', + 'recurringFee': '0', + } + ] + } + locations = {'key': 'wdc07', 'name': 'Washington 7'} + operating_systems = { + 'key': 'OS_UBUNTU_14_04_LTS_TRUSTY_TAHR_64_BIT', + 'name': 'Ubuntu / 14.04-64', + 'referenceCode': 'UBUNTU_14_64', + 'prices': [ + { + 'hourlyRecurringFee': '0', + 'id': 272, + 'locationGroupId': '', + 'recurringFee': '0', + } + ] + } + + port_speeds = { + 'key': '10', + 'name': '10 Mbps Public & Private Network Uplinks', + 'prices': [ + { + 'hourlyRecurringFee': '0', + 'id': 272, + 'locationGroupId': '', + 'recurringFee': '0', + } + ] + } + sizes = { + 'key': 'M1_64X512X25', + 'name': 'M1.64x512x25', + 'hourlyRecurringFee': 0.0, + 'recurringFee': 0.0 + } + + self.assertEqual(options['extras'][0]['prices'][0]['hourlyRecurringFee'], + extras['prices'][0]['hourlyRecurringFee']) + self.assertEqual(options['locations'][0], locations) + self.assertEqual(options['operating_systems'][0]['prices'][0]['locationGroupId'], + operating_systems['prices'][0]['locationGroupId']) + self.assertEqual(options['port_speed'][0]['prices'][0]['id'], port_speeds['prices'][0]['id']) + self.assertEqual(options['sizes'][0], sizes) def test_cancel_instance(self): result = self.vs.cancel_instance(1)