From ab5ba938792df9a9aa5f69edc720adbe670ee64a Mon Sep 17 00:00:00 2001 From: Ryan Rossiter Date: Wed, 22 Nov 2017 14:37:48 -0600 Subject: [PATCH 1/6] Add ordering APIs and commands This change adds a new command, and 5 new subcommands: slcli order - command for interacting with the ordering API slcli order category-list -- subcommand for listing categories in a package slcli order item-list -- subcommand for listing items in a package slcli order package-list -- subcommand for listing available packages slcli order place -- subcommand for verifying/placing orders slcli order preset-list -- subcommand for listing presets of a package API functions in SoftLayer.managers.ordering.OrderingManager were also added to programmatically interact with all of these functions as well. The place_order() and verify_order() commands are built to pass a package keyname, a location, and a list of item keynames. It then transforms the keynames into IDs, which the place/verifyOrder APIs accept. --- SoftLayer/CLI/order/__init__.py | 0 SoftLayer/CLI/order/category_list.py | 37 +++++ SoftLayer/CLI/order/item_list.py | 39 +++++ SoftLayer/CLI/order/package_list.py | 34 +++++ SoftLayer/CLI/order/place.py | 72 ++++++++++ SoftLayer/CLI/order/preset_list.py | 36 +++++ SoftLayer/CLI/routes.py | 7 + SoftLayer/managers/ordering.py | 204 ++++++++++++++++++++++++++- 8 files changed, 423 insertions(+), 6 deletions(-) create mode 100644 SoftLayer/CLI/order/__init__.py create mode 100644 SoftLayer/CLI/order/category_list.py create mode 100644 SoftLayer/CLI/order/item_list.py create mode 100644 SoftLayer/CLI/order/package_list.py create mode 100644 SoftLayer/CLI/order/place.py create mode 100644 SoftLayer/CLI/order/preset_list.py 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..f92d02446 --- /dev/null +++ b/SoftLayer/CLI/order/category_list.py @@ -0,0 +1,37 @@ +"""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 package categories.""" + 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..7f7182017 --- /dev/null +++ b/SoftLayer/CLI/order/item_list.py @@ -0,0 +1,39 @@ +"""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.""" + 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..1c3251e8c --- /dev/null +++ b/SoftLayer/CLI/order/package_list.py @@ -0,0 +1,34 @@ +"""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', ] + + +@click.command() +@click.option('--keyword', + help="A word (or string) used to filter package names.") +@environment.pass_env +def cli(env, keyword): + """List package presets.""" + 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..a2f1a4d56 --- /dev/null +++ b/SoftLayer/CLI/order/place.py @@ -0,0 +1,72 @@ +"""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('--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, extras, order_items): + """Place or verify an order.""" + 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, + '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..03f8fac46 --- /dev/null +++ b/SoftLayer/CLI/order/preset_list.py @@ -0,0 +1,36 @@ +"""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.""" + table = formatting.Table(COLUMNS) + manager = ordering.OrderingManager(env.client) + + _filter = {} + if keyword: + _filter = {'presets': {'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..fce9d759c 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -15,6 +15,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 +30,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 +42,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 +187,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 +200,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 +211,205 @@ 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 = {} + default_mask = '''id, + isRequired, + itemCategory[ + id, + name, + categoryCode + ] + ''' + get_kwargs['mask'] = kwargs.get('mask', default_mask) + + if 'filter' in kwargs: + get_kwargs['filter'] = kwargs['filter'] + + package = self.get_package_by_key(package_keyname, mask='id') + if not package: + raise AttributeError("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 = {} + default_mask = '''id, + keyName, + description + ''' + get_kwargs['mask'] = kwargs.get('mask', default_mask) + + if 'filter' in kwargs: + get_kwargs['filter'] = kwargs['filter'] + + package = self.get_package_by_key(package_keyname, mask='id') + if not package: + raise AttributeError("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 = {} + default_mask = '''id, + name, + keyName, + isActive + ''' + get_kwargs['mask'] = kwargs.get('mask', default_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 = {} + default_mask = '''id, + name, + keyName, + description + ''' + get_kwargs['mask'] = kwargs.get('mask', default_mask) + + if 'filter' in kwargs: + get_kwargs['filter'] = kwargs['filter'] + + package = self.get_package_by_key(package_keyname, mask='id') + if not package: + raise AttributeError("Package {} does not exist".format(package_keyname)) + + presets = self.package_svc.getActivePresets(id=package['id'], **get_kwargs) + return 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 AttributeError( + "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 + + """ + package = self.get_package_by_key(package_keyname, mask='id') + if not package: + raise AttributeError("Package {} does not exist".format(package_keyname)) + + 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 AttributeError( + "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, price_keynames, + hourly=True, preset_keyname=None, extras=None, quantity=1): + """Verifies an order with the given package and prices.""" + order = {} + extras = extras or {} + + package = self.get_package_by_key(package_keyname, mask='id') + if not package: + raise AttributeError("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 + + price_ids = self.get_price_id_list(package_keyname, price_keynames) + order['prices'] = [{'id': price_id} for price_id in price_ids] + + return self.order_svc.verifyOrder(order) + + def place_order(self, package_keyname, location, price_keynames, + hourly=True, preset_keyname=None, extras=None, quantity=1): + """Places an order with the given package and prices.""" + verified_order = self.verify_order(package_keyname, location, price_keynames, + hourly=hourly, + preset_keyname=preset_keyname, + extras=extras, quantity=quantity) + return self.order_svc.placeOrder(verified_order) From 67a54fd84225bd234f8c7c0219efe8502d974667 Mon Sep 17 00:00:00 2001 From: Ryan Rossiter Date: Thu, 30 Nov 2017 16:07:41 -0600 Subject: [PATCH 2/6] Update needed fixes for CLI and add doc The CLI appears to fail out if it gets AttributeError as part of any of the manager calls, so in order to not make it look like a softlayer-pythyon but, we need to spit out something from SoftLayer.exceptions. The base exceptions.SoftLayerError was used. Presets also needed to be handle differently. When listing the presets, both the activePresets and accountRestrictedActivePresets need to be retrieved and merged together before being returned. Finally, extra doc was added to verify_order() and place_order() to make it easier to understand how to use it. --- SoftLayer/managers/ordering.py | 73 ++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index fce9d759c..97dc6f2a7 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -6,6 +6,8 @@ :license: MIT, see LICENSE for more details. """ +from SoftLayer import exceptions + class OrderingManager(object): """Manager to help ordering via the SoftLayer API. @@ -245,7 +247,7 @@ def list_categories(self, package_keyname, **kwargs): package = self.get_package_by_key(package_keyname, mask='id') if not package: - raise AttributeError("Package {} does not exist".format(package_keyname)) + raise exceptions.SoftLayerError("Package {} does not exist".format(package_keyname)) categories = self.package_svc.getConfiguration(id=package['id'], **get_kwargs) return categories @@ -269,7 +271,7 @@ def list_items(self, package_keyname, **kwargs): package = self.get_package_by_key(package_keyname, mask='id') if not package: - raise AttributeError("Package {} does not exist".format(package_keyname)) + raise exceptions.SoftLayerError("Package {} does not exist".format(package_keyname)) items = self.package_svc.getItems(id=package['id'], **get_kwargs) return items @@ -315,10 +317,12 @@ def list_presets(self, package_keyname, **kwargs): package = self.get_package_by_key(package_keyname, mask='id') if not package: - raise AttributeError("Package {} does not exist".format(package_keyname)) + raise exceptions.SoftLayerError("Package {} does not exist".format(package_keyname)) - presets = self.package_svc.getActivePresets(id=package['id'], **get_kwargs) - return presets + acc_presets = self.package_svc.getAccountRestrictedActivePresets( + id=package['id'], **get_kwargs) + active_presets = self.package_svc.getActivePresets(id=package['id'], **get_kwargs) + return acc_presets + active_presets def get_preset_by_key(self, package_keyname, preset_keyname, mask=None): """Gets a single preset with the given key.""" @@ -328,7 +332,7 @@ def get_preset_by_key(self, package_keyname, preset_keyname, mask=None): presets = self.list_presets(package_keyname, mask=mask, filter=_filter) if len(presets) == 0: - raise AttributeError( + raise exceptions.SoftLayerError( "Preset {} does not exist in package {}".format(preset_keyname, package_keyname)) @@ -349,7 +353,7 @@ def get_price_id_list(self, package_keyname, item_keynames): """ package = self.get_package_by_key(package_keyname, mask='id') if not package: - raise AttributeError("Package {} does not exist".format(package_keyname)) + raise exceptions.SoftLayerError("Package {} does not exist".format(package_keyname)) mask = 'id, keyName, prices' items = self.list_items(package_keyname, mask=mask) @@ -362,7 +366,7 @@ def get_price_id_list(self, package_keyname, item_keynames): matching_item = [i for i in items if i['keyName'] == item_keyname][0] except IndexError: - raise AttributeError( + raise exceptions.SoftLayerError( "Item {} does not exist for package {}".format(item_keyname, package_keyname)) @@ -376,15 +380,36 @@ def get_price_id_list(self, package_keyname, item_keynames): return prices - def verify_order(self, package_keyname, location, price_keynames, + def verify_order(self, package_keyname, location, item_keynames, hourly=True, preset_keyname=None, extras=None, quantity=1): - """Verifies an order with the given package and prices.""" + """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 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 AttributeError("Package {} does not exist".format(package_keyname)) + 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 @@ -405,9 +430,31 @@ def verify_order(self, package_keyname, location, price_keynames, return self.order_svc.verifyOrder(order) - def place_order(self, package_keyname, location, price_keynames, + def place_order(self, package_keyname, location, item_keynames, hourly=True, preset_keyname=None, extras=None, quantity=1): - """Places an order with the given package and prices.""" + """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 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 + + """ + # verify the order, and if the order is valid, the proper prices will be filled + # into the order template, so we can just send that to placeOrder to order it verified_order = self.verify_order(package_keyname, location, price_keynames, hourly=hourly, preset_keyname=preset_keyname, From 9526a96930735d46a0a1f32de87c64864aa2033e Mon Sep 17 00:00:00 2001 From: Ryan Rossiter Date: Fri, 1 Dec 2017 09:55:11 -0600 Subject: [PATCH 3/6] Fix more item_keyname renames I updated names from price_keynames -> item_keynames, but I missed a couple. Updated those to be consistent with item_keynames now. --- SoftLayer/managers/ordering.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index 97dc6f2a7..995ed7086 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -425,7 +425,7 @@ def verify_order(self, package_keyname, location, item_keynames, preset_id = self.get_preset_by_key(package_keyname, preset_keyname)['id'] order['presetId'] = preset_id - price_ids = self.get_price_id_list(package_keyname, price_keynames) + price_ids = self.get_price_id_list(package_keyname, item_keynames) order['prices'] = [{'id': price_id} for price_id in price_ids] return self.order_svc.verifyOrder(order) @@ -455,7 +455,7 @@ def place_order(self, package_keyname, location, item_keynames, """ # verify the order, and if the order is valid, the proper prices will be filled # into the order template, so we can just send that to placeOrder to order it - verified_order = self.verify_order(package_keyname, location, price_keynames, + verified_order = self.verify_order(package_keyname, location, item_keynames, hourly=hourly, preset_keyname=preset_keyname, extras=extras, quantity=quantity) From 010f6f8c3dfaba7f1819b39affd00d3cdedeb956 Mon Sep 17 00:00:00 2001 From: Ryan Rossiter Date: Mon, 4 Dec 2017 10:22:57 -0600 Subject: [PATCH 4/6] Add unit tests for the order manager and CLI --- SoftLayer/managers/ordering.py | 62 ++++---- tests/CLI/modules/order_tests.py | 184 +++++++++++++++++++++++ tests/managers/ordering_tests.py | 247 ++++++++++++++++++++++++++++++- 3 files changed, 454 insertions(+), 39 deletions(-) create mode 100644 tests/CLI/modules/order_tests.py diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index 995ed7086..d2b12ea19 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -8,6 +8,32 @@ 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): """Manager to help ordering via the SoftLayer API. @@ -232,15 +258,7 @@ def list_categories(self, package_keyname, **kwargs): :returns: List of categories associated with the package """ get_kwargs = {} - default_mask = '''id, - isRequired, - itemCategory[ - id, - name, - categoryCode - ] - ''' - get_kwargs['mask'] = kwargs.get('mask', default_mask) + get_kwargs['mask'] = kwargs.get('mask', CATEGORY_MASK) if 'filter' in kwargs: get_kwargs['filter'] = kwargs['filter'] @@ -260,11 +278,7 @@ def list_items(self, package_keyname, **kwargs): """ get_kwargs = {} - default_mask = '''id, - keyName, - description - ''' - get_kwargs['mask'] = kwargs.get('mask', default_mask) + get_kwargs['mask'] = kwargs.get('mask', ITEM_MASK) if 'filter' in kwargs: get_kwargs['filter'] = kwargs['filter'] @@ -283,12 +297,7 @@ def list_packages(self, **kwargs): """ get_kwargs = {} - default_mask = '''id, - name, - keyName, - isActive - ''' - get_kwargs['mask'] = kwargs.get('mask', default_mask) + get_kwargs['mask'] = kwargs.get('mask', PACKAGE_MASK) if 'filter' in kwargs: get_kwargs['filter'] = kwargs['filter'] @@ -305,12 +314,7 @@ def list_presets(self, package_keyname, **kwargs): """ get_kwargs = {} - default_mask = '''id, - name, - keyName, - description - ''' - get_kwargs['mask'] = kwargs.get('mask', default_mask) + get_kwargs['mask'] = kwargs.get('mask', PRESET_MASK) if 'filter' in kwargs: get_kwargs['filter'] = kwargs['filter'] @@ -322,7 +326,7 @@ def list_presets(self, package_keyname, **kwargs): acc_presets = self.package_svc.getAccountRestrictedActivePresets( id=package['id'], **get_kwargs) active_presets = self.package_svc.getActivePresets(id=package['id'], **get_kwargs) - return acc_presets + active_presets + 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.""" @@ -351,10 +355,6 @@ def get_price_id_list(self, package_keyname, item_keynames): keynames in the given package """ - package = self.get_package_by_key(package_keyname, mask='id') - if not package: - raise exceptions.SoftLayerError("Package {} does not exist".format(package_keyname)) - mask = 'id, keyName, prices' items = self.list_items(package_keyname, mask=mask) diff --git a/tests/CLI/modules/order_tests.py b/tests/CLI/modules/order_tests.py new file mode 100644 index 000000000..478c6ffca --- /dev/null +++ b/tests/CLI/modules/order_tests.py @@ -0,0 +1,184 @@ +""" + 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']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + 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', + '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', + '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..47b3128c5 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,237 @@ 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_verify_order(self): + ord_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + ord_mock.return_value = {'id': 1234} + pkg = 'PACKAGE_KEYNAME' + items = ['ITEM1', 'ITEM2'] + + mock_pkg, mock_preset, mock_get_ids = self._patch_for_verify() + + order = self.ordering.verify_order(pkg, 'DALLAS13', items) + + 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(ord_mock.return_value, order) + + def test_verify_order_package_not_found(self): + self._assert_package_error(self.ordering.verify_order, + 'PACKAGE_KEYNAME', 'DALLAS13', + ['item1', 'item2']) + + def test_verify_order_with_preset(self): + ord_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + ord_mock.return_value = {'id': 1234} + pkg = 'PACKAGE_KEYNAME' + items = ['ITEM1', 'ITEM2'] + preset = 'PRESET_KEYNAME' + + mock_pkg, mock_preset, mock_get_ids = self._patch_for_verify() + + order = self.ordering.verify_order(pkg, 'DALLAS13', items, + preset_keyname=preset) + + 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(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' + extras = {'foo': 'bar'} + quantity = 1 + + with mock.patch.object(self.ordering, 'verify_order') as verify_mock: + verify_mock.return_value = {'orderContainers': {}} + + order = self.ordering.place_order(pkg, location, items, hourly=hourly, + preset_keyname=preset_keyname, + extras=extras, quantity=quantity) + + verify_mock.assert_called_once_with(pkg, location, items, hourly=hourly, + preset_keyname=preset_keyname, + extras=extras, quantity=quantity) + self.assertEqual(ord_mock.return_value, order) + + def _patch_for_verify(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)) From 08255fe2fb8f2fe2362839e1158a7ef1ec402c7a Mon Sep 17 00:00:00 2001 From: Ryan Rossiter Date: Tue, 19 Dec 2017 16:38:31 -0600 Subject: [PATCH 5/6] Add CLI doc and move order generation verifyOrder and placeOrder need to be called with the same generated order dict, and the output of verifyOrder can't be sent to placeOrder, since it strips out some extra data. --- SoftLayer/CLI/order/category_list.py | 19 ++++- SoftLayer/CLI/order/item_list.py | 23 +++++- SoftLayer/CLI/order/package_list.py | 20 +++++- SoftLayer/CLI/order/place.py | 44 +++++++++++- SoftLayer/CLI/order/preset_list.py | 22 +++++- SoftLayer/managers/ordering.py | 104 ++++++++++++++++++--------- tests/CLI/modules/order_tests.py | 6 +- tests/managers/ordering_tests.py | 92 +++++++++++++++++++----- 8 files changed, 271 insertions(+), 59 deletions(-) diff --git a/SoftLayer/CLI/order/category_list.py b/SoftLayer/CLI/order/category_list.py index f92d02446..91671b8e0 100644 --- a/SoftLayer/CLI/order/category_list.py +++ b/SoftLayer/CLI/order/category_list.py @@ -17,7 +17,24 @@ help="List only the required categories for the package") @environment.pass_env def cli(env, package_keyname, required): - """List package categories.""" + """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) diff --git a/SoftLayer/CLI/order/item_list.py b/SoftLayer/CLI/order/item_list.py index 7f7182017..61841d267 100644 --- a/SoftLayer/CLI/order/item_list.py +++ b/SoftLayer/CLI/order/item_list.py @@ -19,7 +19,28 @@ help="Category code to filter items by") @environment.pass_env def cli(env, package_keyname, keyword, category): - """List package items.""" + """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) diff --git a/SoftLayer/CLI/order/package_list.py b/SoftLayer/CLI/order/package_list.py index 1c3251e8c..9a6b97e6c 100644 --- a/SoftLayer/CLI/order/package_list.py +++ b/SoftLayer/CLI/order/package_list.py @@ -1,4 +1,4 @@ -"""List package presets.""" +"""List packages.""" # :license: MIT, see LICENSE for more details. import click @@ -16,7 +16,23 @@ help="A word (or string) used to filter package names.") @environment.pass_env def cli(env, keyword): - """List package presets.""" + """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) diff --git a/SoftLayer/CLI/order/place.py b/SoftLayer/CLI/order/place.py index a2f1a4d56..f042e2517 100644 --- a/SoftLayer/CLI/order/place.py +++ b/SoftLayer/CLI/order/place.py @@ -28,12 +28,51 @@ 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, extras, order_items): - """Place or verify an order.""" +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: @@ -43,6 +82,7 @@ def cli(env, package_keyname, location, preset, verify, billing, extras, order_i kwargs = {'preset_keyname': preset, 'extras': extras, 'quantity': 1, + 'complex_type': complex_type, 'hourly': True if billing == 'hourly' else False} if verify: diff --git a/SoftLayer/CLI/order/preset_list.py b/SoftLayer/CLI/order/preset_list.py index 03f8fac46..a619caf77 100644 --- a/SoftLayer/CLI/order/preset_list.py +++ b/SoftLayer/CLI/order/preset_list.py @@ -18,13 +18,31 @@ help="A word (or string) used to filter preset names.") @environment.pass_env def cli(env, package_keyname, keyword): - """List package presets.""" + """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 = {'presets': {'name': {'operation': '*= %s' % keyword}}} + _filter = {'activePresets': {'name': {'operation': '*= %s' % keyword}}} presets = manager.list_presets(package_keyname, filter=_filter) for preset in presets: diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index d2b12ea19..7185bc9e9 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -5,6 +5,7 @@ :license: MIT, see LICENSE for more details. """ +# pylint: disable=no-self-use from SoftLayer import exceptions @@ -380,7 +381,7 @@ def get_price_id_list(self, package_keyname, item_keynames): return prices - def verify_order(self, package_keyname, location, item_keynames, + 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. @@ -392,6 +393,8 @@ def verify_order(self, package_keyname, location, item_keynames, :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 @@ -404,33 +407,13 @@ def verify_order(self, package_keyname, location, item_keynames, :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 - - price_ids = self.get_price_id_list(package_keyname, item_keynames) - order['prices'] = [{'id': price_id} for price_id in price_ids] - + 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, + 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. @@ -441,6 +424,40 @@ def place_order(self, package_keyname, location, item_keynames, :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 @@ -453,10 +470,31 @@ def place_order(self, package_keyname, location, item_keynames, :param int quantity: The number of resources to order """ - # verify the order, and if the order is valid, the proper prices will be filled - # into the order template, so we can just send that to placeOrder to order it - verified_order = self.verify_order(package_keyname, location, item_keynames, - hourly=hourly, - preset_keyname=preset_keyname, - extras=extras, quantity=quantity) - return self.order_svc.placeOrder(verified_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 index 478c6ffca..2d704c824 100644 --- a/tests/CLI/modules/order_tests.py +++ b/tests/CLI/modules/order_tests.py @@ -75,10 +75,10 @@ def test_place(self): place_mock.return_value = order items_mock.return_value = self._get_order_items() - result = self.run_command(['-y', 'order', 'place', 'package', 'DALLAS13', 'ITEM1']) + 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', 'verifyOrder') self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') self.assertEqual({'id': 1234, 'created': order_date, @@ -97,6 +97,7 @@ def test_verify_hourly(self): 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) @@ -123,6 +124,7 @@ def test_verify_monthly(self): 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) diff --git a/tests/managers/ordering_tests.py b/tests/managers/ordering_tests.py index 47b3128c5..73cd1b8c3 100644 --- a/tests/managers/ordering_tests.py +++ b/tests/managers/ordering_tests.py @@ -281,41 +281,98 @@ def test_get_price_id_list_item_not_found(self): self.assertEqual("Item ITEM2 does not exist for package PACKAGE_KEYNAME", str(exc)) - def test_verify_order(self): + def test_generate_order(self): ord_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') ord_mock.return_value = {'id': 1234} pkg = 'PACKAGE_KEYNAME' + complex_type = 'SoftLayer_Container_Foo' items = ['ITEM1', 'ITEM2'] - mock_pkg, mock_preset, mock_get_ids = self._patch_for_verify() + mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() - order = self.ordering.verify_order(pkg, 'DALLAS13', items) + order = self.ordering.verify_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(ord_mock.return_value, order) - def test_verify_order_package_not_found(self): - self._assert_package_error(self.ordering.verify_order, + def test_generate_order_package_not_found(self): + self._assert_package_error(self.ordering.generate_order, 'PACKAGE_KEYNAME', 'DALLAS13', ['item1', 'item2']) - def test_verify_order_with_preset(self): - ord_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') - ord_mock.return_value = {'id': 1234} + 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 = {} - mock_pkg, mock_preset, mock_get_ids = self._patch_for_verify() + mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() order = self.ordering.verify_order(pkg, 'DALLAS13', items, - preset_keyname=preset) + 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): + ord_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + ord_mock.return_value = {'id': 1234} + pkg = 'PACKAGE_KEYNAME' + items = ['ITEM1', 'ITEM2'] + complex_type = 'My_Type' + expected_order = {} + + mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() + + order = self.ordering.verify_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): @@ -326,22 +383,25 @@ def test_place_order(self): items = ['ITEM1', 'ITEM2'] hourly = True preset_keyname = 'PRESET' + complex_type = 'Complex_Type' extras = {'foo': 'bar'} quantity = 1 - with mock.patch.object(self.ordering, 'verify_order') as verify_mock: - verify_mock.return_value = {'orderContainers': {}} + 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) - verify_mock.assert_called_once_with(pkg, location, items, hourly=hourly, - preset_keyname=preset_keyname, - 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_verify(self): + 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') From 9d6c96222dc6b81873025972e90c83452b11d406 Mon Sep 17 00:00:00 2001 From: Ryan Rossiter Date: Tue, 2 Jan 2018 09:03:46 -0600 Subject: [PATCH 6/6] Fix up generate_order() unit tests --- tests/managers/ordering_tests.py | 44 +++++++++++++------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/tests/managers/ordering_tests.py b/tests/managers/ordering_tests.py index 73cd1b8c3..80c9f24d1 100644 --- a/tests/managers/ordering_tests.py +++ b/tests/managers/ordering_tests.py @@ -281,23 +281,6 @@ def test_get_price_id_list_item_not_found(self): self.assertEqual("Item ITEM2 does not exist for package PACKAGE_KEYNAME", str(exc)) - def test_generate_order(self): - ord_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') - ord_mock.return_value = {'id': 1234} - pkg = 'PACKAGE_KEYNAME' - complex_type = 'SoftLayer_Container_Foo' - items = ['ITEM1', 'ITEM2'] - - mock_pkg, mock_preset, mock_get_ids = self._patch_for_generate() - - order = self.ordering.verify_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(ord_mock.return_value, order) - def test_generate_order_package_not_found(self): self._assert_package_error(self.ordering.generate_order, 'PACKAGE_KEYNAME', 'DALLAS13', @@ -318,13 +301,19 @@ def test_generate_order_with_preset(self): complex_type = 'SoftLayer_Container_Foo' items = ['ITEM1', 'ITEM2'] preset = 'PRESET_KEYNAME' - expected_order = {} + 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.verify_order(pkg, 'DALLAS13', items, - preset_keyname=preset, - complex_type=complex_type) + 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) @@ -332,17 +321,20 @@ def test_generate_order_with_preset(self): self.assertEqual(expected_order, order) def test_generate_order(self): - ord_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') - ord_mock.return_value = {'id': 1234} pkg = 'PACKAGE_KEYNAME' items = ['ITEM1', 'ITEM2'] complex_type = 'My_Type' - expected_order = {} + 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.verify_order(pkg, 'DALLAS13', items, - complex_type=complex_type) + 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()