diff --git a/SoftLayer/CLI/order/category_list.py b/SoftLayer/CLI/order/category_list.py index 91671b8e0..9e0ea3a76 100644 --- a/SoftLayer/CLI/order/category_list.py +++ b/SoftLayer/CLI/order/category_list.py @@ -19,18 +19,11 @@ 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 diff --git a/SoftLayer/CLI/order/item_list.py b/SoftLayer/CLI/order/item_list.py index 92f83ceaf..ad9ae537f 100644 --- a/SoftLayer/CLI/order/item_list.py +++ b/SoftLayer/CLI/order/item_list.py @@ -18,29 +18,18 @@ 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 item keyNames 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 - Note: + .. Note:: Items with a numbered category, like disk0 or gpu0, can be included multiple times in an order to match how many of the item you want to order. - \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 diff --git a/SoftLayer/CLI/order/package_list.py b/SoftLayer/CLI/order/package_list.py index 145ad0de1..917af56fe 100644 --- a/SoftLayer/CLI/order/package_list.py +++ b/SoftLayer/CLI/order/package_list.py @@ -20,23 +20,16 @@ def cli(env, keyword, package_type): """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. + # List out all packages for ordering + slcli order package-list - \b - Example: # List out all packages with "server" in the name slcli order package-list --keyword server - Package types can be used to remove unwanted packages - \b - Example: + # Select only specifict package types slcli order package-list --package_type BARE_METAL_CPU """ manager = ordering.OrderingManager(env.client) diff --git a/SoftLayer/CLI/order/place.py b/SoftLayer/CLI/order/place.py index c6f6a129b..6b66fb110 100644 --- a/SoftLayer/CLI/order/place.py +++ b/SoftLayer/CLI/order/place.py @@ -32,8 +32,8 @@ 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('--complex-type', + help=("The complex type of the order. Starts 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) @@ -55,8 +55,9 @@ def cli(env, package_keyname, location, preset, verify, billing, complex_type, 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: + + 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 \\ diff --git a/SoftLayer/CLI/order/place_quote.py b/SoftLayer/CLI/order/place_quote.py index c7bbe6265..28865ff70 100644 --- a/SoftLayer/CLI/order/place_quote.py +++ b/SoftLayer/CLI/order/place_quote.py @@ -20,9 +20,9 @@ help="A custom name to be assigned to the quote (optional)") @click.option('--send-email', is_flag=True, - help="The quote will be sent to the email address associated.") -@click.option('--complex-type', help=("The complex type of the order. This typically begins" - " with 'SoftLayer_Container_Product_Order_'.")) + help="The quote will be sent to the email address associated with your user.") +@click.option('--complex-type', + help="The complex type of the order. Starts 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) @@ -31,7 +31,7 @@ def cli(env, package_keyname, location, preset, name, send_email, complex_type, extras, order_items): """Place a quote. - This CLI command is used for placing a quote of the specified package in + This CLI command is used for creating a quote 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_quote() with the same keynames. @@ -44,8 +44,9 @@ def cli(env, package_keyname, location, preset, name, send_email, complex_type, 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: + + Example:: + # Place quote a 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-quote --name "foobar" --send-email CLOUD_SERVER DALLAS13 \\ diff --git a/SoftLayer/CLI/order/preset_list.py b/SoftLayer/CLI/order/preset_list.py index a619caf77..7397f9428 100644 --- a/SoftLayer/CLI/order/preset_list.py +++ b/SoftLayer/CLI/order/preset_list.py @@ -20,19 +20,15 @@ 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. + .. Note:: + Presets are set CPU / RAM / Disk allotments. You still need to specify required items. + 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 diff --git a/SoftLayer/CLI/order/quote.py b/SoftLayer/CLI/order/quote.py new file mode 100644 index 000000000..498dbdc56 --- /dev/null +++ b/SoftLayer/CLI/order/quote.py @@ -0,0 +1,130 @@ +"""View and Order a quote""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers +from SoftLayer.managers import ImageManager as ImageManager +from SoftLayer.managers import ordering +from SoftLayer.managers import SshKeyManager as SshKeyManager + + +def _parse_create_args(client, args): + """Converts CLI arguments to args for VSManager.create_instance. + + :param dict args: CLI arguments + """ + data = {} + + if args.get('quantity'): + data['quantity'] = int(args.get('quantity')) + if args.get('postinstall'): + data['provisionScripts'] = [args.get('postinstall')] + if args.get('complex_type'): + data['complexType'] = args.get('complex_type') + + if args.get('fqdn'): + servers = [] + for name in args.get('fqdn'): + fqdn = name.split(".", 1) + servers.append({'hostname': fqdn[0], 'domain': fqdn[1]}) + data['hardware'] = servers + + if args.get('image'): + if args.get('image').isdigit(): + image_mgr = ImageManager(client) + image_details = image_mgr.get_image(args.get('image'), mask="id,globalIdentifier") + data['imageTemplateGlobalIdentifier'] = image_details['globalIdentifier'] + else: + data['imageTemplateGlobalIdentifier'] = args['image'] + + userdata = None + if args.get('userdata'): + userdata = args['userdata'] + elif args.get('userfile'): + with open(args['userfile'], 'r') as userfile: + userdata = userfile.read() + if userdata: + for hardware in data['hardware']: + hardware['userData'] = [{'value': userdata}] + + # Get the SSH keys + if args.get('key'): + keys = [] + for key in args.get('key'): + resolver = SshKeyManager(client).resolve_ids + key_id = helpers.resolve_id(resolver, key, 'SshKey') + keys.append(key_id) + data['sshKeys'] = keys + + return data + + +@click.command() +@click.argument('quote') +@click.option('--verify', is_flag=True, default=False, show_default=True, + help="If specified, will only show what the quote will order, will NOT place an order") +@click.option('--quantity', type=int, default=None, + help="The quantity of the item being ordered if different from quoted value") +@click.option('--complex-type', default='SoftLayer_Container_Product_Order_Hardware_Server', show_default=True, + help=("The complex type of the order. Starts with 'SoftLayer_Container_Product_Order'.")) +@click.option('--userdata', '-u', help="User defined metadata string") +@click.option('--userfile', '-F', type=click.Path(exists=True, readable=True, resolve_path=True), + help="Read userdata from file") +@click.option('--postinstall', '-i', help="Post-install script to download") +@helpers.multi_option('--key', '-k', help="SSH keys to add to the root user") +@helpers.multi_option('--fqdn', required=True, + help=". formatted name to use. Specify one fqdn per server") +@click.option('--image', help="Image ID. See: 'slcli image list' for reference") +@environment.pass_env +def cli(env, quote, **args): + """View and Order a quote + + \f + :note: + The hostname and domain are split out from the fully qualified domain name. + + If you want to order multiple servers, you need to specify each FQDN. Postinstall, userdata, and + sshkeys are applied to all servers in an order. + + :: + + slcli order quote 12345 --fqdn testing.tester.com \\ + --complex-type SoftLayer_Container_Product_Order_Virtual_Guest -k sshKeyNameLabel\\ + -i https://domain.com/runthis.sh --userdata DataGoesHere + + """ + table = formatting.Table([ + 'Id', 'Name', 'Created', 'Expiration', 'Status' + ]) + create_args = _parse_create_args(env.client, args) + + manager = ordering.OrderingManager(env.client) + quote_details = manager.get_quote_details(quote) + + package = quote_details['order']['items'][0]['package'] + create_args['packageId'] = package['id'] + + if args.get('verify'): + result = manager.verify_quote(quote, create_args) + verify_table = formatting.Table(['keyName', 'description', 'cost']) + verify_table.align['keyName'] = 'l' + verify_table.align['description'] = 'l' + for price in result['prices']: + cost_key = 'hourlyRecurringFee' if result['useHourlyPricing'] is True else 'recurringFee' + verify_table.add_row([ + price['item']['keyName'], + price['item']['description'], + price[cost_key] if cost_key in price else formatting.blank() + ]) + env.fout(verify_table) + else: + result = manager.order_quote(quote, create_args) + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['id', result['orderId']]) + table.add_row(['created', result['orderDate']]) + table.add_row(['status', result['placedOrder']['status']]) + env.fout(table) diff --git a/SoftLayer/CLI/order/quote_detail.py b/SoftLayer/CLI/order/quote_detail.py new file mode 100644 index 000000000..cef5f41e5 --- /dev/null +++ b/SoftLayer/CLI/order/quote_detail.py @@ -0,0 +1,38 @@ +"""View a quote""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering +from SoftLayer.utils import lookup + + +@click.command() +@click.argument('quote') +@environment.pass_env +def cli(env, quote): + """View a quote""" + + manager = ordering.OrderingManager(env.client) + result = manager.get_quote_details(quote) + + package = result['order']['items'][0]['package'] + title = "{} - Package: {}, Id {}".format(result.get('name'), package['keyName'], package['id']) + table = formatting.Table([ + 'Category', 'Description', 'Quantity', 'Recurring', 'One Time' + ], title=title) + table.align['Category'] = 'l' + table.align['Description'] = 'l' + + items = lookup(result, 'order', 'items') + for item in items: + table.add_row([ + item.get('categoryCode'), + item.get('description'), + item.get('quantity'), + item.get('recurringFee'), + item.get('oneTimeFee') + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/order/quote_list.py b/SoftLayer/CLI/order/quote_list.py new file mode 100644 index 000000000..c43ff7542 --- /dev/null +++ b/SoftLayer/CLI/order/quote_list.py @@ -0,0 +1,36 @@ +"""List active quotes on an account.""" +# :license: MIT, see LICENSE for more details. +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers import ordering +from SoftLayer.utils import clean_time + + +@click.command() +@environment.pass_env +def cli(env): + """List all active quotes on an account""" + table = formatting.Table([ + 'Id', 'Name', 'Created', 'Expiration', 'Status', 'Package Name', 'Package Id' + ]) + table.align['Name'] = 'l' + table.align['Package Name'] = 'r' + table.align['Package Id'] = 'l' + + manager = ordering.OrderingManager(env.client) + items = manager.get_quotes() + + for item in items: + package = item['order']['items'][0]['package'] + table.add_row([ + item.get('id'), + item.get('name'), + clean_time(item.get('createDate')), + clean_time(item.get('modifyDate')), + item.get('status'), + package.get('keyName'), + package.get('id') + ]) + env.fout(table) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 97551aeff..81ba46672 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -83,15 +83,13 @@ ('block:replica-failover', 'SoftLayer.CLI.block.replication.failover:cli'), ('block:replica-order', 'SoftLayer.CLI.block.replication.order:cli'), ('block:replica-partners', 'SoftLayer.CLI.block.replication.partners:cli'), - ('block:replica-locations', - 'SoftLayer.CLI.block.replication.locations:cli'), + ('block:replica-locations', 'SoftLayer.CLI.block.replication.locations:cli'), ('block:snapshot-cancel', 'SoftLayer.CLI.block.snapshot.cancel:cli'), ('block:snapshot-create', 'SoftLayer.CLI.block.snapshot.create:cli'), ('block:snapshot-delete', 'SoftLayer.CLI.block.snapshot.delete:cli'), ('block:snapshot-disable', 'SoftLayer.CLI.block.snapshot.disable:cli'), ('block:snapshot-enable', 'SoftLayer.CLI.block.snapshot.enable:cli'), - ('block:snapshot-schedule-list', - 'SoftLayer.CLI.block.snapshot.schedule_list:cli'), + ('block:snapshot-schedule-list', 'SoftLayer.CLI.block.snapshot.schedule_list:cli'), ('block:snapshot-list', 'SoftLayer.CLI.block.snapshot.list:cli'), ('block:snapshot-order', 'SoftLayer.CLI.block.snapshot.order:cli'), ('block:snapshot-restore', 'SoftLayer.CLI.block.snapshot.restore:cli'), @@ -122,8 +120,7 @@ ('file:snapshot-delete', 'SoftLayer.CLI.file.snapshot.delete:cli'), ('file:snapshot-disable', 'SoftLayer.CLI.file.snapshot.disable:cli'), ('file:snapshot-enable', 'SoftLayer.CLI.file.snapshot.enable:cli'), - ('file:snapshot-schedule-list', - 'SoftLayer.CLI.file.snapshot.schedule_list:cli'), + ('file:snapshot-schedule-list', 'SoftLayer.CLI.file.snapshot.schedule_list:cli'), ('file:snapshot-list', 'SoftLayer.CLI.file.snapshot.list:cli'), ('file:snapshot-order', 'SoftLayer.CLI.file.snapshot.order:cli'), ('file:snapshot-restore', 'SoftLayer.CLI.file.snapshot.restore:cli'), @@ -164,10 +161,8 @@ ('ipsec:subnet-add', 'SoftLayer.CLI.vpn.ipsec.subnet.add:cli'), ('ipsec:subnet-remove', 'SoftLayer.CLI.vpn.ipsec.subnet.remove:cli'), ('ipsec:translation-add', 'SoftLayer.CLI.vpn.ipsec.translation.add:cli'), - ('ipsec:translation-remove', - 'SoftLayer.CLI.vpn.ipsec.translation.remove:cli'), - ('ipsec:translation-update', - 'SoftLayer.CLI.vpn.ipsec.translation.update:cli'), + ('ipsec:translation-remove', 'SoftLayer.CLI.vpn.ipsec.translation.remove:cli'), + ('ipsec:translation-update', 'SoftLayer.CLI.vpn.ipsec.translation.update:cli'), ('ipsec:update', 'SoftLayer.CLI.vpn.ipsec.update:cli'), ('loadbal', 'SoftLayer.CLI.loadbal'), @@ -195,12 +190,10 @@ ('nas:credentials', 'SoftLayer.CLI.nas.credentials:cli'), ('object-storage', 'SoftLayer.CLI.object_storage'), - ('object-storage:accounts', - 'SoftLayer.CLI.object_storage.list_accounts:cli'), - ('object-storage:endpoints', - 'SoftLayer.CLI.object_storage.list_endpoints:cli'), - ('object-storage:credential', - 'SoftLayer.CLI.object_storage.credential:cli'), + + ('object-storage:accounts', 'SoftLayer.CLI.object_storage.list_accounts:cli'), + ('object-storage:endpoints', 'SoftLayer.CLI.object_storage.list_endpoints:cli'), + ('object-storage:credential', 'SoftLayer.CLI.object_storage.credential:cli'), ('order', 'SoftLayer.CLI.order'), ('order:category-list', 'SoftLayer.CLI.order.category_list:cli'), @@ -210,6 +203,9 @@ ('order:preset-list', 'SoftLayer.CLI.order.preset_list:cli'), ('order:package-locations', 'SoftLayer.CLI.order.package_locations:cli'), ('order:place-quote', 'SoftLayer.CLI.order.place_quote:cli'), + ('order:quote-list', 'SoftLayer.CLI.order.quote_list:cli'), + ('order:quote-detail', 'SoftLayer.CLI.order.quote_detail:cli'), + ('order:quote', 'SoftLayer.CLI.order.quote:cli'), ('rwhois', 'SoftLayer.CLI.rwhois'), ('rwhois:edit', 'SoftLayer.CLI.rwhois.edit:cli'), diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index ffe556dee..cf884aefd 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -489,9 +489,38 @@ }] getActiveQuotes = [{ + 'accountId': 1234, 'id': 1234, 'name': 'TestQuote1234', 'quoteKey': '1234test4321', + 'createDate': '2019-04-10T14:26:03-06:00', + 'modifyDate': '2019-04-10T14:26:03-06:00', + 'order': { + 'id': 37623333, + 'items': [ + { + 'categoryCode': 'guest_core', + 'description': '4 x 2.0 GHz or higher Cores', + 'id': 468394713, + 'itemId': 859, + 'itemPriceId': '1642', + 'oneTimeAfterTaxAmount': '0', + 'oneTimeFee': '0', + 'oneTimeFeeTaxRate': '0', + 'oneTimeTaxAmount': '0', + 'quantity': 1, + 'recurringAfterTaxAmount': '0', + 'recurringFee': '0', + 'recurringTaxAmount': '0', + 'setupAfterTaxAmount': '0', + 'setupFee': '0', + 'setupFeeDeferralMonths': None, + 'setupFeeTaxRate': '0', + 'setupTaxAmount': '0', + 'package': {'id': 46, 'keyName': 'CLOUD_SERVER'} + }, + ] + } }] getOrders = [{ diff --git a/SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py b/SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py index 6302bfa94..f1ca8c497 100644 --- a/SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py +++ b/SoftLayer/fixtures/SoftLayer_Billing_Order_Quote.py @@ -3,16 +3,102 @@ 'id': 1234, 'name': 'TestQuote1234', 'quoteKey': '1234test4321', + 'order': { + 'id': 37623333, + 'items': [ + { + 'categoryCode': 'guest_core', + 'description': '4 x 2.0 GHz or higher Cores', + 'id': 468394713, + 'itemId': 859, + 'itemPriceId': '1642', + 'oneTimeAfterTaxAmount': '0', + 'oneTimeFee': '0', + 'oneTimeFeeTaxRate': '0', + 'oneTimeTaxAmount': '0', + 'quantity': 1, + 'recurringAfterTaxAmount': '0', + 'recurringFee': '0', + 'recurringTaxAmount': '0', + 'setupAfterTaxAmount': '0', + 'setupFee': '0', + 'setupFeeDeferralMonths': None, + 'setupFeeTaxRate': '0', + 'setupTaxAmount': '0', + 'package': {'id': 46, 'keyName': 'CLOUD_SERVER'} + }, + ] + } } getRecalculatedOrderContainer = { - 'orderContainers': [{ - 'presetId': '', + 'presetId': '', + 'prices': [{ + 'id': 1921 + }], + 'quantity': 1, + 'packageId': 50, + 'useHourlyPricing': '', + 'reservedCapacityId': '', + +} + +verifyOrder = { + 'orderId': 1234, + 'orderDate': '2013-08-01 15:23:45', + 'useHourlyPricing': False, + 'prices': [{ + 'id': 1, + 'laborFee': '2', + 'oneTimeFee': '2', + 'oneTimeFeeTax': '.1', + 'quantity': 1, + 'recurringFee': '2', + 'recurringFeeTax': '.1', + 'hourlyRecurringFee': '2', + 'setupFee': '1', + 'item': {'id': 1, 'description': 'this is a thing', 'keyName': 'TheThing'}, + }]} + +placeOrder = { + 'orderId': 1234, + 'orderDate': '2013-08-01 15:23:45', + 'orderDetails': { 'prices': [{ - 'id': 1921 + 'id': 1, + 'laborFee': '2', + 'oneTimeFee': '2', + 'oneTimeFeeTax': '.1', + 'quantity': 1, + 'recurringFee': '2', + 'recurringFeeTax': '.1', + 'hourlyRecurringFee': '2', + 'setupFee': '1', + 'item': {'id': 1, 'description': 'this is a thing'}, }], - 'quantity': 1, - 'packageId': 50, - 'useHourlyPricing': '', - }], + 'virtualGuests': [{ + 'id': 1234567, + 'globalIdentifier': '1a2b3c-1701', + 'fullyQualifiedDomainName': 'test.guest.com' + }], + }, + 'placedOrder': { + 'id': 37985543, + 'orderQuoteId': 2639077, + 'orderTypeId': 4, + 'status': 'PENDING_AUTO_APPROVAL', + 'items': [ + { + 'categoryCode': 'guest_core', + 'description': '4 x 2.0 GHz or higher Cores', + 'id': 472527133, + 'itemId': 859, + 'itemPriceId': '1642', + 'laborFee': '0', + 'oneTimeFee': '0', + 'recurringFee': '0', + 'setupFee': '0', + } + ] + } } diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index 44f2fcab7..e20378914 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -11,6 +11,7 @@ from SoftLayer import exceptions + CATEGORY_MASK = '''id, isRequired, itemCategory[id, name, categoryCode] @@ -132,12 +133,12 @@ def get_package_id_by_type(self, package_type): raise ValueError("No package found for type: " + package_type) def get_quotes(self): - """Retrieve a list of quotes. + """Retrieve a list of active quotes. :returns: a list of SoftLayer_Billing_Order_Quote """ - - quotes = self.client['Account'].getActiveQuotes() + mask = "mask[order[id,items[id,package[id,keyName]]]]" + quotes = self.client['Account'].getActiveQuotes(mask=mask) return quotes def get_quote_details(self, quote_id): @@ -146,7 +147,8 @@ def get_quote_details(self, quote_id): :param quote_id: ID number of target quote """ - quote = self.client['Billing_Order_Quote'].getObject(id=quote_id) + mask = "mask[order[id,items[package[id,keyName]]]]" + quote = self.client['Billing_Order_Quote'].getObject(id=quote_id, mask=mask) return quote def get_order_container(self, quote_id): @@ -157,62 +159,75 @@ def get_order_container(self, quote_id): quote = self.client['Billing_Order_Quote'] container = quote.getRecalculatedOrderContainer(id=quote_id) - return container['orderContainers'][0] + return container def generate_order_template(self, quote_id, extra, quantity=1): """Generate a complete order template. :param int quote_id: ID of target quote - :param list extra: List of dictionaries that have extra details about - the order such as hostname or domain names for - virtual servers or hardware nodes - :param int quantity: Number of ~things~ to order + :param dictionary extra: Overrides for the defaults of SoftLayer_Container_Product_Order + :param int quantity: Number of items to order. """ - container = self.get_order_container(quote_id) - container['quantity'] = quantity + if not isinstance(extra, dict): + raise ValueError("extra is not formatted properly") - # NOTE(kmcdonald): This will only work with virtualGuests and hardware. - # There has to be a better way, since this is based on - # an existing quote that supposedly knows about this - # detail - if container['packageId'] == 46: - product_type = 'virtualGuests' - else: - product_type = 'hardware' + container = self.get_order_container(quote_id) - if len(extra) != quantity: - raise ValueError("You must specify extra for each server in the quote") + container['quantity'] = quantity + for key in extra.keys(): + container[key] = extra[key] - container[product_type] = [] - for extra_details in extra: - container[product_type].append(extra_details) - container['presetId'] = None return container - def verify_quote(self, quote_id, extra, quantity=1): + def verify_quote(self, quote_id, extra): """Verifies that a quote order is valid. + :: + + extras = { + 'hardware': {'hostname': 'test', 'domain': 'testing.com'}, + 'quantity': 2 + } + manager = ordering.OrderingManager(env.client) + result = manager.verify_quote(12345, extras) + + :param int quote_id: ID for the target quote - :param list hostnames: hostnames of the servers - :param string domain: domain of the new servers + :param dictionary extra: Overrides for the defaults of SoftLayer_Container_Product_Order :param int quantity: Quantity to override default """ + container = self.generate_order_template(quote_id, extra) + clean_container = {} - container = self.generate_order_template(quote_id, extra, quantity=quantity) - return self.order_svc.verifyOrder(container) + # There are a few fields that wil cause exceptions in the XML endpoing if you send in '' + # reservedCapacityId and hostId specifically. But we clean all just to be safe. + # This for some reason is only a problem on verify_quote. + for key in container.keys(): + if container.get(key) != '': + clean_container[key] = container[key] - def order_quote(self, quote_id, extra, quantity=1): + return self.client.call('SoftLayer_Billing_Order_Quote', 'verifyOrder', clean_container, id=quote_id) + + def order_quote(self, quote_id, extra): """Places an order using a quote + :: + + extras = { + 'hardware': {'hostname': 'test', 'domain': 'testing.com'}, + 'quantity': 2 + } + manager = ordering.OrderingManager(env.client) + result = manager.order_quote(12345, extras) + :param int quote_id: ID for the target quote - :param list hostnames: hostnames of the servers - :param string domain: domain of the new server + :param dictionary extra: Overrides for the defaults of SoftLayer_Container_Product_Order :param int quantity: Quantity to override default """ - container = self.generate_order_template(quote_id, extra, quantity=quantity) - return self.order_svc.placeOrder(container) + container = self.generate_order_template(quote_id, extra) + return self.client.call('SoftLayer_Billing_Order_Quote', 'placeOrder', container, id=quote_id) def get_package_by_key(self, package_keyname, mask=None): """Get a single package with a given key. diff --git a/docs/api/client.rst b/docs/api/client.rst index 6c447bead..9fc13c1e7 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -86,9 +86,9 @@ offsets, and retrieving objects by id. The following section assumes you have an initialized client named 'client'. The best way to test our setup is to call the -`getObject `_ +`getObject `_ method on the -`SoftLayer_Account `_ +`SoftLayer_Account `_ service. :: @@ -97,7 +97,7 @@ service. For a more complex example we'll retrieve a support ticket with id 123456 along with the ticket's updates, the user it's assigned to, the servers attached to it, and the datacenter those servers are in. To retrieve our extra information -using an `object mask `_. +using an `object mask `_. Retrieve a ticket using object masks. :: @@ -106,22 +106,28 @@ Retrieve a ticket using object masks. id=123456, mask="updates, assignedUser, attachedHardware.datacenter") -Now add an update to the ticket with -`Ticket.addUpdate `_. +Now add an update to the ticket with `Ticket.addUpdate `_. This uses a parameter, which translate to positional arguments in the order that they appear in the API docs. + + :: update = client.call('Ticket', 'addUpdate', {'entry' : 'Hello!'}, id=123456) Let's get a listing of virtual guests using the domain example.com + + :: client.call('Account', 'getVirtualGuests', filter={'virtualGuests': {'domain': {'operation': 'example.com'}}}) -This call gets tickets created between the beginning of March 1, 2013 and -March 15, 2013. +This call gets tickets created between the beginning of March 1, 2013 and March 15, 2013. +More information on `Object Filters `_. + +:NOTE: The `value` field for startDate and endDate is in `[]`, if you do not put the date in brackets the filter will not work. + :: client.call('Account', 'getTickets', @@ -141,14 +147,24 @@ March 15, 2013. SoftLayer's XML-RPC API also allows for pagination. :: - client.call('Account', 'getVirtualGuests', limit=10, offset=0) # Page 1 - client.call('Account', 'getVirtualGuests', limit=10, offset=10) # Page 2 + from pprint import pprint + + page1 = client.call('Account', 'getVirtualGuests', limit=10, offset=0) # Page 1 + page2 = client.call('Account', 'getVirtualGuests', limit=10, offset=10) # Page 2 + + #Automatic Pagination (v5.5.3+), default limit is 100 + result = client.call('Account', 'getVirtualGuests', iter=True, limit=10) + pprint(result) + + # Using a python generator, default limit is 100 + results = client.iter_call('Account', 'getVirtualGuests', limit=10) + for result in results: + pprint(result) - #Automatic Pagination (v5.5.3+) - client.call('Account', 'getVirtualGuests', iter=True) # Page 2 +:NOTE: `client.call(iter=True)` will pull all results, then return. `client.iter_call()` will return a generator, and only make API calls as you iterate over the results. Here's how to create a new Cloud Compute Instance using -`SoftLayer_Virtual_Guest.createObject `_. +`SoftLayer_Virtual_Guest.createObject `_. Be warned, this call actually creates an hourly virtual server so this will have billing implications. :: diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst index 5b3f480b1..f6bb7e488 100644 --- a/docs/cli/hardware.rst +++ b/docs/cli/hardware.rst @@ -86,9 +86,6 @@ This function updates the firmware of a server. If already at the latest version :show-nested: - :show-nested: - - .. click:: SoftLayer.CLI.hardware.ready:cli :prog: hw ready :show-nested: diff --git a/docs/cli/ordering.rst b/docs/cli/ordering.rst index 0724cb60f..acaf3a07e 100644 --- a/docs/cli/ordering.rst +++ b/docs/cli/ordering.rst @@ -1,7 +1,7 @@ .. _cli_order: Ordering -========== +======== The Order :ref:`cli` commands can be used to build an order for any product in the SoftLayer catalog. The basic flow for ordering goes something like this... @@ -11,11 +11,14 @@ The basic flow for ordering goes something like this... #. item-list #. place -.. _cli_ordering_package_list: -order package-list ------------------- -This command will list all of the packages that are available to be ordered. This is the starting point for placing any order. Find the package keyName you want to order, and use it for the next steps. + + + +.. click:: SoftLayer.CLI.order.package_list:cli + :prog: order package-list + :show-nested: + .. note:: * CLOUD_SERVER: These are Virtual Servers @@ -27,10 +30,15 @@ This command will list all of the packages that are available to be ordered. Thi Bluemix services listed here may still need to be ordered through the Bluemix CLI/Portal -.. _cli_ordering_category_list: -order category-list -------------------- +.. click:: SoftLayer.CLI.order.package_locations:cli + :prog: order package-locations + :show-nested: + +.. click:: SoftLayer.CLI.order.category_list:cli + :prog: order category-list + :show-nested: + Shows all the available categories for a certain package, useful in finding the required categories. Categories that are required will need to have a corresponding item included with any orders These are all the required categories for ``BARE_METAL_SERVER`` @@ -52,23 +60,22 @@ These are all the required categories for ``BARE_METAL_SERVER`` : VPN Management - Private Network : vpn_management : Y : :........................................:.......................:............: -.. _cli_ordering_item_list: +.. click:: SoftLayer.CLI.order.item_list:cli + :prog: order item-list + :show-nested: -order item-list ---------------- Shows all the prices for a given package. Collect all the items you want included on your server. Don't forget to include the required category items. If forgotten, ``order place`` will tell you about it. -.. _cli_ordering_preset_list: +.. click:: SoftLayer.CLI.order.preset_list:cli + :prog: order preset-list + :show-nested: -order preset-list ------------------ -Some packages have presets which makes ordering significantly simpler. These will have set CPU / RAM / Disk allotments. You still need to specify required items -.. _cli_ordering_place: +.. click:: SoftLayer.CLI.order.place:cli + :prog: order place + :show-nested: -order place ------------ Now that you have the package you want, the prices needed, and found a location, it is time to place an order. order place @@ -85,6 +92,7 @@ order place --extras '{"hardware": [{"hostname" : "testOrder", "domain": "cgallo.com"}]}' \ --complex-type SoftLayer_Container_Product_Order_Hardware_Server + order place ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -105,4 +113,25 @@ order place 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 \ No newline at end of file + --complex-type SoftLayer_Container_Product_Order_Virtual_Guest + + + +Quotes +====== +.. click:: SoftLayer.CLI.order.quote:cli + :prog: order quote + :show-nested: + + +.. click:: SoftLayer.CLI.order.quote_list:cli + :prog: order quote-list + :show-nested: + +.. click:: SoftLayer.CLI.order.quote_detail:cli + :prog: order quote-detail + :show-nested: + +.. click:: SoftLayer.CLI.order.place_quote:cli + :prog: order place-quote + :show-nested: diff --git a/tests/CLI/modules/order_tests.py b/tests/CLI/modules/order_tests.py index 02141e808..a82b731fc 100644 --- a/tests/CLI/modules/order_tests.py +++ b/tests/CLI/modules/order_tests.py @@ -4,6 +4,8 @@ :license: MIT, see LICENSE for more details. """ import json +import sys +import tempfile from SoftLayer.CLI import exceptions from SoftLayer import testing @@ -252,6 +254,12 @@ def test_preset_list(self): 'description': 'description3'}], json.loads(result.output)) + def test_preset_list_keywork(self): + result = self.run_command(['order', 'preset-list', 'package', '--keyword', 'testKeyWord']) + _filter = {'activePresets': {'name': {'operation': '*= testKeyWord'}}} + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Product_Package', 'getActivePresets', filter=_filter) + def test_location_list(self): result = self.run_command(['order', 'package-locations', 'package']) self.assert_no_fail(result) @@ -262,6 +270,102 @@ def test_location_list(self): print(result.output) self.assertEqual(expected_results, json.loads(result.output)) + def test_quote_verify(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + + def test_quote_verify_image(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--image', '1234', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Virtual_Guest_Block_Device_Template_Group', 'getObject', identifier='1234') + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual('0B5DEAF4-643D-46CA-A695-CECBE8832C9D', verify_args['imageTemplateGlobalIdentifier']) + + def test_quote_verify_image_guid(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--image', + '0B5DEAF4-643D-46CA-A695-CECBE8832C9D', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual('0B5DEAF4-643D-46CA-A695-CECBE8832C9D', verify_args['imageTemplateGlobalIdentifier']) + + def test_quote_verify_userdata(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--userdata', 'aaaa1234', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual([{'value': 'aaaa1234'}], verify_args['hardware'][0]['userData']) + + def test_quote_verify_userdata_file(self): + if (sys.platform.startswith("win")): + self.skipTest("TempFile tests doesn't work in Windows") + with tempfile.NamedTemporaryFile() as userfile: + userfile.write(b"some data") + userfile.flush() + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--userfile', userfile.name, + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual([{'value': 'some data'}], verify_args['hardware'][0]['userData']) + + def test_quote_verify_sshkey(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--key', 'Test 1', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + + self.assert_called_with('SoftLayer_Account', 'getSshKeys') + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual(['100'], verify_args['sshKeys']) + + def test_quote_verify_postinstall_others(self): + result = self.run_command([ + 'order', 'quote', '12345', '--verify', '--fqdn', 'test01.test.com', '--quantity', '2', + '--postinstall', 'https://127.0.0.1/test.sh', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder', identifier='12345') + verify_call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder') + verify_args = getattr(verify_call[0], 'args')[0] + self.assertEqual(['https://127.0.0.1/test.sh'], verify_args['provisionScripts']) + self.assertEqual(2, verify_args['quantity']) + + def test_quote_place(self): + result = self.run_command([ + 'order', 'quote', '12345', '--fqdn', 'test01.test.com', + '--complex-type', 'SoftLayer_Container_Product_Order_Virtual_Guest']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'placeOrder', identifier='12345') + + def test_quote_detail(self): + result = self.run_command(['order', 'quote-detail', '12345']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'getObject', identifier='12345') + + def test_quote_list(self): + result = self.run_command(['order', 'quote-list']) + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Account', 'getActiveQuotes') + def _get_order_items(self): item1 = {'keyName': 'ITEM1', 'description': 'description1', 'itemCategory': {'categoryCode': 'cat1'}, diff --git a/tests/managers/ordering_tests.py b/tests/managers/ordering_tests.py index 66eb2f405..40f9f5db3 100644 --- a/tests/managers/ordering_tests.py +++ b/tests/managers/ordering_tests.py @@ -90,9 +90,8 @@ def test_get_package_id_by_type_fails_for_nonexistent_package_type(self): def test_get_order_container(self): container = self.ordering.get_order_container(1234) - quote = self.ordering.client['Billing_Order_Quote'] - container_fixture = quote.getRecalculatedOrderContainer(id=1234) - self.assertEqual(container, container_fixture['orderContainers'][0]) + self.assertEqual(1, container['quantity']) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'getRecalculatedOrderContainer') def test_get_quotes(self): quotes = self.ordering.get_quotes() @@ -106,13 +105,16 @@ def test_get_quote_details(self): self.assertEqual(quote, quote_fixture) def test_verify_quote(self): - result = self.ordering.verify_quote(1234, - [{'hostname': 'test1', - 'domain': 'example.com'}], - quantity=1) + extras = { + 'hardware': [{ + 'hostname': 'test1', + 'domain': 'example.com' + }] + } + result = self.ordering.verify_quote(1234, extras) - self.assertEqual(result, fixtures.SoftLayer_Product_Order.verifyOrder) - self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + self.assertEqual(result, fixtures.SoftLayer_Billing_Order_Quote.verifyOrder) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder') def test_order_quote_virtual_guest(self): guest_quote = { @@ -126,38 +128,37 @@ def test_order_quote_virtual_guest(self): 'useHourlyPricing': '', }], } - + extras = { + 'hardware': [{ + 'hostname': 'test1', + 'domain': 'example.com' + }] + } mock = self.set_mock('SoftLayer_Billing_Order_Quote', 'getRecalculatedOrderContainer') mock.return_value = guest_quote - result = self.ordering.order_quote(1234, - [{'hostname': 'test1', - 'domain': 'example.com'}], - quantity=1) + result = self.ordering.order_quote(1234, extras) - self.assertEqual(result, fixtures.SoftLayer_Product_Order.placeOrder) - self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + self.assertEqual(result, fixtures.SoftLayer_Billing_Order_Quote.placeOrder) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'placeOrder') def test_generate_order_template(self): - result = self.ordering.generate_order_template( - 1234, [{'hostname': 'test1', 'domain': 'example.com'}], quantity=1) - self.assertEqual(result, {'presetId': None, - 'hardware': [{'domain': 'example.com', - 'hostname': 'test1'}], - 'useHourlyPricing': '', - 'packageId': 50, - 'prices': [{'id': 1921}], - 'quantity': 1}) + extras = {'hardware': [{'hostname': 'test1', 'domain': 'example.com'}]} + + result = self.ordering.generate_order_template(1234, extras, quantity=1) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'getRecalculatedOrderContainer') + self.assertEqual(result['hardware'][0]['domain'], 'example.com') def test_generate_order_template_virtual(self): - result = self.ordering.generate_order_template( - 1234, [{'hostname': 'test1', 'domain': 'example.com'}], quantity=1) - self.assertEqual(result, {'presetId': None, - 'hardware': [{'domain': 'example.com', - 'hostname': 'test1'}], - 'useHourlyPricing': '', - 'packageId': 50, - 'prices': [{'id': 1921}], - 'quantity': 1}) + extras = { + 'hardware': [{ + 'hostname': 'test1', + 'domain': 'example.com' + }], + 'testProperty': 100 + } + result = self.ordering.generate_order_template(1234, extras, quantity=1) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'getRecalculatedOrderContainer') + self.assertEqual(result['testProperty'], 100) def test_generate_order_template_extra_quantity(self): self.assertRaises(ValueError, @@ -654,3 +655,20 @@ def test_issues1067(self): package = 'DUAL_INTEL_XEON_PROCESSOR_SCALABLE_FAMILY_4_DRIVES' result = self.ordering.get_price_id_list(package, item_keynames, None) self.assertIn(201161, result) + + def test_clean_quote_verify(self): + extras = { + 'hardware': [{ + 'hostname': 'test1', + 'domain': 'example.com' + }], + 'testProperty': '' + } + result = self.ordering.verify_quote(1234, extras) + + self.assertEqual(result, fixtures.SoftLayer_Billing_Order_Quote.verifyOrder) + self.assert_called_with('SoftLayer_Billing_Order_Quote', 'verifyOrder') + call = self.calls('SoftLayer_Billing_Order_Quote', 'verifyOrder')[0] + order_container = call.args[0] + self.assertNotIn('testProperty', order_container) + self.assertNotIn('reservedCapacityId', order_container)