diff --git a/SoftLayer/CLI/order/__init__.py b/SoftLayer/CLI/order/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/SoftLayer/CLI/order/category_list.py b/SoftLayer/CLI/order/category_list.py new file mode 100644 index 000000000..91671b8e0 --- /dev/null +++ b/SoftLayer/CLI/order/category_list.py @@ -0,0 +1,54 @@ +"""List package categories.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering + +COLUMNS = ['name', 'categoryCode', 'isRequired'] + + +@click.command() +@click.argument('package_keyname') +@click.option('--required', + is_flag=True, + help="List only the required categories for the package") +@environment.pass_env +def cli(env, package_keyname, required): + """List the categories of a package. + + Package keynames can be retrieved from `slcli order package-list` + + \b + Example: + # List the categories of Bare Metal servers + slcli order category-list BARE_METAL_SERVER + + When using the --required flag, it will list out only the categories + that are required for ordering that package (see `slcli order item-list`) + + \b + Example: + # List the required categories for Bare Metal servers + slcli order category-list BARE_METAL_SERVER --required + + """ + client = env.client + manager = ordering.OrderingManager(client) + table = formatting.Table(COLUMNS) + + categories = manager.list_categories(package_keyname) + + if required: + categories = [cat for cat in categories if cat['isRequired']] + + for cat in categories: + table.add_row([ + cat['itemCategory']['name'], + cat['itemCategory']['categoryCode'], + 'Y' if cat['isRequired'] else 'N' + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/order/item_list.py b/SoftLayer/CLI/order/item_list.py new file mode 100644 index 000000000..61841d267 --- /dev/null +++ b/SoftLayer/CLI/order/item_list.py @@ -0,0 +1,60 @@ +"""List package items.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering + +COLUMNS = ['keyName', + 'description', ] + + +@click.command() +@click.argument('package_keyname') +@click.option('--keyword', + help="A word (or string) used to filter item names.") +@click.option('--category', + help="Category code to filter items by") +@environment.pass_env +def cli(env, package_keyname, keyword, category): + """List package items used for ordering. + + The items listed can be used with `slcli order place` to specify + the items that are being ordered in the package. + + Package keynames can be retrieved using `slcli order package-list` + + \b + Example: + # List all items in the VSI package + slcli order item-list CLOUD_SERVER + + The --keyword option is used to filter items by name. + The --category option is used to filter items by category. + Both --keyword and --category can be used together. + + \b + Example: + # List Ubuntu OSes from the os category of the Bare Metal package + slcli order item-list BARE_METAL_SERVER --category os --keyword ubuntu + + """ + table = formatting.Table(COLUMNS) + manager = ordering.OrderingManager(env.client) + + _filter = {'items': {}} + if keyword: + _filter['items']['description'] = {'operation': '*= %s' % keyword} + if category: + _filter['items']['categories'] = {'categoryCode': {'operation': '_= %s' % category}} + + items = manager.list_items(package_keyname, filter=_filter) + + for item in items: + table.add_row([ + item['keyName'], + item['description'], + ]) + env.fout(table) diff --git a/SoftLayer/CLI/order/package_list.py b/SoftLayer/CLI/order/package_list.py new file mode 100644 index 000000000..9a6b97e6c --- /dev/null +++ b/SoftLayer/CLI/order/package_list.py @@ -0,0 +1,50 @@ +"""List packages.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering + +COLUMNS = ['name', + 'keyName', ] + + +@click.command() +@click.option('--keyword', + help="A word (or string) used to filter package names.") +@environment.pass_env +def cli(env, keyword): + """List packages that can be ordered via the placeOrder API. + + \b + Example: + # List out all packages for ordering + slcli order package-list + + + Keywords can also be used for some simple filtering functionality + to help find a package easier. + + \b + Example: + # List out all packages with "server" in the name + slcli order package-list --keyword server + + """ + manager = ordering.OrderingManager(env.client) + table = formatting.Table(COLUMNS) + + _filter = {} + if keyword: + _filter = {'name': {'operation': '*= %s' % keyword}} + + packages = manager.list_packages(filter=_filter) + + for package in packages: + table.add_row([ + package['name'], + package['keyName'], + ]) + env.fout(table) diff --git a/SoftLayer/CLI/order/place.py b/SoftLayer/CLI/order/place.py new file mode 100644 index 000000000..f042e2517 --- /dev/null +++ b/SoftLayer/CLI/order/place.py @@ -0,0 +1,112 @@ +"""Verify or place an order.""" +# :license: MIT, see LICENSE for more details. + +import json + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering + +COLUMNS = ['keyName', + 'description', + 'cost', ] + + +@click.command() +@click.argument('package_keyname') +@click.argument('location') +@click.option('--preset', + help="The order preset (if required by the package)") +@click.option('--verify', + is_flag=True, + help="Flag denoting whether or not to only verify the order, not place it") +@click.option('--billing', + type=click.Choice(['hourly', 'monthly']), + default='hourly', + show_default=True, + help="Billing rate") +@click.option('--complex-type', help=("The complex type of the order. This typically begins" + " with 'SoftLayer_Container_Product_Order_'.")) +@click.option('--extras', + help="JSON string denoting extra data that needs to be sent with the order") +@click.argument('order_items', nargs=-1) +@environment.pass_env +def cli(env, package_keyname, location, preset, verify, billing, complex_type, + extras, order_items): + """Place or verify an order. + + This CLI command is used for placing/verifying an order of the specified package in + the given location (denoted by a datacenter's long name). Orders made via the CLI + can then be converted to be made programmatically by calling + SoftLayer.OrderingManager.place_order() with the same keynames. + + Packages for ordering can be retrived from `slcli order package-list` + Presets for ordering can be retrieved from `slcli order preset-list` (not all packages + have presets) + + Items can be retrieved from `slcli order item-list`. In order to find required + items for the order, use `slcli order category-list`, and then provide the + --category option for each category code in `slcli order item-list`. + + \b + Example: + # Order an hourly VSI with 4 CPU, 16 GB RAM, 100 GB SAN disk, + # Ubuntu 16.04, and 1 Gbps public & private uplink in dal13 + slcli order place --billing hourly CLOUD_SERVER DALLAS13 \\ + GUEST_CORES_4 \\ + RAM_16_GB \\ + REBOOT_REMOTE_CONSOLE \\ + 1_GBPS_PUBLIC_PRIVATE_NETWORK_UPLINKS \\ + BANDWIDTH_0_GB_2 \\ + 1_IP_ADDRESS \\ + GUEST_DISK_100_GB_SAN \\ + OS_UBUNTU_16_04_LTS_XENIAL_XERUS_MINIMAL_64_BIT_FOR_VSI \\ + MONITORING_HOST_PING \\ + NOTIFICATION_EMAIL_AND_TICKET \\ + AUTOMATED_NOTIFICATION \\ + UNLIMITED_SSL_VPN_USERS_1_PPTP_VPN_USER_PER_ACCOUNT \\ + NESSUS_VULNERABILITY_ASSESSMENT_REPORTING \\ + --extras '{"virtualGuests": [{"hostname": "test", "domain": "softlayer.com"}]}' \\ + --complex-type SoftLayer_Container_Product_Order_Virtual_Guest + + """ + manager = ordering.OrderingManager(env.client) + + if extras: + extras = json.loads(extras) + + args = (package_keyname, location, order_items) + kwargs = {'preset_keyname': preset, + 'extras': extras, + 'quantity': 1, + 'complex_type': complex_type, + 'hourly': True if billing == 'hourly' else False} + + if verify: + table = formatting.Table(COLUMNS) + order_to_place = manager.verify_order(*args, **kwargs) + for price in order_to_place['prices']: + cost_key = 'hourlyRecurringFee' if billing == 'hourly' else 'recurringFee' + table.add_row([ + price['item']['keyName'], + price['item']['description'], + price[cost_key] if cost_key in price else formatting.blank() + ]) + + else: + if not (env.skip_confirmations or formatting.confirm( + "This action will incur charges on your account. Continue?")): + raise exceptions.CLIAbort("Aborting order.") + + order = manager.place_order(*args, **kwargs) + + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['id', order['orderId']]) + table.add_row(['created', order['orderDate']]) + table.add_row(['status', order['placedOrder']['status']]) + env.fout(table) diff --git a/SoftLayer/CLI/order/preset_list.py b/SoftLayer/CLI/order/preset_list.py new file mode 100644 index 000000000..a619caf77 --- /dev/null +++ b/SoftLayer/CLI/order/preset_list.py @@ -0,0 +1,54 @@ +"""List package presets.""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering + +COLUMNS = ['name', + 'keyName', + 'description', ] + + +@click.command() +@click.argument('package_keyname') +@click.option('--keyword', + help="A word (or string) used to filter preset names.") +@environment.pass_env +def cli(env, package_keyname, keyword): + """List package presets. + + Package keynames can be retrieved from `slcli order package-list`. + Some packages do not have presets. + + \b + Example: + # List the presets for Bare Metal servers + slcli order preset-list BARE_METAL_SERVER + + The --keyword option can also be used for additional filtering on + the returned presets. + + \b + Example: + # List the Bare Metal server presets that include a GPU + slcli order preset-list BARE_METAL_SERVER --keyword gpu + + """ + table = formatting.Table(COLUMNS) + manager = ordering.OrderingManager(env.client) + + _filter = {} + if keyword: + _filter = {'activePresets': {'name': {'operation': '*= %s' % keyword}}} + presets = manager.list_presets(package_keyname, filter=_filter) + + for preset in presets: + table.add_row([ + preset['name'], + preset['keyName'], + preset['description'] + ]) + env.fout(table) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 2ff3379ac..7514bbc4e 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -191,6 +191,13 @@ ('object-storage:endpoints', 'SoftLayer.CLI.object_storage.list_endpoints:cli'), + ('order', 'SoftLayer.CLI.order'), + ('order:category-list', 'SoftLayer.CLI.order.category_list:cli'), + ('order:item-list', 'SoftLayer.CLI.order.item_list:cli'), + ('order:package-list', 'SoftLayer.CLI.order.package_list:cli'), + ('order:place', 'SoftLayer.CLI.order.place:cli'), + ('order:preset-list', 'SoftLayer.CLI.order.preset_list:cli'), + ('rwhois', 'SoftLayer.CLI.rwhois'), ('rwhois:edit', 'SoftLayer.CLI.rwhois.edit:cli'), ('rwhois:show', 'SoftLayer.CLI.rwhois.show:cli'), diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index acaaa3901..7185bc9e9 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -5,6 +5,35 @@ :license: MIT, see LICENSE for more details. """ +# pylint: disable=no-self-use + +from SoftLayer import exceptions + +CATEGORY_MASK = '''id, + isRequired, + itemCategory[ + id, + name, + categoryCode + ] + ''' + +ITEM_MASK = '''id, + keyName, + description + ''' + +PACKAGE_MASK = '''id, + name, + keyName, + isActive + ''' + +PRESET_MASK = '''id, + name, + keyName, + description + ''' class OrderingManager(object): @@ -15,6 +44,9 @@ class OrderingManager(object): def __init__(self, client): self.client = client + self.package_svc = client['Product_Package'] + self.order_svc = client['Product_Order'] + self.billing_svc = client['Billing_Order'] def get_packages_of_type(self, package_types, mask=None): """Get packages that match a certain type. @@ -27,7 +59,6 @@ def get_packages_of_type(self, package_types, mask=None): :param string mask: Mask to specify the properties we want to retrieve """ - package_service = self.client['Product_Package'] _filter = { 'type': { 'keyName': { @@ -40,7 +71,7 @@ def get_packages_of_type(self, package_types, mask=None): }, } - packages = package_service.getAllObjects(mask=mask, filter=_filter) + packages = self.package_svc.getAllObjects(mask=mask, filter=_filter) packages = self.filter_outlet_packages(packages) return packages @@ -185,7 +216,7 @@ def verify_quote(self, quote_id, extra, quantity=1): container = self.generate_order_template(quote_id, extra, quantity=quantity) - return self.client['Product_Order'].verifyOrder(container) + return self.order_svc.verifyOrder(container) def order_quote(self, quote_id, extra, quantity=1): """Places an order using a quote @@ -198,7 +229,7 @@ def order_quote(self, quote_id, extra, quantity=1): container = self.generate_order_template(quote_id, extra, quantity=quantity) - return self.client['Product_Order'].placeOrder(container) + return self.order_svc.placeOrder(container) def get_package_by_key(self, package_keyname, mask=None): """Get a single package with a given key. @@ -209,15 +240,261 @@ def get_package_by_key(self, package_keyname, mask=None): we are interested in. :param string mask: Mask to specify the properties we want to retrieve """ - package_service = self.client['Product_Package'] _filter = { 'keyName': { 'operation': package_keyname, }, } - packages = package_service.getAllObjects(mask=mask, filter=_filter) + packages = self.package_svc.getAllObjects(mask=mask, filter=_filter) if len(packages) == 0: return None else: return packages.pop() + + def list_categories(self, package_keyname, **kwargs): + """List the categories for the given package. + + :param str package_keyname: The package for which to get the categories. + :returns: List of categories associated with the package + """ + get_kwargs = {} + get_kwargs['mask'] = kwargs.get('mask', CATEGORY_MASK) + + if 'filter' in kwargs: + get_kwargs['filter'] = kwargs['filter'] + + package = self.get_package_by_key(package_keyname, mask='id') + if not package: + raise exceptions.SoftLayerError("Package {} does not exist".format(package_keyname)) + + categories = self.package_svc.getConfiguration(id=package['id'], **get_kwargs) + return categories + + def list_items(self, package_keyname, **kwargs): + """List the items for the given package. + + :param str package_keyname: The package for which to get the items. + :returns: List of items in the package + + """ + get_kwargs = {} + get_kwargs['mask'] = kwargs.get('mask', ITEM_MASK) + + if 'filter' in kwargs: + get_kwargs['filter'] = kwargs['filter'] + + package = self.get_package_by_key(package_keyname, mask='id') + if not package: + raise exceptions.SoftLayerError("Package {} does not exist".format(package_keyname)) + + items = self.package_svc.getItems(id=package['id'], **get_kwargs) + return items + + def list_packages(self, **kwargs): + """List active packages. + + :returns: List of active packages. + + """ + get_kwargs = {} + get_kwargs['mask'] = kwargs.get('mask', PACKAGE_MASK) + + if 'filter' in kwargs: + get_kwargs['filter'] = kwargs['filter'] + + packages = self.package_svc.getAllObjects(**get_kwargs) + + return [package for package in packages if package['isActive']] + + def list_presets(self, package_keyname, **kwargs): + """Gets active presets for the given package. + + :param str package_keyname: The package for which to get presets + :returns: A list of package presets that can be used for ordering + + """ + get_kwargs = {} + get_kwargs['mask'] = kwargs.get('mask', PRESET_MASK) + + if 'filter' in kwargs: + get_kwargs['filter'] = kwargs['filter'] + + package = self.get_package_by_key(package_keyname, mask='id') + if not package: + raise exceptions.SoftLayerError("Package {} does not exist".format(package_keyname)) + + acc_presets = self.package_svc.getAccountRestrictedActivePresets( + id=package['id'], **get_kwargs) + active_presets = self.package_svc.getActivePresets(id=package['id'], **get_kwargs) + return active_presets + acc_presets + + def get_preset_by_key(self, package_keyname, preset_keyname, mask=None): + """Gets a single preset with the given key.""" + preset_operation = '_= %s' % preset_keyname + _filter = {'activePresets': {'keyName': {'operation': preset_operation}}} + + presets = self.list_presets(package_keyname, mask=mask, filter=_filter) + + if len(presets) == 0: + raise exceptions.SoftLayerError( + "Preset {} does not exist in package {}".format(preset_keyname, + package_keyname)) + + return presets[0] + + def get_price_id_list(self, package_keyname, item_keynames): + """Converts a list of item keynames to a list of price IDs. + + This function is used to convert a list of item keynames into + a list of price IDs that are used in the Product_Order verifyOrder() + and placeOrder() functions. + + :param str package_keyname: The package associated with the prices + :param list item_keynames: A list of item keyname strings + :returns: A list of price IDs associated with the given item + keynames in the given package + + """ + mask = 'id, keyName, prices' + items = self.list_items(package_keyname, mask=mask) + + prices = [] + for item_keyname in item_keynames: + try: + # Need to find the item in the package that has a matching + # keyName with the current item we are searching for + matching_item = [i for i in items + if i['keyName'] == item_keyname][0] + except IndexError: + raise exceptions.SoftLayerError( + "Item {} does not exist for package {}".format(item_keyname, + package_keyname)) + + # we want to get the price ID that has no location attached to it, + # because that is the most generic price. verifyOrder/placeOrder + # can take that ID and create the proper price for us in the location + # in which the order is made + price_id = [p['id'] for p in matching_item['prices'] + if p['locationGroupId'] == ''][0] + prices.append(price_id) + + return prices + + def verify_order(self, package_keyname, location, item_keynames, complex_type=None, + hourly=True, preset_keyname=None, extras=None, quantity=1): + """Verifies an order with the given package and prices. + + This function takes in parameters needed for an order and verifies the order + to ensure the given items are compatible with the given package. + + :param str package_keyname: The keyname for the package being ordered + :param str location: The datacenter location string for ordering (Ex: DALLAS13) + :param list item_keynames: The list of item keyname strings to order. To see list of + possible keynames for a package, use list_items() + (or `slcli order item-list`) + :param str complex_type: The complex type to send with the order. Typically begins + with 'SoftLayer_Container_Product_Order_'. + :param bool hourly: If true, uses hourly billing, otherwise uses monthly billing + :param string preset_keyname: If needed, specifies a preset to use for that package. + To see a list of possible keynames for a package, use + list_preset() (or `slcli order preset-list`) + :param dict extras: The extra data for the order in dictionary format. + Example: A VSI order requires hostname and domain to be set, so + extras will look like the following: + {'virtualGuests': [{'hostname': 'test', + 'domain': 'softlayer.com'}]} + :param int quantity: The number of resources to order + + """ + order = self.generate_order(package_keyname, location, item_keynames, + complex_type=complex_type, hourly=hourly, + preset_keyname=preset_keyname, + extras=extras, quantity=quantity) + return self.order_svc.verifyOrder(order) + + def place_order(self, package_keyname, location, item_keynames, complex_type=None, + hourly=True, preset_keyname=None, extras=None, quantity=1): + """Places an order with the given package and prices. + + This function takes in parameters needed for an order and places the order. + + :param str package_keyname: The keyname for the package being ordered + :param str location: The datacenter location string for ordering (Ex: DALLAS13) + :param list item_keynames: The list of item keyname strings to order. To see list of + possible keynames for a package, use list_items() + (or `slcli order item-list`) + :param str complex_type: The complex type to send with the order. Typically begins + with 'SoftLayer_Container_Product_Order_'. + :param bool hourly: If true, uses hourly billing, otherwise uses monthly billing + :param string preset_keyname: If needed, specifies a preset to use for that package. + To see a list of possible keynames for a package, use + list_preset() (or `slcli order preset-list`) + :param dict extras: The extra data for the order in dictionary format. + Example: A VSI order requires hostname and domain to be set, so + extras will look like the following: + {'virtualGuests': [{'hostname': 'test', + 'domain': 'softlayer.com'}]} + :param int quantity: The number of resources to order + + """ + order = self.generate_order(package_keyname, location, item_keynames, + complex_type=complex_type, hourly=hourly, + preset_keyname=preset_keyname, + extras=extras, quantity=quantity) + return self.order_svc.placeOrder(order) + + def generate_order(self, package_keyname, location, item_keynames, complex_type=None, + hourly=True, preset_keyname=None, extras=None, quantity=1): + """Generates an order with the given package and prices. + + This function takes in parameters needed for an order and generates an order + dictionary. This dictionary can then be used in either verify or placeOrder(). + + :param str package_keyname: The keyname for the package being ordered + :param str location: The datacenter location string for ordering (Ex: DALLAS13) + :param list item_keynames: The list of item keyname strings to order. To see list of + possible keynames for a package, use list_items() + (or `slcli order item-list`) + :param str complex_type: The complex type to send with the order. Typically begins + with 'SoftLayer_Container_Product_Order_'. + :param bool hourly: If true, uses hourly billing, otherwise uses monthly billing + :param string preset_keyname: If needed, specifies a preset to use for that package. + To see a list of possible keynames for a package, use + list_preset() (or `slcli order preset-list`) + :param dict extras: The extra data for the order in dictionary format. + Example: A VSI order requires hostname and domain to be set, so + extras will look like the following: + {'virtualGuests': [{'hostname': 'test', + 'domain': 'softlayer.com'}]} + :param int quantity: The number of resources to order + + """ + order = {} + extras = extras or {} + + package = self.get_package_by_key(package_keyname, mask='id') + if not package: + raise exceptions.SoftLayerError("Package {} does not exist".format(package_keyname)) + + # if there was extra data given for the order, add it to the order + # example: VSIs require hostname and domain set on the order, so + # extras will be {'virtualGuests': [{'hostname': 'test', + # 'domain': 'softlayer.com'}]} + order.update(extras) + order['packageId'] = package['id'] + order['location'] = location + order['quantity'] = quantity + order['useHourlyPricing'] = hourly + + if preset_keyname: + preset_id = self.get_preset_by_key(package_keyname, preset_keyname)['id'] + order['presetId'] = preset_id + + if not complex_type: + raise exceptions.SoftLayerError("A complex type must be specified with the order") + order['complexType'] = complex_type + + price_ids = self.get_price_id_list(package_keyname, item_keynames) + order['prices'] = [{'id': price_id} for price_id in price_ids] + return order diff --git a/tests/CLI/modules/order_tests.py b/tests/CLI/modules/order_tests.py new file mode 100644 index 000000000..2d704c824 --- /dev/null +++ b/tests/CLI/modules/order_tests.py @@ -0,0 +1,186 @@ +""" + SoftLayer.tests.CLI.modules.order_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + :license: MIT, see LICENSE for more details. +""" +import json + +from SoftLayer import testing + + +class OrderTests(testing.TestCase): + def test_category_list(self): + cat1 = {'itemCategory': {'name': 'cat1', 'categoryCode': 'code1'}, + 'isRequired': 1} + cat2 = {'itemCategory': {'name': 'cat2', 'categoryCode': 'code2'}, + 'isRequired': 0} + p_mock = self.set_mock('SoftLayer_Product_Package', 'getConfiguration') + p_mock.return_value = [cat1, cat2] + + result = self.run_command(['order', 'category-list', 'package']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Package', 'getConfiguration') + self.assertEqual([{'name': 'cat1', + 'categoryCode': 'code1', + 'isRequired': 'Y'}, + {'name': 'cat2', + 'categoryCode': 'code2', + 'isRequired': 'N'}], + json.loads(result.output)) + + def test_item_list(self): + item1 = {'keyName': 'item1', 'description': 'description1'} + item2 = {'keyName': 'item2', 'description': 'description2'} + p_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + p_mock.return_value = [item1, item2] + + result = self.run_command(['order', 'item-list', 'package']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Package', 'getItems') + self.assertEqual([{'keyName': 'item1', + 'description': 'description1'}, + {'keyName': 'item2', + 'description': 'description2'}], + json.loads(result.output)) + + def test_package_list(self): + item1 = {'name': 'package1', 'keyName': 'PACKAGE1', + 'isActive': 1} + item2 = {'name': 'package2', 'keyName': 'PACKAGE2', + 'isActive': 1} + p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + p_mock.return_value = [item1, item2] + + result = self.run_command(['order', 'package-list']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects') + self.assertEqual([{'name': 'package1', + 'keyName': 'PACKAGE1'}, + {'name': 'package2', + 'keyName': 'PACKAGE2'}], + json.loads(result.output)) + + def test_place(self): + order_date = '2017-04-04 07:39:20' + order = {'orderId': 1234, 'orderDate': order_date, + 'placedOrder': {'status': 'APPROVED'}} + verify_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + place_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + items_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + + verify_mock.return_value = self._get_verified_order_return() + place_mock.return_value = order + items_mock.return_value = self._get_order_items() + + result = self.run_command(['-y', 'order', 'place', 'package', 'DALLAS13', 'ITEM1', + '--complex-type', 'SoftLayer_Container_Product_Order_Thing']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + self.assertEqual({'id': 1234, + 'created': order_date, + 'status': 'APPROVED'}, + json.loads(result.output)) + + def test_verify_hourly(self): + order_date = '2017-04-04 07:39:20' + order = {'orderId': 1234, 'orderDate': order_date, + 'placedOrder': {'status': 'APPROVED'}} + verify_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + items_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + + order = self._get_verified_order_return() + verify_mock.return_value = order + items_mock.return_value = self._get_order_items() + + result = self.run_command(['order', 'place', '--billing', 'hourly', '--verify', + '--complex-type', 'SoftLayer_Container_Product_Order_Thing', + 'package', 'DALLAS13', 'ITEM1', 'ITEM2']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + expected_results = [] + + for price in order['prices']: + expected_results.append({'keyName': price['item']['keyName'], + 'description': price['item']['description'], + 'cost': price['hourlyRecurringFee']}) + + self.assertEqual(expected_results, + json.loads(result.output)) + + def test_verify_monthly(self): + order_date = '2017-04-04 07:39:20' + order = {'orderId': 1234, 'orderDate': order_date, + 'placedOrder': {'status': 'APPROVED'}} + verify_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + items_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + + order = self._get_verified_order_return() + verify_mock.return_value = order + items_mock.return_value = self._get_order_items() + + result = self.run_command(['order', 'place', '--billing', 'monthly', '--verify', + '--complex-type', 'SoftLayer_Container_Product_Order_Thing', + 'package', 'DALLAS13', 'ITEM1', 'ITEM2']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + expected_results = [] + + for price in order['prices']: + expected_results.append({'keyName': price['item']['keyName'], + 'description': price['item']['description'], + 'cost': price['recurringFee']}) + + self.assertEqual(expected_results, + json.loads(result.output)) + + def test_preset_list(self): + active_preset1 = {'name': 'active1', 'keyName': 'PRESET1', + 'description': 'description1'} + active_preset2 = {'name': 'active2', 'keyName': 'PRESET2', + 'description': 'description2'} + acc_preset = {'name': 'account1', 'keyName': 'PRESET3', + 'description': 'description3'} + active_mock = self.set_mock('SoftLayer_Product_Package', 'getActivePresets') + account_mock = self.set_mock('SoftLayer_Product_Package', + 'getAccountRestrictedActivePresets') + active_mock.return_value = [active_preset1, active_preset2] + account_mock.return_value = [acc_preset] + + result = self.run_command(['order', 'preset-list', 'package']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Package', 'getActivePresets') + self.assert_called_with('SoftLayer_Product_Package', + 'getAccountRestrictedActivePresets') + self.assertEqual([{'name': 'active1', + 'keyName': 'PRESET1', + 'description': 'description1'}, + {'name': 'active2', + 'keyName': 'PRESET2', + 'description': 'description2'}, + {'name': 'account1', + 'keyName': 'PRESET3', + 'description': 'description3'}], + json.loads(result.output)) + + def _get_order_items(self): + item1 = {'keyName': 'ITEM1', 'description': 'description1', + 'prices': [{'id': 1111, 'locationGroupId': ''}]} + item2 = {'keyName': 'ITEM2', 'description': 'description2', + 'prices': [{'id': 2222, 'locationGroupId': ''}]} + + return [item1, item2] + + def _get_verified_order_return(self): + item1, item2 = self._get_order_items() + price1 = {'item': item1, 'hourlyRecurringFee': '0.04', + 'recurringFee': '120'} + price2 = {'item': item2, 'hourlyRecurringFee': '0.05', + 'recurringFee': '150'} + return {'prices': [price1, price2]} diff --git a/tests/managers/ordering_tests.py b/tests/managers/ordering_tests.py index 0119b16bf..80c9f24d1 100644 --- a/tests/managers/ordering_tests.py +++ b/tests/managers/ordering_tests.py @@ -4,7 +4,10 @@ :license: MIT, see LICENSE for more details. """ +import mock + import SoftLayer +from SoftLayer import exceptions from SoftLayer import fixtures from SoftLayer import testing @@ -45,16 +48,16 @@ def test_get_package_by_type_returns_if_found(self): self.assertIsNotNone(package) def test_get_package_by_type_returns_none_if_not_found(self): - mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - mock.return_value = [] + p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + p_mock.return_value = [] package = self.ordering.get_package_by_type("PIZZA_FLAVORED_SERVERS") self.assertIsNone(package) def test_get_package_id_by_type_returns_valid_id(self): - mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - mock.return_value = [ + p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + p_mock.return_value = [ {'id': 46, 'name': 'Virtual Servers', 'description': 'Virtual Server Instances', 'type': {'keyName': 'VIRTUAL_SERVER_INSTANCE'}, 'isActive': 1}, @@ -66,8 +69,8 @@ def test_get_package_id_by_type_returns_valid_id(self): self.assertEqual(46, package_id) def test_get_package_id_by_type_fails_for_nonexistent_package_type(self): - mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - mock.return_value = [] + p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + p_mock.return_value = [] self.assertRaises(ValueError, self.ordering.get_package_id_by_type, @@ -131,9 +134,289 @@ def test_get_package_by_key_returns_if_found(self): self.assertIsNotNone(package) def test_get_package_by_key_returns_none_if_not_found(self): - mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - mock.return_value = [] + p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + p_mock.return_value = [] package = self.ordering.get_package_by_key("WILLY_NILLY_SERVERS") self.assertIsNone(package) + + def test_list_categories(self): + p_mock = self.set_mock('SoftLayer_Product_Package', 'getConfiguration') + p_mock.return_value = ['cat1', 'cat2'] + + with mock.patch.object(self.ordering, 'get_package_by_key') as mock_get_pkg: + mock_get_pkg.return_value = {'id': 1234} + + cats = self.ordering.list_categories('PACKAGE_KEYNAME') + + mock_get_pkg.assert_called_once_with('PACKAGE_KEYNAME', mask='id') + self.assertEqual(p_mock.return_value, cats) + + def test_list_categories_package_not_found(self): + self._assert_package_error(self.ordering.list_categories, + 'PACKAGE_KEYNAME') + + def test_list_items(self): + p_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + p_mock.return_value = ['item1', 'item2'] + + with mock.patch.object(self.ordering, 'get_package_by_key') as mock_get_pkg: + mock_get_pkg.return_value = {'id': 1234} + + items = self.ordering.list_items('PACKAGE_KEYNAME') + + mock_get_pkg.assert_called_once_with('PACKAGE_KEYNAME', mask='id') + self.assertEqual(p_mock.return_value, items) + + def test_list_items_package_not_found(self): + self._assert_package_error(self.ordering.list_items, + 'PACKAGE_KEYNAME') + + def test_list_packages(self): + packages = [{'id': 1234, 'isActive': True}, + {'id': 1235, 'isActive': True}] + p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + p_mock.return_value = packages + + actual_pkgs = self.ordering.list_packages() + + self.assertEqual(packages, actual_pkgs) + + def test_list_packages_not_active(self): + packages = [{'id': 1234, 'isActive': True}, + {'id': 1235, 'isActive': False}] + p_mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + p_mock.return_value = packages + + actual_pkgs = self.ordering.list_packages() + + # Make sure that the list returned only contained the package + # that was active + self.assertEqual([packages[0]], actual_pkgs) + + def test_list_presets(self): + acct_presets = ['acctPreset1', 'acctPreset2'] + active_presets = ['activePreset3', 'activePreset4'] + + acct_preset_mock = self.set_mock('SoftLayer_Product_Package', + 'getAccountRestrictedActivePresets') + active_preset_mock = self.set_mock('SoftLayer_Product_Package', + 'getActivePresets') + acct_preset_mock.return_value = acct_presets + active_preset_mock.return_value = active_presets + + presets = self.ordering.list_presets('PACKAGE_KEYNAME') + + # Make sure the preset list returns both active presets and + # account restricted presets + self.assertEqual(active_presets + acct_presets, presets) + + def test_list_presets_package_not_found(self): + self._assert_package_error(self.ordering.list_presets, + 'PACKAGE_KEYNAME') + + def test_get_preset_by_key(self): + keyname = 'PRESET_KEYNAME' + preset_filter = {'activePresets': {'keyName': {'operation': '_= %s' % keyname}}} + + with mock.patch.object(self.ordering, 'list_presets') as list_mock: + list_mock.return_value = ['preset1'] + + preset = self.ordering.get_preset_by_key('PACKAGE_KEYNAME', keyname) + + list_mock.assert_called_once_with('PACKAGE_KEYNAME', filter=preset_filter, + mask=None) + self.assertEqual(list_mock.return_value[0], preset) + + def test_get_preset_by_key_preset_not_found(self): + keyname = 'PRESET_KEYNAME' + preset_filter = {'activePresets': {'keyName': {'operation': '_= %s' % keyname}}} + + with mock.patch.object(self.ordering, 'list_presets') as list_mock: + list_mock.return_value = [] + + exc = self.assertRaises(exceptions.SoftLayerError, + self.ordering.get_preset_by_key, + 'PACKAGE_KEYNAME', keyname) + + list_mock.assert_called_once_with('PACKAGE_KEYNAME', filter=preset_filter, + mask=None) + self.assertEqual('Preset {} does not exist in package {}'.format(keyname, + 'PACKAGE_KEYNAME'), + str(exc)) + + def test_get_price_id_list(self): + price1 = {'id': 1234, 'locationGroupId': ''} + item1 = {'id': 1111, + 'keyName': 'ITEM1', + 'prices': [price1]} + price2 = {'id': 5678, 'locationGroupId': ''} + item2 = {'id': 2222, + 'keyName': 'ITEM2', + 'prices': [price2]} + + with mock.patch.object(self.ordering, 'list_items') as list_mock: + list_mock.return_value = [item1, item2] + + prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', + ['ITEM1', 'ITEM2']) + + list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, keyName, prices') + self.assertEqual([price1['id'], price2['id']], prices) + + def test_get_price_id_list_item_not_found(self): + price1 = {'id': 1234, 'locationGroupId': ''} + item1 = {'id': 1111, + 'keyName': 'ITEM1', + 'prices': [price1]} + + with mock.patch.object(self.ordering, 'list_items') as list_mock: + list_mock.return_value = [item1] + + exc = self.assertRaises(exceptions.SoftLayerError, + self.ordering.get_price_id_list, + 'PACKAGE_KEYNAME', ['ITEM2']) + list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, keyName, prices') + self.assertEqual("Item ITEM2 does not exist for package PACKAGE_KEYNAME", + str(exc)) + + def test_generate_order_package_not_found(self): + self._assert_package_error(self.ordering.generate_order, + 'PACKAGE_KEYNAME', 'DALLAS13', + ['item1', 'item2']) + + def test_generate_no_complex_type(self): + pkg = 'PACKAGE_KEYNAME' + items = ['ITEM1', 'ITEM2'] + exc = self.assertRaises(exceptions.SoftLayerError, + self.ordering.generate_order, + pkg, 'DALLAS13', items) + + self.assertEqual("A complex type must be specified with the order", + str(exc)) + + def test_generate_order_with_preset(self): + pkg = 'PACKAGE_KEYNAME' + complex_type = 'SoftLayer_Container_Foo' + items = ['ITEM1', 'ITEM2'] + preset = 'PRESET_KEYNAME' + expected_order = {'complexType': 'SoftLayer_Container_Foo', + 'location': 'DALLAS13', + 'packageId': 1234, + 'presetId': 5678, + 'prices': [{'id': 1111}, {'id': 2222}], + 'quantity': 1, + 'useHourlyPricing': True} + + mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() + + order = self.ordering.generate_order(pkg, 'DALLAS13', items, + preset_keyname=preset, + complex_type=complex_type) + + mock_pkg.assert_called_once_with(pkg, mask='id') + mock_preset.assert_called_once_with(pkg, preset) + mock_get_ids.assert_called_once_with(pkg, items) + self.assertEqual(expected_order, order) + + def test_generate_order(self): + pkg = 'PACKAGE_KEYNAME' + items = ['ITEM1', 'ITEM2'] + complex_type = 'My_Type' + expected_order = {'complexType': 'My_Type', + 'location': 'DALLAS13', + 'packageId': 1234, + 'prices': [{'id': 1111}, {'id': 2222}], + 'quantity': 1, + 'useHourlyPricing': True} + + mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() + + order = self.ordering.generate_order(pkg, 'DALLAS13', items, + complex_type=complex_type) + + mock_pkg.assert_called_once_with(pkg, mask='id') + mock_preset.assert_not_called() + mock_get_ids.assert_called_once_with(pkg, items) + self.assertEqual(expected_order, order) + + def test_verify_order(self): + ord_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + ord_mock.return_value = {'id': 1234} + pkg = 'PACKAGE_KEYNAME' + location = 'DALLAS13' + items = ['ITEM1', 'ITEM2'] + hourly = True + preset_keyname = 'PRESET' + complex_type = 'Complex_Type' + extras = {'foo': 'bar'} + quantity = 1 + + with mock.patch.object(self.ordering, 'generate_order') as gen_mock: + gen_mock.return_value = {'order': {}} + + order = self.ordering.verify_order(pkg, location, items, hourly=hourly, + preset_keyname=preset_keyname, + complex_type=complex_type, + extras=extras, quantity=quantity) + + gen_mock.assert_called_once_with(pkg, location, items, hourly=hourly, + preset_keyname=preset_keyname, + complex_type=complex_type, + extras=extras, quantity=quantity) + self.assertEqual(ord_mock.return_value, order) + + def test_place_order(self): + ord_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + ord_mock.return_value = {'id': 1234} + pkg = 'PACKAGE_KEYNAME' + location = 'DALLAS13' + items = ['ITEM1', 'ITEM2'] + hourly = True + preset_keyname = 'PRESET' + complex_type = 'Complex_Type' + extras = {'foo': 'bar'} + quantity = 1 + + with mock.patch.object(self.ordering, 'generate_order') as gen_mock: + gen_mock.return_value = {'order': {}} + + order = self.ordering.place_order(pkg, location, items, hourly=hourly, + preset_keyname=preset_keyname, + complex_type=complex_type, + extras=extras, quantity=quantity) + + gen_mock.assert_called_once_with(pkg, location, items, hourly=hourly, + preset_keyname=preset_keyname, + complex_type=complex_type, + extras=extras, quantity=quantity) + self.assertEqual(ord_mock.return_value, order) + + def _patch_for_generate(self): + # mock out get_package_by_key, get_preset_by_key, and get_price_id_list + # with patchers + mock_pkg = mock.patch.object(self.ordering, 'get_package_by_key') + mock_preset = mock.patch.object(self.ordering, 'get_preset_by_key') + mock_get_ids = mock.patch.object(self.ordering, 'get_price_id_list') + + # start each patcher, and set a cleanup to stop each patcher as well + to_return = [] + for mock_func in [mock_pkg, mock_preset, mock_get_ids]: + to_return.append(mock_func.start()) + self.addCleanup(mock_func.stop) + + # set the return values on each of the mocks + to_return[0].return_value = {'id': 1234} + to_return[1].return_value = {'id': 5678} + to_return[2].return_value = [1111, 2222] + return to_return + + def _assert_package_error(self, order_callable, pkg_key, *args, **kwargs): + with mock.patch.object(self.ordering, 'get_package_by_key') as mock_get_pkg: + mock_get_pkg.return_value = None + + exc = self.assertRaises(exceptions.SoftLayerError, order_callable, + pkg_key, *args, **kwargs) + self.assertEqual('Package {} does not exist'.format(pkg_key), + str(exc))