diff --git a/CHANGELOG.md b/CHANGELOG.md index e587211a6..7c408f0ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,31 @@ # Change Log - -## [5.5.1] - 2018-08-31 +## [5.6.0] - 2018-10-16 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.3...v5.6.0 + ++ #1026 Support for [Reserved Capacity](https://console.bluemix.net/docs/vsi/vsi_about_reserved.html#about-reserved-virtual-servers) + * `slcli vs capacity create` + * `slcli vs capacity create-guest` + * `slcli vs capacity create-options` + * `slcli vs capacity detail` + * `slcli vs capacity list` ++ #1050 Fix `post_uri` parameter name on docstring ++ #1039 Fixed suspend cloud server order. ++ #1055 Update to use click 7 ++ #1053 Add export/import capabilities to/from IBM Cloud Object Storage to the image manager as well as the slcli. + + +## [5.5.3] - 2018-08-31 +- Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.2...v5.5.3 + ++ Added `slcli user delete` ++ #1023 Added `slcli order quote` to let users create a quote from the slcli. ++ #1032 Fixed vs upgrades when using flavors. ++ #1034 Added pagination to ticket list commands ++ #1037 Fixed DNS manager to be more flexible and support more zone types. ++ #1044 Pinned Click library version at >=5 < 7 + +## [5.5.2] - 2018-08-31 - Changes: https://github.com/softlayer/softlayer-python/compare/v5.5.1...v5.5.2 + #1018 Fixed hardware credentials. diff --git a/SoftLayer/CLI/columns.py b/SoftLayer/CLI/columns.py index 50bf89763..8cfdf0bd7 100644 --- a/SoftLayer/CLI/columns.py +++ b/SoftLayer/CLI/columns.py @@ -57,7 +57,7 @@ def mask(self): def get_formatter(columns): """This function returns a callback to use with click options. - The retuend function parses a comma-separated value and returns a new + The returned function parses a comma-separated value and returns a new ColumnFormatter. :param columns: a list of Column instances diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py index a02bf65a4..a05ffaa54 100644 --- a/SoftLayer/CLI/core.py +++ b/SoftLayer/CLI/core.py @@ -137,7 +137,7 @@ def cli(env, @cli.resultcallback() @environment.pass_env -def output_diagnostics(env, verbose=0, **kwargs): +def output_diagnostics(env, result, verbose=0, **kwargs): """Output diagnostic information.""" if verbose > 0: diff --git a/SoftLayer/CLI/image/export.py b/SoftLayer/CLI/image/export.py index 327cef475..375de7842 100644 --- a/SoftLayer/CLI/image/export.py +++ b/SoftLayer/CLI/image/export.py @@ -12,17 +12,25 @@ @click.command() @click.argument('identifier') @click.argument('uri') +@click.option('--ibm-api-key', + default=None, + help="The IBM Cloud API Key with access to IBM Cloud Object " + "Storage instance. For help creating this key see " + "https://console.bluemix.net/docs/services/cloud-object-" + "storage/iam/users-serviceids.html#serviceidapikeys") @environment.pass_env -def cli(env, identifier, uri): +def cli(env, identifier, uri, ibm_api_key): """Export an image to object storage. The URI for an object storage object (.vhd/.iso file) of the format: swift://@// + or cos://// if using IBM Cloud + Object Storage """ image_mgr = SoftLayer.ImageManager(env.client) image_id = helpers.resolve_id(image_mgr.resolve_ids, identifier, 'image') - result = image_mgr.export_image_to_uri(image_id, uri) + result = image_mgr.export_image_to_uri(image_id, uri, ibm_api_key) if not result: raise exceptions.CLIAbort("Failed to export Image") diff --git a/SoftLayer/CLI/image/import.py b/SoftLayer/CLI/image/import.py index 03ec25acb..525564416 100644 --- a/SoftLayer/CLI/image/import.py +++ b/SoftLayer/CLI/image/import.py @@ -16,15 +16,44 @@ default="", help="The note to be applied to the imported template") @click.option('--os-code', - default="", help="The referenceCode of the operating system software" - " description for the imported VHD") + " description for the imported VHD, ISO, or RAW image") +@click.option('--ibm-api-key', + default=None, + help="The IBM Cloud API Key with access to IBM Cloud Object " + "Storage instance and IBM KeyProtect instance. For help " + "creating this key see https://console.bluemix.net/docs/" + "services/cloud-object-storage/iam/users-serviceids.html" + "#serviceidapikeys") +@click.option('--root-key-id', + default=None, + help="ID of the root key in Key Protect") +@click.option('--wrapped-dek', + default=None, + help="Wrapped Data Encryption Key provided by IBM KeyProtect. " + "For more info see https://console.bluemix.net/docs/" + "services/key-protect/wrap-keys.html#wrap-keys") +@click.option('--kp-id', + default=None, + help="ID of the IBM Key Protect Instance") +@click.option('--cloud-init', + is_flag=True, + help="Specifies if image is cloud-init") +@click.option('--byol', + is_flag=True, + help="Specifies if image is bring your own license") +@click.option('--is-encrypted', + is_flag=True, + help="Specifies if image is encrypted") @environment.pass_env -def cli(env, name, note, os_code, uri): +def cli(env, name, note, os_code, uri, ibm_api_key, root_key_id, wrapped_dek, + kp_id, cloud_init, byol, is_encrypted): """Import an image. The URI for an object storage object (.vhd/.iso file) of the format: swift://@// + or cos://// if using IBM Cloud + Object Storage """ image_mgr = SoftLayer.ImageManager(env.client) @@ -33,6 +62,13 @@ def cli(env, name, note, os_code, uri): note=note, os_code=os_code, uri=uri, + ibm_api_key=ibm_api_key, + root_key_id=root_key_id, + wrapped_dek=wrapped_dek, + kp_id=kp_id, + cloud_init=cloud_init, + byol=byol, + is_encrypted=is_encrypted ) if not result: diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index b1c9fb0e2..fa46f36ac 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -30,6 +30,7 @@ ('virtual:reload', 'SoftLayer.CLI.virt.reload:cli'), ('virtual:upgrade', 'SoftLayer.CLI.virt.upgrade:cli'), ('virtual:credentials', 'SoftLayer.CLI.virt.credentials:cli'), + ('virtual:capacity', 'SoftLayer.CLI.virt.capacity:cli'), ('dedicatedhost', 'SoftLayer.CLI.dedicatedhost'), ('dedicatedhost:list', 'SoftLayer.CLI.dedicatedhost.list:cli'), diff --git a/SoftLayer/CLI/virt/capacity/__init__.py b/SoftLayer/CLI/virt/capacity/__init__.py new file mode 100644 index 000000000..2b10885df --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/__init__.py @@ -0,0 +1,48 @@ +"""Manages Reserved Capacity.""" +# :license: MIT, see LICENSE for more details. + +import importlib +import os + +import click + +CONTEXT = {'help_option_names': ['-h', '--help'], + 'max_content_width': 999} + + +class CapacityCommands(click.MultiCommand): + """Loads module for capacity related commands. + + Will automatically replace _ with - where appropriate. + I'm not sure if this is better or worse than using a long list of manual routes, so I'm trying it here. + CLI/virt/capacity/create_guest.py -> slcli vs capacity create-guest + """ + + def __init__(self, **attrs): + click.MultiCommand.__init__(self, **attrs) + self.path = os.path.dirname(__file__) + + def list_commands(self, ctx): + """List all sub-commands.""" + commands = [] + for filename in os.listdir(self.path): + if filename == '__init__.py': + continue + if filename.endswith('.py'): + commands.append(filename[:-3].replace("_", "-")) + commands.sort() + return commands + + def get_command(self, ctx, cmd_name): + """Get command for click.""" + path = "%s.%s" % (__name__, cmd_name) + path = path.replace("-", "_") + module = importlib.import_module(path) + return getattr(module, 'cli') + + +# Required to get the sub-sub-sub command to work. +@click.group(cls=CapacityCommands, context_settings=CONTEXT) +def cli(): + """Base command for all capacity related concerns""" + pass diff --git a/SoftLayer/CLI/virt/capacity/create.py b/SoftLayer/CLI/virt/capacity/create.py new file mode 100644 index 000000000..92da7745c --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/create.py @@ -0,0 +1,51 @@ +"""Create a Reserved Capacity instance.""" + +import click + + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + + +@click.command(epilog=click.style("""WARNING: Reserved Capacity is on a yearly contract""" + """ and not cancelable until the contract is expired.""", fg='red')) +@click.option('--name', '-n', required=True, prompt=True, + help="Name for your new reserved capacity") +@click.option('--backend_router_id', '-b', required=True, prompt=True, type=int, + help="backendRouterId, create-options has a list of valid ids to use.") +@click.option('--flavor', '-f', required=True, prompt=True, + help="Capacity keyname (C1_2X2_1_YEAR_TERM for example).") +@click.option('--instances', '-i', required=True, prompt=True, type=int, + help="Number of VSI instances this capacity reservation can support.") +@click.option('--test', is_flag=True, + help="Do not actually create the virtual server") +@environment.pass_env +def cli(env, name, backend_router_id, flavor, instances, test=False): + """Create a Reserved Capacity instance. + + *WARNING*: Reserved Capacity is on a yearly contract and not cancelable until the contract is expired. + """ + manager = CapacityManager(env.client) + + result = manager.create( + name=name, + backend_router_id=backend_router_id, + flavor=flavor, + instances=instances, + test=test) + if test: + table = formatting.Table(['Name', 'Value'], "Test Order") + container = result['orderContainers'][0] + table.add_row(['Name', container['name']]) + table.add_row(['Location', container['locationObject']['longName']]) + for price in container['prices']: + table.add_row(['Contract', price['item']['description']]) + table.add_row(['Hourly Total', result['postTaxRecurring']]) + else: + table = formatting.Table(['Name', 'Value'], "Reciept") + table.add_row(['Order Date', result['orderDate']]) + table.add_row(['Order ID', result['orderId']]) + table.add_row(['status', result['placedOrder']['status']]) + table.add_row(['Hourly Total', result['orderDetails']['postTaxRecurring']]) + env.fout(table) diff --git a/SoftLayer/CLI/virt/capacity/create_guest.py b/SoftLayer/CLI/virt/capacity/create_guest.py new file mode 100644 index 000000000..854fe3f94 --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/create_guest.py @@ -0,0 +1,63 @@ +"""List Reserved Capacity""" +# :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.CLI.virt.create import _parse_create_args as _parse_create_args +from SoftLayer.CLI.virt.create import _update_with_like_args as _update_with_like_args +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + + +@click.command() +@click.option('--capacity-id', type=click.INT, help="Reserve capacity Id to provision this guest into.") +@click.option('--primary-disk', type=click.Choice(['25', '100']), default='25', help="Size of the main drive.") +@click.option('--hostname', '-H', required=True, prompt=True, help="Host portion of the FQDN.") +@click.option('--domain', '-D', required=True, prompt=True, help="Domain portion of the FQDN.") +@click.option('--os', '-o', help="OS install code. Tip: you can specify _LATEST.") +@click.option('--image', help="Image ID. See: 'slcli image list' for reference.") +@click.option('--boot-mode', type=click.STRING, + help="Specify the mode to boot the OS in. Supported modes are HVM and PV.") +@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('--disk', help="Additional disk sizes.") +@click.option('--private', is_flag=True, help="Forces the VS to only have access the private network.") +@click.option('--like', is_eager=True, callback=_update_with_like_args, + help="Use the configuration from an existing VS.") +@click.option('--network', '-n', help="Network port speed in Mbps.") +@helpers.multi_option('--tag', '-g', help="Tags to add to the instance.") +@click.option('--userdata', '-u', help="User defined metadata string.") +@click.option('--ipv6', is_flag=True, help="Adds an IPv6 address to this guest") +@click.option('--test', is_flag=True, + help="Test order, will return the order container, but not actually order a server.") +@environment.pass_env +def cli(env, **args): + """Allows for creating a virtual guest in a reserved capacity.""" + create_args = _parse_create_args(env.client, args) + if args.get('ipv6'): + create_args['ipv6'] = True + create_args['primary_disk'] = args.get('primary_disk') + manager = CapacityManager(env.client) + capacity_id = args.get('capacity_id') + test = args.get('test') + + result = manager.create_guest(capacity_id, test, create_args) + + env.fout(_build_receipt(result, test)) + + +def _build_receipt(result, test=False): + title = "OrderId: %s" % (result.get('orderId', 'No order placed')) + table = formatting.Table(['Item Id', 'Description'], title=title) + table.align['Description'] = 'l' + + if test: + prices = result['prices'] + else: + prices = result['orderDetails']['prices'] + + for item in prices: + table.add_row([item['id'], item['item']['description']]) + return table diff --git a/SoftLayer/CLI/virt/capacity/create_options.py b/SoftLayer/CLI/virt/capacity/create_options.py new file mode 100644 index 000000000..14203cb48 --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/create_options.py @@ -0,0 +1,45 @@ +"""List options for creating Reserved Capacity""" +# :license: MIT, see LICENSE for more details. + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + + +@click.command() +@environment.pass_env +def cli(env): + """List options for creating Reserved Capacity""" + manager = CapacityManager(env.client) + items = manager.get_create_options() + + items.sort(key=lambda term: int(term['capacity'])) + table = formatting.Table(["KeyName", "Description", "Term", "Default Hourly Price Per Instance"], + title="Reserved Capacity Options") + table.align["Hourly Price"] = "l" + table.align["Description"] = "l" + table.align["KeyName"] = "l" + for item in items: + table.add_row([ + item['keyName'], item['description'], item['capacity'], get_price(item) + ]) + env.fout(table) + + regions = manager.get_available_routers() + location_table = formatting.Table(['Location', 'POD', 'BackendRouterId'], 'Orderable Locations') + for region in regions: + for location in region['locations']: + for pod in location['location']['pods']: + location_table.add_row([region['keyname'], pod['backendRouterName'], pod['backendRouterId']]) + env.fout(location_table) + + +def get_price(item): + """Finds the price with the default locationGroupId""" + the_price = "No Default Pricing" + for price in item.get('prices', []): + if not price.get('locationGroupId'): + the_price = "%0.4f" % float(price['hourlyRecurringFee']) + return the_price diff --git a/SoftLayer/CLI/virt/capacity/detail.py b/SoftLayer/CLI/virt/capacity/detail.py new file mode 100644 index 000000000..60dc644f8 --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/detail.py @@ -0,0 +1,57 @@ +"""Shows the details of a reserved capacity group""" + +import click + +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + +COLUMNS = [ + column_helper.Column('Id', ('id',)), + column_helper.Column('hostname', ('hostname',)), + column_helper.Column('domain', ('domain',)), + column_helper.Column('primary_ip', ('primaryIpAddress',)), + column_helper.Column('backend_ip', ('primaryBackendIpAddress',)), +] + +DEFAULT_COLUMNS = [ + 'id', + 'hostname', + 'domain', + 'primary_ip', + 'backend_ip' +] + + +@click.command(epilog="Once provisioned, virtual guests can be managed with the slcli vs commands") +@click.argument('identifier') +@click.option('--columns', + callback=column_helper.get_formatter(COLUMNS), + help='Columns to display. [options: %s]' + % ', '.join(column.name for column in COLUMNS), + default=','.join(DEFAULT_COLUMNS), + show_default=True) +@environment.pass_env +def cli(env, identifier, columns): + """Reserved Capacity Group details. Will show which guests are assigned to a reservation.""" + + manager = CapacityManager(env.client) + mask = """mask[instances[id,createDate,guestId,billingItem[id, description, recurringFee, category[name]], + guest[modifyDate,id, primaryBackendIpAddress, primaryIpAddress,domain, hostname]]]""" + result = manager.get_object(identifier, mask) + + try: + flavor = result['instances'][0]['billingItem']['description'] + except KeyError: + flavor = "Pending Approval..." + + table = formatting.Table(columns.columns, title="%s - %s" % (result.get('name'), flavor)) + # RCI = Reserved Capacity Instance + for rci in result['instances']: + guest = rci.get('guest', None) + if guest is not None: + table.add_row([value or formatting.blank() for value in columns.row(guest)]) + else: + table.add_row(['-' for value in columns.columns]) + env.fout(table) diff --git a/SoftLayer/CLI/virt/capacity/list.py b/SoftLayer/CLI/virt/capacity/list.py new file mode 100644 index 000000000..a5a5445c1 --- /dev/null +++ b/SoftLayer/CLI/virt/capacity/list.py @@ -0,0 +1,32 @@ +"""List Reserved Capacity""" + +import click + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.managers.vs_capacity import CapacityManager as CapacityManager + + +@click.command() +@environment.pass_env +def cli(env): + """List Reserved Capacity groups.""" + manager = CapacityManager(env.client) + result = manager.list() + table = formatting.Table( + ["ID", "Name", "Capacity", "Flavor", "Location", "Created"], + title="Reserved Capacity" + ) + for r_c in result: + occupied_string = "#" * int(r_c.get('occupiedInstanceCount', 0)) + available_string = "-" * int(r_c.get('availableInstanceCount', 0)) + + try: + flavor = r_c['instances'][0]['billingItem']['description'] + # cost = float(r_c['instances'][0]['billingItem']['hourlyRecurringFee']) + except KeyError: + flavor = "Unknown Billing Item" + location = r_c['backendRouter']['hostname'] + capacity = "%s%s" % (occupied_string, available_string) + table.add_row([r_c['id'], r_c['name'], capacity, flavor, location, r_c['createDate']]) + env.fout(table) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index ee904ba6a..79af92585 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -72,11 +72,11 @@ def _parse_create_args(client, args): :param dict args: CLI arguments """ data = { - "hourly": args['billing'] == 'hourly', + "hourly": args.get('billing', 'hourly') == 'hourly', "domain": args['domain'], "hostname": args['hostname'], - "private": args['private'], - "dedicated": args['dedicated'], + "private": args.get('private', None), + "dedicated": args.get('dedicated', None), "disks": args['disk'], "cpus": args.get('cpu', None), "memory": args.get('memory', None), @@ -89,7 +89,7 @@ def _parse_create_args(client, args): if not args.get('san') and args.get('flavor'): data['local_disk'] = None else: - data['local_disk'] = not args['san'] + data['local_disk'] = not args.get('san') if args.get('os'): data['os_code'] = args['os'] diff --git a/SoftLayer/CLI/vpn/ipsec/subnet/add.py b/SoftLayer/CLI/vpn/ipsec/subnet/add.py index 438dfc5fc..08d0bc5ec 100644 --- a/SoftLayer/CLI/vpn/ipsec/subnet/add.py +++ b/SoftLayer/CLI/vpn/ipsec/subnet/add.py @@ -18,14 +18,14 @@ type=int, help='Subnet identifier to add') @click.option('-t', - '--type', '--subnet-type', + '--type', required=True, type=click.Choice(['internal', 'remote', 'service']), help='Subnet type to add') @click.option('-n', - '--network', '--network-identifier', + '--network', default=None, type=NetworkParamType(), help='Subnet network identifier to create') diff --git a/SoftLayer/CLI/vpn/ipsec/subnet/remove.py b/SoftLayer/CLI/vpn/ipsec/subnet/remove.py index 2d8b34d9b..41d450a33 100644 --- a/SoftLayer/CLI/vpn/ipsec/subnet/remove.py +++ b/SoftLayer/CLI/vpn/ipsec/subnet/remove.py @@ -16,8 +16,8 @@ type=int, help='Subnet identifier to remove') @click.option('-t', - '--type', '--subnet-type', + '--type', required=True, type=click.Choice(['internal', 'remote', 'service']), help='Subnet type to add') diff --git a/SoftLayer/CLI/vpn/ipsec/update.py b/SoftLayer/CLI/vpn/ipsec/update.py index 68e09b0a9..4056f3b8f 100644 --- a/SoftLayer/CLI/vpn/ipsec/update.py +++ b/SoftLayer/CLI/vpn/ipsec/update.py @@ -20,48 +20,48 @@ @click.option('--preshared-key', default=None, help='Preshared key value') -@click.option('--p1-auth', - '--phase1-auth', +@click.option('--phase1-auth', + '--p1-auth', default=None, type=click.Choice(['MD5', 'SHA1', 'SHA256']), help='Phase 1 authentication value') -@click.option('--p1-crypto', - '--phase1-crypto', +@click.option('--phase1-crypto', + '--p1-crypto', default=None, type=click.Choice(['DES', '3DES', 'AES128', 'AES192', 'AES256']), help='Phase 1 encryption value') -@click.option('--p1-dh', - '--phase1-dh', +@click.option('--phase1-dh', + '--p1-dh', default=None, type=click.Choice(['0', '1', '2', '5']), help='Phase 1 diffie hellman group value') -@click.option('--p1-key-ttl', - '--phase1-key-ttl', +@click.option('--phase1-key-ttl', + '--p1-key-ttl', default=None, type=click.IntRange(120, 172800), help='Phase 1 key life value') -@click.option('--p2-auth', - '--phase2-auth', +@click.option('--phase2-auth', + '--p2-auth', default=None, type=click.Choice(['MD5', 'SHA1', 'SHA256']), help='Phase 2 authentication value') -@click.option('--p2-crypto', - '--phase2-crypto', +@click.option('--phase2-crypto', + '--p2-crypto', default=None, type=click.Choice(['DES', '3DES', 'AES128', 'AES192', 'AES256']), help='Phase 2 encryption value') -@click.option('--p2-dh', - '--phase2-dh', +@click.option('--phase2-dh', + '--p2-dh', default=None, type=click.Choice(['0', '1', '2', '5']), help='Phase 2 diffie hellman group value') -@click.option('--p2-forward-secrecy', - '--phase2-forward-secrecy', +@click.option('--phase2-forward-secrecy', + '--p2-forward-secrecy', default=None, type=click.IntRange(0, 1), help='Phase 2 perfect forward secrecy value') -@click.option('--p2-key-ttl', - '--phase2-key-ttl', +@click.option('--phase2-key-ttl', + '--p2-key-ttl', default=None, type=click.IntRange(120, 172800), help='Phase 2 key life value') diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index bbb8582b3..29e3f58d2 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v5.5.2' +VERSION = 'v5.6.0' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index 586e597a9..9b7b41da0 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -1,5 +1,6 @@ # -*- coding: UTF-8 -*- +# # pylint: disable=bad-continuation getPrivateBlockDeviceTemplateGroups = [{ 'accountId': 1234, 'blockDevices': [], @@ -553,11 +554,11 @@ 'name': 'dal05' }, 'memoryCapacity': 242, - 'name': 'khnguyendh', + 'name': 'test-dedicated', 'diskCapacity': 1200, 'guestCount': 1, 'cpuCount': 56, - 'id': 44701 + 'id': 12345 }] @@ -575,3 +576,64 @@ 'username': 'sl1234-abob', 'virtualGuestCount': 99} ] + +getReservedCapacityGroups = [ + { + 'accountId': 1234, + 'backendRouterId': 1411193, + 'createDate': '2018-09-24T16:33:09-06:00', + 'id': 3103, + 'modifyDate': '', + 'name': 'test-capacity', + 'availableInstanceCount': 1, + 'instanceCount': 2, + 'occupiedInstanceCount': 1, + 'backendRouter': { + 'accountId': 1, + 'bareMetalInstanceFlag': 0, + 'domain': 'softlayer.com', + 'fullyQualifiedDomainName': 'bcr02a.dal13.softlayer.com', + 'hardwareStatusId': 5, + 'hostname': 'bcr02a.dal13', + 'id': 1411193, + 'notes': '', + 'provisionDate': '', + 'serviceProviderId': 1, + 'serviceProviderResourceId': '', + 'primaryIpAddress': '10.0.144.28', + 'datacenter': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13', + 'statusId': 2 + }, + 'hardwareFunction': { + 'code': 'ROUTER', + 'description': 'Router', + 'id': 1 + }, + 'topLevelLocation': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13', + 'statusId': 2 + } + }, + 'instances': [ + { + 'id': 3501, + 'billingItem': { + 'description': 'B1.1x2 (1 Year Term)', + 'hourlyRecurringFee': '.032' + } + }, + { + 'id': 3519, + 'billingItem': { + 'description': 'B1.1x2 (1 Year Term)', + 'hourlyRecurringFee': '.032' + } + } + ] + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Network_Pod.py b/SoftLayer/fixtures/SoftLayer_Network_Pod.py new file mode 100644 index 000000000..4e6088270 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Network_Pod.py @@ -0,0 +1,22 @@ +getAllObjects = [ + { + 'backendRouterId': 117917, + 'backendRouterName': 'bcr01a.ams01', + 'datacenterId': 265592, + 'datacenterLongName': 'Amsterdam 1', + 'datacenterName': 'ams01', + 'frontendRouterId': 117960, + 'frontendRouterName': 'fcr01a.ams01', + 'name': 'ams01.pod01' + }, + { + 'backendRouterId': 1115295, + 'backendRouterName': 'bcr01a.wdc07', + 'datacenterId': 2017603, + 'datacenterLongName': 'Washington 7', + 'datacenterName': 'wdc07', + 'frontendRouterId': 1114993, + 'frontendRouterName': 'fcr01a.wdc07', + 'name': 'wdc07.pod01' + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Product_Order.py b/SoftLayer/fixtures/SoftLayer_Product_Order.py index 5b3cf27ca..5be637c9b 100644 --- a/SoftLayer/fixtures/SoftLayer_Product_Order.py +++ b/SoftLayer/fixtures/SoftLayer_Product_Order.py @@ -14,3 +14,69 @@ 'item': {'id': 1, 'description': 'this is a thing'}, }]} placeOrder = verifyOrder + +# Reserved Capacity Stuff + +rsc_verifyOrder = { + 'orderContainers': [ + { + 'locationObject': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13' + }, + 'name': 'test-capacity', + 'postTaxRecurring': '0.32', + 'prices': [ + { + 'item': { + 'id': 1, + 'description': 'B1.1x2 (1 Year ''Term)', + 'keyName': 'B1_1X2_1_YEAR_TERM', + } + } + ] + } + ], + 'postTaxRecurring': '0.32', +} + +rsc_placeOrder = { + 'orderDate': '2013-08-01 15:23:45', + 'orderId': 1234, + 'orderDetails': { + 'postTaxRecurring': '0.32', + }, + 'placedOrder': { + 'status': 'Great, thanks for asking', + 'locationObject': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13' + }, + 'name': 'test-capacity', + 'items': [ + { + 'description': 'B1.1x2 (1 Year ''Term)', + 'keyName': 'B1_1X2_1_YEAR_TERM', + 'categoryCode': 'guest_core', + } + ] + } +} + +rsi_placeOrder = { + 'orderId': 1234, + 'orderDetails': { + 'prices': [ + { + 'id': 4, + 'item': { + 'id': 1, + 'description': 'B1.1x2 (1 Year ''Term)', + 'keyName': 'B1_1X2_1_YEAR_TERM', + } + } + ] + } +} diff --git a/SoftLayer/fixtures/SoftLayer_Product_Package.py b/SoftLayer/fixtures/SoftLayer_Product_Package.py index b7b008788..186674282 100644 --- a/SoftLayer/fixtures/SoftLayer_Product_Package.py +++ b/SoftLayer/fixtures/SoftLayer_Product_Package.py @@ -789,131 +789,152 @@ getItems = [ { 'id': 1234, + 'keyName': 'KeyName01', 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, 'prices': [{'id': 1122, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 26, 'name': 'Uplink Port Speeds', 'categoryCode': 'port_speed'}]}], }, { 'id': 2233, + 'keyName': 'KeyName02', 'capacity': '1000', 'description': 'Public & Private Networks', 'itemCategory': {'categoryCode': 'Uplink Port Speeds'}, 'prices': [{'id': 4477, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 26, 'name': 'Uplink Port Speeds', 'categoryCode': 'port_speed'}]}], }, { 'id': 1239, + 'keyName': 'KeyName03', 'capacity': '2', 'description': 'RAM', 'itemCategory': {'categoryCode': 'RAM'}, 'prices': [{'id': 1133, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 3, 'name': 'RAM', 'categoryCode': 'ram'}]}], }, { 'id': 1240, + 'keyName': 'KeyName014', 'capacity': '4', 'units': 'PRIVATE_CORE', 'description': 'Computing Instance (Dedicated)', 'itemCategory': {'categoryCode': 'Computing Instance'}, 'prices': [{'id': 1007, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 80, 'name': 'Computing Instance', 'categoryCode': 'guest_core'}]}], }, { 'id': 1250, + 'keyName': 'KeyName015', 'capacity': '4', 'units': 'CORE', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, 'prices': [{'id': 1144, 'locationGroupId': None, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 80, 'name': 'Computing Instance', 'categoryCode': 'guest_core'}]}], }, { 'id': 112233, + 'keyName': 'KeyName016', 'capacity': '55', 'units': 'CORE', 'description': 'Computing Instance', 'itemCategory': {'categoryCode': 'Computing Instance'}, 'prices': [{'id': 332211, 'locationGroupId': 1, + 'hourlyRecurringFee': 0.0, 'categories': [{'id': 80, 'name': 'Computing Instance', 'categoryCode': 'guest_core'}]}], }, { 'id': 4439, + 'keyName': 'KeyName017', 'capacity': '1', 'description': '1 GB iSCSI Storage', 'itemCategory': {'categoryCode': 'iscsi'}, - 'prices': [{'id': 2222}], + 'prices': [{'id': 2222, 'hourlyRecurringFee': 0.0}], }, { 'id': 1121, + 'keyName': 'KeyName081', 'capacity': '20', 'description': '20 GB iSCSI snapshot', 'itemCategory': {'categoryCode': 'iscsi_snapshot_space'}, - 'prices': [{'id': 2014}], + 'prices': [{'id': 2014, 'hourlyRecurringFee': 0.0}], }, { 'id': 4440, + 'keyName': 'KeyName019', 'capacity': '4', 'description': '4 Portable Public IP Addresses', 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_pub'}, - 'prices': [{'id': 4444}], + 'prices': [{'id': 4444, 'hourlyRecurringFee': 0.0}], }, { 'id': 8880, + 'keyName': 'KeyName0199', 'capacity': '8', 'description': '8 Portable Public IP Addresses', 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_pub'}, - 'prices': [{'id': 8888}], + 'prices': [{'id': 8888, 'hourlyRecurringFee': 0.0}], }, { 'id': 44400, + 'keyName': 'KeyName0155', 'capacity': '4', 'description': '4 Portable Private IP Addresses', 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_priv'}, - 'prices': [{'id': 44441}], + 'prices': [{'id': 44441, 'hourlyRecurringFee': 0.0}], }, { 'id': 88800, + 'keyName': 'KeyName0144', 'capacity': '8', 'description': '8 Portable Private IP Addresses', 'itemCategory': {'categoryCode': 'sov_sec_ip_addresses_priv'}, - 'prices': [{'id': 88881}], + 'prices': [{'id': 88881, 'hourlyRecurringFee': 0.0}], }, { 'id': 10, + 'keyName': 'KeyName0341', 'capacity': '0', 'description': 'Global IPv4', 'itemCategory': {'categoryCode': 'global_ipv4'}, - 'prices': [{'id': 11}], + 'prices': [{'id': 11, 'hourlyRecurringFee': 0.0}], }, { 'id': 66464, + 'keyName': 'KeyName0211', 'capacity': '64', 'description': '/64 Block Portable Public IPv6 Addresses', 'itemCategory': {'categoryCode': 'static_ipv6_addresses'}, - 'prices': [{'id': 664641}], + 'prices': [{'id': 664641, 'hourlyRecurringFee': 0.0}], }, { 'id': 610, + 'keyName': 'KeyName031', 'capacity': '0', 'description': 'Global IPv6', 'itemCategory': {'categoryCode': 'global_ipv6'}, - 'prices': [{'id': 611}], + 'prices': [{'id': 611, 'hourlyRecurringFee': 0.0}], }] getItemPricesISCSI = [ @@ -1346,6 +1367,11 @@ "hourlyRecurringFee": ".093", "id": 204015, "recurringFee": "62", + "categories": [ + { + "categoryCode": "guest_core" + } + ], "item": { "description": "4 x 2.0 GHz or higher Cores", "id": 859, @@ -1542,3 +1568,85 @@ ] getAccountRestrictedActivePresets = [] + +RESERVED_CAPACITY = [{"id": 1059}] +getItems_RESERVED_CAPACITY = [ + { + 'id': 12273, + 'keyName': 'B1_1X2_1_YEAR_TERM', + 'description': 'B1 1x2 1 year term', + 'capacity': 12, + 'itemCategory': { + 'categoryCode': 'reserved_capacity', + 'id': 2060, + 'name': 'Reserved Capacity', + 'quantityLimit': 20, + 'sortOrder': '' + }, + 'prices': [ + { + 'currentPriceFlag': '', + 'hourlyRecurringFee': '.032', + 'id': 217561, + 'itemId': 12273, + 'laborFee': '0', + 'locationGroupId': '', + 'onSaleFlag': '', + 'oneTimeFee': '0', + 'quantity': '', + 'setupFee': '0', + 'sort': 0, + 'tierMinimumThreshold': '', + 'categories': [ + { + 'categoryCode': 'reserved_capacity', + 'id': 2060, + 'name': 'Reserved Capacity', + 'quantityLimit': 20, + 'sortOrder': '' + } + ] + } + ] + } +] + +getItems_1_IPV6_ADDRESS = [ + { + 'id': 4097, + 'keyName': '1_IPV6_ADDRESS', + 'itemCategory': { + 'categoryCode': 'pri_ipv6_addresses', + 'id': 325, + 'name': 'Primary IPv6 Addresses', + 'quantityLimit': 0, + 'sortOrder': 34 + }, + 'prices': [ + { + 'currentPriceFlag': '', + 'hourlyRecurringFee': '0', + 'id': 17129, + 'itemId': 4097, + 'laborFee': '0', + 'locationGroupId': '', + 'onSaleFlag': '', + 'oneTimeFee': '0', + 'quantity': '', + 'recurringFee': '0', + 'setupFee': '0', + 'sort': 0, + 'tierMinimumThreshold': '', + 'categories': [ + { + 'categoryCode': 'pri_ipv6_addresses', + 'id': 325, + 'name': 'Primary IPv6 Addresses', + 'quantityLimit': 0, + 'sortOrder': 34 + } + ] + } + ] + } +] diff --git a/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py b/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py index d111b9595..ec3356c1d 100644 --- a/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py +++ b/SoftLayer/fixtures/SoftLayer_Product_Package_Preset.py @@ -49,6 +49,7 @@ "id": 209595, "recurringFee": "118.26", "item": { + "capacity": 8, "description": "8 x 2.0 GHz or higher Cores", "id": 11307, "keyName": "GUEST_CORE_8", diff --git a/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py b/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py index 9d1f99571..a7ecdb29a 100644 --- a/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py +++ b/SoftLayer/fixtures/SoftLayer_Security_Ssh_Key.py @@ -6,3 +6,4 @@ 'notes': 'notes', 'key': 'ssh-rsa AAAAB3N...pa67 user@example.com'} createObject = getObject +getAllObjects = [getObject] diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py b/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py index ea1775eb9..94c8e5cc4 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_DedicatedHost.py @@ -11,57 +11,57 @@ getAvailableRouters = [ - {'hostname': 'bcr01a.dal05', 'id': 51218}, - {'hostname': 'bcr02a.dal05', 'id': 83361}, - {'hostname': 'bcr03a.dal05', 'id': 122762}, - {'hostname': 'bcr04a.dal05', 'id': 147566} + {'hostname': 'bcr01a.dal05', 'id': 12345}, + {'hostname': 'bcr02a.dal05', 'id': 12346}, + {'hostname': 'bcr03a.dal05', 'id': 12347}, + {'hostname': 'bcr04a.dal05', 'id': 12348} ] getObjectById = { 'datacenter': { - 'id': 138124, + 'id': 12345, 'name': 'dal05', 'longName': 'Dallas 5' }, 'memoryCapacity': 242, 'modifyDate': '2017-11-06T11:38:20-06:00', - 'name': 'khnguyendh', + 'name': 'test-dedicated', 'diskCapacity': 1200, 'backendRouter': { - 'domain': 'softlayer.com', + 'domain': 'test.com', 'hostname': 'bcr01a.dal05', - 'id': 51218 + 'id': 12345 }, 'guestCount': 1, 'cpuCount': 56, 'guests': [{ - 'domain': 'Softlayer.com', - 'hostname': 'khnguyenDHI', - 'id': 43546081, - 'uuid': '806a56ec-0383-4c2e-e6a9-7dc89c4b29a2' + 'domain': 'test.com', + 'hostname': 'test-dedicated', + 'id': 12345, + 'uuid': 'F9329795-4220-4B0A-B970-C86B950667FA' }], 'billingItem': { 'nextInvoiceTotalRecurringAmount': 1515.556, 'orderItem': { - 'id': 263060473, + 'id': 12345, 'order': { 'status': 'APPROVED', 'privateCloudOrderFlag': False, 'modifyDate': '2017-11-02T11:42:50-07:00', 'orderQuoteId': '', - 'userRecordId': 6908745, + 'userRecordId': 12345, 'createDate': '2017-11-02T11:40:56-07:00', 'impersonatingUserRecordId': '', 'orderTypeId': 7, 'presaleEventId': '', 'userRecord': { - 'username': '232298_khuong' + 'username': 'test-dedicated' }, - 'id': 20093269, - 'accountId': 232298 + 'id': 12345, + 'accountId': 12345 } }, - 'id': 235379377, + 'id': 12345, 'children': [ { 'nextInvoiceTotalRecurringAmount': 0.0, @@ -73,6 +73,6 @@ } ] }, - 'id': 44701, + 'id': 12345, 'createDate': '2017-11-02T11:40:56-07:00' } diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py index ee58ad762..785ed3b05 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest_Block_Device_Template_Group.py @@ -29,5 +29,11 @@ 'id': 100, 'name': 'test_image', }] - +createFromIcos = [{ + 'createDate': '2013-12-05T21:53:03-06:00', + 'globalIdentifier': '0B5DEAF4-643D-46CA-A695-CECBE8832C9D', + 'id': 100, + 'name': 'test_image', +}] copyToExternalSource = True +copyToIcos = True diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py b/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py new file mode 100644 index 000000000..67f496d6e --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Virtual_ReservedCapacityGroup.py @@ -0,0 +1,85 @@ +getObject = { + 'accountId': 1234, + 'backendRouterId': 1411193, + 'backendRouter': { + 'fullyQualifiedDomainName': 'bcr02a.dal13.softlayer.com', + 'hostname': 'bcr02a.dal13', + 'id': 1411193, + 'datacenter': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13', + + } + }, + 'createDate': '2018-09-24T16:33:09-06:00', + 'id': 3103, + 'modifyDate': '', + 'name': 'test-capacity', + 'instances': [ + { + 'createDate': '2018-09-24T16:33:09-06:00', + 'guestId': 62159257, + 'id': 3501, + 'billingItem': { + 'id': 348319479, + 'recurringFee': '3.04', + 'category': {'name': 'Reserved Capacity'}, + 'item': { + 'keyName': 'B1_1X2_1_YEAR_TERM' + } + }, + 'guest': { + 'domain': 'cgallo.com', + 'hostname': 'test-reserved-instance', + 'id': 62159257, + 'modifyDate': '2018-09-27T16:49:26-06:00', + 'primaryBackendIpAddress': '10.73.150.179', + 'primaryIpAddress': '169.62.147.165' + } + }, + { + 'createDate': '2018-09-24T16:33:10-06:00', + 'guestId': 62159275, + 'id': 3519, + 'billingItem': { + 'id': 348319443, + 'recurringFee': '3.04', + 'category': { + 'name': 'Reserved Capacity' + }, + 'item': { + 'keyName': 'B1_1X2_1_YEAR_TERM' + } + } + } + ] +} + + +getObject_pending = { + 'accountId': 1234, + 'backendRouterId': 1411193, + 'backendRouter': { + 'fullyQualifiedDomainName': 'bcr02a.dal13.softlayer.com', + 'hostname': 'bcr02a.dal13', + 'id': 1411193, + 'datacenter': { + 'id': 1854895, + 'longName': 'Dallas 13', + 'name': 'dal13', + + } + }, + 'createDate': '2018-09-24T16:33:09-06:00', + 'id': 3103, + 'modifyDate': '', + 'name': 'test-capacity', + 'instances': [ + { + 'createDate': '2018-09-24T16:33:09-06:00', + 'guestId': 62159257, + 'id': 3501, + } + ] +} diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py index f0602579e..b6cc1faa5 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -27,10 +27,12 @@ from SoftLayer.managers.ticket import TicketManager from SoftLayer.managers.user import UserManager from SoftLayer.managers.vs import VSManager +from SoftLayer.managers.vs_capacity import CapacityManager __all__ = [ 'BlockStorageManager', + 'CapacityManager', 'CDNManager', 'DedicatedHostManager', 'DNSManager', diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index c105b3b5d..6980f4397 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -268,7 +268,7 @@ def reload(self, hardware_id, post_uri=None, ssh_keys=None): """Perform an OS reload of a server with its current configuration. :param integer hardware_id: the instance ID to reload - :param string post_url: The URI of the post-install script to run + :param string post_uri: The URI of the post-install script to run after reload :param list ssh_keys: The SSH keys to add to the root user """ diff --git a/SoftLayer/managers/image.py b/SoftLayer/managers/image.py index 81eee9282..abd60ba8a 100644 --- a/SoftLayer/managers/image.py +++ b/SoftLayer/managers/image.py @@ -120,28 +120,68 @@ def edit(self, image_id, name=None, note=None, tag=None): return bool(name or note or tag) - def import_image_from_uri(self, name, uri, os_code=None, note=None): + def import_image_from_uri(self, name, uri, os_code=None, note=None, + ibm_api_key=None, root_key_id=None, + wrapped_dek=None, kp_id=None, cloud_init=False, + byol=False, is_encrypted=False): """Import a new image from object storage. :param string name: Name of the new image :param string uri: The URI for an object storage object (.vhd/.iso file) of the format: swift://@// + or (.vhd/.iso/.raw file) of the format: + cos://// if using IBM Cloud + Object Storage :param string os_code: The reference code of the operating system :param string note: Note to add to the image + :param string ibm_api_key: Ibm Api Key needed to communicate with ICOS + and Key Protect + :param string root_key_id: ID of the root key in Key Protect + :param string wrapped_dek: Wrapped Data Encryption Key provided by + IBM KeyProtect + :param string kp_id: ID of the IBM Key Protect Instance + :param boolean cloud_init: Specifies if image is cloud-init + :param boolean byol: Specifies if image is bring your own license + :param boolean is_encrypted: Specifies if image is encrypted """ - return self.vgbdtg.createFromExternalSource({ - 'name': name, - 'note': note, - 'operatingSystemReferenceCode': os_code, - 'uri': uri, - }) - - def export_image_to_uri(self, image_id, uri): + if 'cos://' in uri: + return self.vgbdtg.createFromIcos({ + 'name': name, + 'note': note, + 'operatingSystemReferenceCode': os_code, + 'uri': uri, + 'ibmApiKey': ibm_api_key, + 'rootKeyId': root_key_id, + 'wrappedDek': wrapped_dek, + 'keyProtectId': kp_id, + 'cloudInit': cloud_init, + 'byol': byol, + 'isEncrypted': is_encrypted + }) + else: + return self.vgbdtg.createFromExternalSource({ + 'name': name, + 'note': note, + 'operatingSystemReferenceCode': os_code, + 'uri': uri, + }) + + def export_image_to_uri(self, image_id, uri, ibm_api_key=None): """Export image into the given object storage :param int image_id: The ID of the image :param string uri: The URI for object storage of the format swift://@// + or cos://// if using IBM Cloud + Object Storage + :param string ibm_api_key: Ibm Api Key needed to communicate with IBM + Cloud Object Storage """ - return self.vgbdtg.copyToExternalSource({'uri': uri}, id=image_id) + if 'cos://' in uri: + return self.vgbdtg.copyToIcos({ + 'uri': uri, + 'ibmApiKey': ibm_api_key + }, id=image_id) + else: + return self.vgbdtg.copyToExternalSource({'uri': uri}, id=image_id) diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index 01a182ae1..18a606b05 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -322,7 +322,7 @@ def get_preset_by_key(self, package_keyname, preset_keyname, mask=None): return presets[0] - def get_price_id_list(self, package_keyname, item_keynames): + def get_price_id_list(self, package_keyname, item_keynames, core=None): """Converts a list of item keynames to a list of price IDs. This function is used to convert a list of item keynames into @@ -331,6 +331,7 @@ def get_price_id_list(self, package_keyname, item_keynames): :param str package_keyname: The package associated with the prices :param list item_keynames: A list of item keyname strings + :param str core: preset guest core capacity. :returns: A list of price IDs associated with the given item keynames in the given package @@ -356,8 +357,7 @@ def get_price_id_list(self, package_keyname, item_keynames): # can take that ID and create the proper price for us in the location # in which the order is made if matching_item['itemCategory']['categoryCode'] != "gpu0": - price_id = [p['id'] for p in matching_item['prices'] - if not p['locationGroupId']][0] + price_id = self.get_item_price_id(core, matching_item['prices']) else: # GPU items has two generic prices and they are added to the list # according to the number of gpu items added in the order. @@ -370,6 +370,20 @@ def get_price_id_list(self, package_keyname, item_keynames): return prices + @staticmethod + def get_item_price_id(core, prices): + """get item price id""" + price_id = None + for price in prices: + if not price['locationGroupId']: + capacity_min = int(price.get('capacityRestrictionMinimum', -1)) + capacity_max = int(price.get('capacityRestrictionMaximum', -1)) + if capacity_min == -1: + price_id = price['id'] + elif capacity_min <= int(core) <= capacity_max: + price_id = price['id'] + return price_id + def get_preset_prices(self, preset): """Get preset item prices. @@ -461,7 +475,6 @@ def place_order(self, package_keyname, location, item_keynames, complex_type=Non def place_quote(self, package_keyname, location, item_keynames, complex_type=None, preset_keyname=None, extras=None, quantity=1, quote_name=None, send_email=False): - """Place a quote with the given package and prices. This function takes in parameters needed for an order and places the quote. @@ -534,15 +547,20 @@ def generate_order(self, package_keyname, location, item_keynames, complex_type= order['quantity'] = quantity order['useHourlyPricing'] = hourly + preset_core = None if preset_keyname: preset_id = self.get_preset_by_key(package_keyname, preset_keyname)['id'] + preset_items = self.get_preset_prices(preset_id) + for item in preset_items['prices']: + if item['item']['itemCategory']['categoryCode'] == "guest_core": + preset_core = item['item']['capacity'] 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) + price_ids = self.get_price_id_list(package_keyname, item_keynames, preset_core) order['prices'] = [{'id': price_id} for price_id in price_ids] container['orderContainers'] = [order] diff --git a/SoftLayer/managers/vs_capacity.py b/SoftLayer/managers/vs_capacity.py new file mode 100644 index 000000000..c2be6a615 --- /dev/null +++ b/SoftLayer/managers/vs_capacity.py @@ -0,0 +1,174 @@ +""" + SoftLayer.vs_capacity + ~~~~~~~~~~~~~~~~~~~~~~~ + Reserved Capacity Manager and helpers + + :license: MIT, see License for more details. +""" + +import logging +import SoftLayer + +from SoftLayer.managers import ordering +from SoftLayer.managers.vs import VSManager +from SoftLayer import utils + +# Invalid names are ignored due to long method names and short argument names +# pylint: disable=invalid-name, no-self-use + +LOGGER = logging.getLogger(__name__) + + +class CapacityManager(utils.IdentifierMixin, object): + """Manages SoftLayer Reserved Capacity Groups. + + Product Information + + - https://console.bluemix.net/docs/vsi/vsi_about_reserved.html + - https://softlayer.github.io/reference/services/SoftLayer_Virtual_ReservedCapacityGroup/ + - https://softlayer.github.io/reference/services/SoftLayer_Virtual_ReservedCapacityGroup_Instance/ + + + :param SoftLayer.API.BaseClient client: the client instance + :param SoftLayer.managers.OrderingManager ordering_manager: an optional manager to handle ordering. + If none is provided, one will be auto initialized. + """ + + def __init__(self, client, ordering_manager=None): + self.client = client + self.account = client['Account'] + self.capacity_package = 'RESERVED_CAPACITY' + self.rcg_service = 'Virtual_ReservedCapacityGroup' + + if ordering_manager is None: + self.ordering_manager = ordering.OrderingManager(client) + + def list(self): + """List Reserved Capacities""" + mask = """mask[availableInstanceCount, occupiedInstanceCount, +instances[id, billingItem[description, hourlyRecurringFee]], instanceCount, backendRouter[datacenter]]""" + results = self.client.call('Account', 'getReservedCapacityGroups', mask=mask) + return results + + def get_object(self, identifier, mask=None): + """Get a Reserved Capacity Group + + :param int identifier: Id of the SoftLayer_Virtual_ReservedCapacityGroup + :param string mask: override default object Mask + """ + if mask is None: + mask = "mask[instances[billingItem[item[keyName],category], guest], backendRouter[datacenter]]" + result = self.client.call(self.rcg_service, 'getObject', id=identifier, mask=mask) + return result + + def get_create_options(self): + """List available reserved capacity plans""" + mask = "mask[attributes,prices[pricingLocationGroup]]" + results = self.ordering_manager.list_items(self.capacity_package, mask=mask) + return results + + def get_available_routers(self, dc=None): + """Pulls down all backendRouterIds that are available + + :param string dc: A specific location to get routers for, like 'dal13'. + :returns list: A list of locations where RESERVED_CAPACITY can be ordered. + """ + mask = "mask[locations]" + # Step 1, get the package id + package = self.ordering_manager.get_package_by_key(self.capacity_package, mask="id") + + # Step 2, get the regions this package is orderable in + regions = self.client.call('Product_Package', 'getRegions', id=package['id'], mask=mask, iter=True) + _filter = None + routers = {} + if dc is not None: + _filter = {'datacenterName': {'operation': dc}} + + # Step 3, for each location in each region, get the pod details, which contains the router id + pods = self.client.call('Network_Pod', 'getAllObjects', filter=_filter, iter=True) + for region in regions: + routers[region['keyname']] = [] + for location in region['locations']: + location['location']['pods'] = list() + for pod in pods: + if pod['datacenterName'] == location['location']['name']: + location['location']['pods'].append(pod) + + # Step 4, return the data. + return regions + + def create(self, name, backend_router_id, flavor, instances, test=False): + """Orders a Virtual_ReservedCapacityGroup + + :param string name: Name for the new reserved capacity + :param int backend_router_id: This selects the pod. See create_options for a list + :param string flavor: Capacity KeyName, see create_options for a list + :param int instances: Number of guest this capacity can support + :param bool test: If True, don't actually order, just test. + """ + + # Since orderManger needs a DC id, just send in 0, the API will ignore it + args = (self.capacity_package, 0, [flavor]) + extras = {"backendRouterId": backend_router_id, "name": name} + kwargs = { + 'extras': extras, + 'quantity': instances, + 'complex_type': 'SoftLayer_Container_Product_Order_Virtual_ReservedCapacity', + 'hourly': True + } + if test: + receipt = self.ordering_manager.verify_order(*args, **kwargs) + else: + receipt = self.ordering_manager.place_order(*args, **kwargs) + return receipt + + def create_guest(self, capacity_id, test, guest_object): + """Turns an empty Reserve Capacity into a real Virtual Guest + + :param int capacity_id: ID of the RESERVED_CAPACITY_GROUP to create this guest into + :param bool test: True will use verifyOrder, False will use placeOrder + :param dictionary guest_object: Below is the minimum info you need to send in + guest_object = { + 'domain': 'test.com', + 'hostname': 'A1538172419', + 'os_code': 'UBUNTU_LATEST_64', + 'primary_disk': '25', + } + + """ + + vs_manager = VSManager(self.client) + mask = "mask[instances[id, billingItem[id, item[id,keyName]]], backendRouter[id, datacenter[name]]]" + capacity = self.get_object(capacity_id, mask=mask) + try: + capacity_flavor = capacity['instances'][0]['billingItem']['item']['keyName'] + flavor = _flavor_string(capacity_flavor, guest_object['primary_disk']) + except KeyError: + raise SoftLayer.SoftLayerError("Unable to find capacity Flavor.") + + guest_object['flavor'] = flavor + guest_object['datacenter'] = capacity['backendRouter']['datacenter']['name'] + + # Reserved capacity only supports SAN as of 20181008 + guest_object['local_disk'] = False + template = vs_manager.verify_create_instance(**guest_object) + template['reservedCapacityId'] = capacity_id + if guest_object.get('ipv6'): + ipv6_price = self.ordering_manager.get_price_id_list('PUBLIC_CLOUD_SERVER', ['1_IPV6_ADDRESS']) + template['prices'].append({'id': ipv6_price[0]}) + + if test: + result = self.client.call('Product_Order', 'verifyOrder', template) + else: + result = self.client.call('Product_Order', 'placeOrder', template) + + return result + + +def _flavor_string(capacity_key, primary_disk): + """Removed the _X_YEAR_TERM from capacity_key and adds the primary disk size, creating the flavor keyName + + This will work fine unless 10 year terms are invented... or flavor format changes... + """ + flavor = "%sX%s" % (capacity_key[:-12], primary_disk) + return flavor diff --git a/docs/api/client.rst b/docs/api/client.rst index a29974be2..6c447bead 100644 --- a/docs/api/client.rst +++ b/docs/api/client.rst @@ -144,6 +144,9 @@ 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 + #Automatic Pagination (v5.5.3+) + client.call('Account', 'getVirtualGuests', iter=True) # Page 2 + Here's how to create a new Cloud Compute Instance using `SoftLayer_Virtual_Guest.createObject `_. Be warned, this call actually creates an hourly virtual server so this will @@ -161,6 +164,28 @@ have billing implications. }) +Debugging +------------- +If you ever need to figure out what exact API call the client is making, you can do the following: + +*NOTE* the `print_reproduceable` method produces different output for REST and XML-RPC endpoints. If you are using REST, this will produce a CURL call. IF you are using XML-RPC, it will produce some pure python code you can use outside of the SoftLayer library. + +:: + + # Setup the client as usual + client = SoftLayer.Client() + # Create an instance of the DebugTransport, which logs API calls + debugger = SoftLayer.DebugTransport(client.transport) + # Set that as the default client transport + client.transport = debugger + # Make your API call + client.call('Account', 'getObject') + + # Print out the reproduceable call + for call in client.transport.get_last_calls(): + print(client.transport.print_reproduceable(call)) + + API Reference ------------- diff --git a/docs/api/managers/vs_capacity.rst b/docs/api/managers/vs_capacity.rst new file mode 100644 index 000000000..3255a40b1 --- /dev/null +++ b/docs/api/managers/vs_capacity.rst @@ -0,0 +1,5 @@ +.. _vs_capacity: + +.. automodule:: SoftLayer.managers.vs_capacity + :members: + :inherited-members: diff --git a/docs/cli/vs.rst b/docs/cli/vs.rst index f61b9fd92..55ee3c189 100644 --- a/docs/cli/vs.rst +++ b/docs/cli/vs.rst @@ -28,6 +28,8 @@ virtual server (VS), we need to know what options are available to us: RAM, CPU, operating systems, disk sizes, disk types, datacenters, and so on. Luckily, there's a simple command to show all options: `slcli vs create-options`. +*Some values were ommitted for brevity* + :: $ slcli vs create-options @@ -36,182 +38,16 @@ Luckily, there's a simple command to show all options: `slcli vs create-options` :................................:.................................................................................: : datacenter : ams01 : : : ams03 : - : : che01 : - : : dal01 : - : : dal05 : - : : dal06 : - : : dal09 : - : : dal10 : - : : dal12 : - : : dal13 : - : : fra02 : - : : hkg02 : - : : hou02 : - : : lon02 : - : : lon04 : - : : lon06 : - : : mel01 : - : : mex01 : - : : mil01 : - : : mon01 : - : : osl01 : - : : par01 : - : : sao01 : - : : sea01 : - : : seo01 : - : : sjc01 : - : : sjc03 : - : : sjc04 : - : : sng01 : - : : syd01 : - : : syd04 : - : : tok02 : - : : tor01 : - : : wdc01 : - : : wdc04 : - : : wdc06 : : : wdc07 : : flavors (balanced) : B1_1X2X25 : : : B1_1X2X25 : : : B1_1X2X100 : - : : B1_1X2X100 : - : : B1_1X4X25 : - : : B1_1X4X25 : - : : B1_1X4X100 : - : : B1_1X4X100 : - : : B1_2X4X25 : - : : B1_2X4X25 : - : : B1_2X4X100 : - : : B1_2X4X100 : - : : B1_2X8X25 : - : : B1_2X8X25 : - : : B1_2X8X100 : - : : B1_2X8X100 : - : : B1_4X8X25 : - : : B1_4X8X25 : - : : B1_4X8X100 : - : : B1_4X8X100 : - : : B1_4X16X25 : - : : B1_4X16X25 : - : : B1_4X16X100 : - : : B1_4X16X100 : - : : B1_8X16X25 : - : : B1_8X16X25 : - : : B1_8X16X100 : - : : B1_8X16X100 : - : : B1_8X32X25 : - : : B1_8X32X25 : - : : B1_8X32X100 : - : : B1_8X32X100 : - : : B1_16X32X25 : - : : B1_16X32X25 : - : : B1_16X32X100 : - : : B1_16X32X100 : - : : B1_16X64X25 : - : : B1_16X64X25 : - : : B1_16X64X100 : - : : B1_16X64X100 : - : : B1_32X64X25 : - : : B1_32X64X25 : - : : B1_32X64X100 : - : : B1_32X64X100 : - : : B1_32X128X25 : - : : B1_32X128X25 : - : : B1_32X128X100 : - : : B1_32X128X100 : - : : B1_48X192X25 : - : : B1_48X192X25 : - : : B1_48X192X100 : - : : B1_48X192X100 : - : flavors (balanced local - hdd) : BL1_1X2X100 : - : : BL1_1X4X100 : - : : BL1_2X4X100 : - : : BL1_2X8X100 : - : : BL1_4X8X100 : - : : BL1_4X16X100 : - : : BL1_8X16X100 : - : : BL1_8X32X100 : - : : BL1_16X32X100 : - : : BL1_16X64X100 : - : : BL1_32X64X100 : - : : BL1_32X128X100 : - : : BL1_56X242X100 : - : flavors (balanced local - ssd) : BL2_1X2X100 : - : : BL2_1X4X100 : - : : BL2_2X4X100 : - : : BL2_2X8X100 : - : : BL2_4X8X100 : - : : BL2_4X16X100 : - : : BL2_8X16X100 : - : : BL2_8X32X100 : - : : BL2_16X32X100 : - : : BL2_16X64X100 : - : : BL2_32X64X100 : - : : BL2_32X128X100 : - : : BL2_56X242X100 : - : flavors (compute) : C1_1X1X25 : - : : C1_1X1X25 : - : : C1_1X1X100 : - : : C1_1X1X100 : - : : C1_2X2X25 : - : : C1_2X2X25 : - : : C1_2X2X100 : - : : C1_2X2X100 : - : : C1_4X4X25 : - : : C1_4X4X25 : - : : C1_4X4X100 : - : : C1_4X4X100 : - : : C1_8X8X25 : - : : C1_8X8X25 : - : : C1_8X8X100 : - : : C1_8X8X100 : - : : C1_16X16X25 : - : : C1_16X16X25 : - : : C1_16X16X100 : - : : C1_16X16X100 : - : : C1_32X32X25 : - : : C1_32X32X25 : - : : C1_32X32X100 : - : : C1_32X32X100 : - : flavors (memory) : M1_1X8X25 : - : : M1_1X8X25 : - : : M1_1X8X100 : - : : M1_1X8X100 : - : : M1_2X16X25 : - : : M1_2X16X25 : - : : M1_2X16X100 : - : : M1_2X16X100 : - : : M1_4X32X25 : - : : M1_4X32X25 : - : : M1_4X32X100 : - : : M1_4X32X100 : - : : M1_8X64X25 : - : : M1_8X64X25 : - : : M1_8X64X100 : - : : M1_8X64X100 : - : : M1_16X128X25 : - : : M1_16X128X25 : - : : M1_16X128X100 : - : : M1_16X128X100 : - : : M1_30X240X25 : - : : M1_30X240X25 : - : : M1_30X240X100 : - : : M1_30X240X100 : - : flavors (GPU) : AC1_8X60X25 : - : : AC1_8X60X100 : - : : AC1_16X120X25 : - : : AC1_16X120X100 : - : : ACL1_8X60X100 : - : : ACL1_16X120X100 : : cpus (standard) : 1,2,4,8,12,16,32,56 : : cpus (dedicated) : 1,2,4,8,16,32,56 : : cpus (dedicated host) : 1,2,4,8,12,16,32,56 : : memory : 1024,2048,4096,6144,8192,12288,16384,32768,49152,65536,131072,247808 : : memory (dedicated host) : 1024,2048,4096,6144,8192,12288,16384,32768,49152,65536,131072,247808 : : os (CENTOS) : CENTOS_5_64 : - : : CENTOS_6_64 : - : : CENTOS_7_64 : - : : CENTOS_LATEST : : : CENTOS_LATEST_64 : : os (CLOUDLINUX) : CLOUDLINUX_5_64 : : : CLOUDLINUX_6_64 : @@ -221,10 +57,6 @@ Luckily, there's a simple command to show all options: `slcli vs create-options` : : COREOS_LATEST : : : COREOS_LATEST_64 : : os (DEBIAN) : DEBIAN_6_64 : - : : DEBIAN_7_64 : - : : DEBIAN_8_64 : - : : DEBIAN_9_64 : - : : DEBIAN_LATEST : : : DEBIAN_LATEST_64 : : os (OTHERUNIXLINUX) : OTHERUNIXLINUX_1_64 : : : OTHERUNIXLINUX_LATEST : @@ -234,43 +66,11 @@ Luckily, there's a simple command to show all options: `slcli vs create-options` : : REDHAT_7_64 : : : REDHAT_LATEST : : : REDHAT_LATEST_64 : - : os (UBUNTU) : UBUNTU_12_64 : - : : UBUNTU_14_64 : - : : UBUNTU_16_64 : - : : UBUNTU_LATEST : - : : UBUNTU_LATEST_64 : - : os (VYATTACE) : VYATTACE_6.5_64 : - : : VYATTACE_6.6_64 : - : : VYATTACE_LATEST : - : : VYATTACE_LATEST_64 : - : os (WIN) : WIN_2003-DC-SP2-1_32 : - : : WIN_2003-DC-SP2-1_64 : - : : WIN_2003-ENT-SP2-5_32 : - : : WIN_2003-ENT-SP2-5_64 : - : : WIN_2003-STD-SP2-5_32 : - : : WIN_2003-STD-SP2-5_64 : - : : WIN_2008-STD-R2-SP1_64 : - : : WIN_2008-STD-SP2_32 : - : : WIN_2008-STD-SP2_64 : - : : WIN_2012-STD-R2_64 : - : : WIN_2012-STD_64 : - : : WIN_2016-STD_64 : - : : WIN_LATEST : - : : WIN_LATEST_32 : - : : WIN_LATEST_64 : : san disk(0) : 25,100 : : san disk(2) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : - : san disk(3) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : - : san disk(4) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : - : san disk(5) : 10,20,25,30,40,50,75,100,125,150,175,200,250,300,350,400,500,750,1000,1500,2000 : : local disk(0) : 25,100 : : local disk(2) : 25,100,150,200,300 : : local (dedicated host) disk(0) : 25,100 : - : local (dedicated host) disk(2) : 25,100,150,200,300,400 : - : local (dedicated host) disk(3) : 25,100,150,200,300,400 : - : local (dedicated host) disk(4) : 25,100,150,200,300,400 : - : local (dedicated host) disk(5) : 25,100,150,200,300,400 : - : nic : 10,100,1000 : : nic (dedicated host) : 100,1000 : :................................:.................................................................................: @@ -281,7 +81,7 @@ datacenter using the command `slcli vs create`. :: - $ slcli vs create --hostname=example --domain=softlayer.com --cpu 2 --memory 1024 -o UBUNTU_14_64 --datacenter=sjc01 --billing=hourly + $ slcli vs create --hostname=example --domain=softlayer.com --cpu 2 --memory 1024 -o DEBIAN_LATEST_64 --datacenter=ams01 --billing=hourly This action will incur charges on your account. Continue? [y/N]: y :.........:......................................: : name : value : @@ -301,7 +101,7 @@ instantly appear in your virtual server list now. :.........:............:.......................:.......:........:................:..............:....................: : id : datacenter : host : cores : memory : primary_ip : backend_ip : active_transaction : :.........:............:.......................:.......:........:................:..............:....................: - : 1234567 : sjc01 : example.softlayer.com : 2 : 1G : 108.168.200.11 : 10.54.80.200 : Assign Host : + : 1234567 : ams01 : example.softlayer.com : 2 : 1G : 108.168.200.11 : 10.54.80.200 : Assign Host : :.........:............:.......................:.......:........:................:..............:....................: Cool. You may ask, "It's creating... but how do I know when it's done?" Well, @@ -338,12 +138,12 @@ username is 'root' and password is 'ABCDEFGH'. : hostname : example.softlayer.com : : status : Active : : state : Running : - : datacenter : sjc01 : + : datacenter : ams01 : : cores : 2 : : memory : 1G : : public_ip : 108.168.200.11 : : private_ip : 10.54.80.200 : - : os : Ubuntu : + : os : Debian : : private_only : False : : private_cpu : False : : created : 2013-06-13T08:29:44-06:00 : @@ -385,3 +185,12 @@ use `slcli help vs`. rescue Reboot into a rescue image. resume Resumes a paused virtual server. upgrade Upgrade a virtual server. + + +Reserved Capacity +----------------- +.. toctree:: + :maxdepth: 2 + + vs/reserved_capacity + diff --git a/docs/cli/vs/reserved_capacity.rst b/docs/cli/vs/reserved_capacity.rst new file mode 100644 index 000000000..3193febff --- /dev/null +++ b/docs/cli/vs/reserved_capacity.rst @@ -0,0 +1,57 @@ +.. _vs_reserved_capacity_user_docs: + +Working with Reserved Capacity +============================== +There are two main concepts for Reserved Capacity. The `Reserved Capacity Group `_ and the `Reserved Capacity Instance `_ +The Reserved Capacity Group, is a set block of capacity set aside for you at the time of the order. It will contain a set number of Instances which are all the same size. Instances can be ordered like normal VSIs, with the exception that you need to include the reservedCapacityGroupId, and it must be the same size as the group you are ordering the instance in. + +- `About Reserved Capacity `_ +- `Reserved Capacity FAQ `_ + +The SLCLI supports some basic Reserved Capacity Features. + + +.. _cli_vs_capacity_create: + +vs capacity create +------------------ +This command will create a Reserved Capacity Group. + +.. warning:: + + **These groups can not be canceled until their contract expires in 1 or 3 years!** + +:: + + $ slcli vs capacity create --name test-capacity -d dal13 -b 1411193 -c B1_1X2_1_YEAR_TERM -q 10 + +vs cacpacity create_options +--------------------------- +This command will print out the Flavors that can be used to create a Reserved Capacity Group, as well as the backend routers available, as those are needed when creating a new group. + +vs capacity create_guest +------------------------ +This command will create a virtual server (Reserved Capacity Instance) inside of your Reserved Capacity Group. This command works very similar to the `slcli vs create` command. + +:: + + $ slcli vs capacity create-guest --capacity-id 1234 --primary-disk 25 -H ABCD -D test.com -o UBUNTU_LATEST_64 --ipv6 -k test-key --test + +vs capacity detail +------------------ +This command will print out some basic information about the specified Reserved Capacity Group. + +vs capacity list +----------------- +This command will list out all Reserved Capacity Groups. a **#** symbol represents a filled instance, and a **-** symbol respresents an empty instance + +:: + + $ slcli vs capacity list + :............................................................................................................: + : Reserved Capacity : + :......:......................:............:......................:..............:...........................: + : ID : Name : Capacity : Flavor : Location : Created : + :......:......................:............:......................:..............:...........................: + : 1234 : test-capacity : ####------ : B1.1x2 (1 Year Term) : bcr02a.dal13 : 2018-09-24T16:33:09-06:00 : + :......:......................:............:......................:..............:...........................: \ No newline at end of file diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 21bb0d403..a0abdcc13 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -87,6 +87,33 @@ is: py.test tests +Fixtures +~~~~~~~~ + +Testing of this project relies quite heavily on fixtures to simulate API calls. When running the unit tests, we use the FixtureTransport class, which instead of making actual API calls, loads data from `/fixtures/SoftLayer_Service_Name.py` and tries to find a variable that matches the method you are calling. + +When adding new Fixtures you should try to sanitize the data of any account identifiying results, such as account ids, username, and that sort of thing. It is ok to leave the id in place for things like datacenter ids, price ids. + +To Overwrite a fixture, you can use a mock object to do so. Like either of these two methods: + +:: + + # From tests/CLI/modules/vs_capacity_tests.py + from SoftLayer.fixtures import SoftLayer_Product_Package + + def test_create_test(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + + def test_detail_pending(self): + capacity_mock = self.set_mock('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject') + get_object = { + 'name': 'test-capacity', + 'instances': [] + } + capacity_mock.return_value = get_object + + Documentation ------------- The project is documented in @@ -106,6 +133,7 @@ fabric, use the following commands. cd docs make html + sphinx-build -b html ./ ./html The primary docs are built at `Read the Docs `_. @@ -121,6 +149,17 @@ Flake8, with project-specific exceptions, can be run by using tox: tox -e analysis +Autopep8 can fix a lot of the simple flake8 errors about whitespace and indention. + +:: + + autopep8 -r -a -v -i --max-line-length 119 + + + + + + Contributing ------------ diff --git a/setup.py b/setup.py index 2753aff95..b4c86c2f5 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name='SoftLayer', - version='5.5.2', + version='5.6.0', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='SoftLayer Technologies, Inc.', @@ -32,11 +32,11 @@ install_requires=[ 'six >= 1.7.0', 'ptable >= 0.9.2', - 'click >= 5, < 7', - 'requests >= 2.18.4', + 'click >= 7', + 'requests == 2.19.1', 'prompt_toolkit >= 0.53', 'pygments >= 2.0.0', - 'urllib3 >= 1.22' + 'urllib3 == 1.22' ], keywords=['softlayer', 'cloud'], classifiers=[ diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 9d059f357..224313b06 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: slcli # check to see if it's available -version: '5.5.2+git' # check versioning +version: '5.6.0+git' # check versioning summary: Python based SoftLayer API Tool. # 79 char long summary description: | A command-line interface is also included and can be used to manage various SoftLayer products and services. diff --git a/tests/CLI/modules/dedicatedhost_tests.py b/tests/CLI/modules/dedicatedhost_tests.py index 59f6cab7c..1769d8cbf 100644 --- a/tests/CLI/modules/dedicatedhost_tests.py +++ b/tests/CLI/modules/dedicatedhost_tests.py @@ -6,7 +6,6 @@ """ import json import mock -import os import SoftLayer from SoftLayer.CLI import exceptions @@ -29,21 +28,17 @@ def test_list_dedicated_hosts(self): 'datacenter': 'dal05', 'diskCapacity': 1200, 'guestCount': 1, - 'id': 44701, + 'id': 12345, 'memoryCapacity': 242, - 'name': 'khnguyendh' + 'name': 'test-dedicated' }] ) - def tear_down(self): - if os.path.exists("test.txt"): - os.remove("test.txt") - def test_details(self): mock = self.set_mock('SoftLayer_Virtual_DedicatedHost', 'getObject') mock.return_value = SoftLayer_Virtual_DedicatedHost.getObjectById - result = self.run_command(['dedicatedhost', 'detail', '44701', '--price', '--guests']) + result = self.run_command(['dedicatedhost', 'detail', '12345', '--price', '--guests']) self.assert_no_fail(result) self.assertEqual(json.loads(result.output), @@ -54,19 +49,19 @@ def test_details(self): 'disk capacity': 1200, 'guest count': 1, 'guests': [{ - 'domain': 'Softlayer.com', - 'hostname': 'khnguyenDHI', - 'id': 43546081, - 'uuid': '806a56ec-0383-4c2e-e6a9-7dc89c4b29a2' + 'domain': 'test.com', + 'hostname': 'test-dedicated', + 'id': 12345, + 'uuid': 'F9329795-4220-4B0A-B970-C86B950667FA' }], - 'id': 44701, + 'id': 12345, 'memory capacity': 242, 'modify date': '2017-11-06T11:38:20-06:00', - 'name': 'khnguyendh', - 'owner': '232298_khuong', + 'name': 'test-dedicated', + 'owner': 'test-dedicated', 'price_rate': 1515.556, 'router hostname': 'bcr01a.dal05', - 'router id': 51218} + 'router id': 12345} ) def test_details_no_owner(self): @@ -85,18 +80,18 @@ def test_details_no_owner(self): 'disk capacity': 1200, 'guest count': 1, 'guests': [{ - 'domain': 'Softlayer.com', - 'hostname': 'khnguyenDHI', - 'id': 43546081, - 'uuid': '806a56ec-0383-4c2e-e6a9-7dc89c4b29a2'}], - 'id': 44701, + 'domain': 'test.com', + 'hostname': 'test-dedicated', + 'id': 12345, + 'uuid': 'F9329795-4220-4B0A-B970-C86B950667FA'}], + 'id': 12345, 'memory capacity': 242, 'modify date': '2017-11-06T11:38:20-06:00', - 'name': 'khnguyendh', + 'name': 'test-dedicated', 'owner': None, 'price_rate': 0, 'router hostname': 'bcr01a.dal05', - 'router id': 51218} + 'router id': 12345} ) def test_create_options(self): @@ -116,8 +111,8 @@ def test_create_options(self): '56 Cores X 242 RAM X 1.2 TB', 'value': '56_CORES_X_242_RAM_X_1_4_TB' } - ]] - ) + ]] + ) def test_create_options_with_only_datacenter(self): mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') @@ -149,7 +144,7 @@ def test_create_options_get_routers(self): 'Available Backend Routers': 'bcr04a.dal05' } ]] - ) + ) def test_create(self): SoftLayer.CLI.formatting.confirm = mock.Mock() @@ -159,30 +154,30 @@ def test_create(self): mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH result = self.run_command(['dedicatedhost', 'create', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=hourly']) self.assert_no_fail(result) args = ({ - 'hardware': [{ - 'domain': 'example.com', - 'primaryBackendNetworkComponent': { - 'router': { - 'id': 51218 - } - }, - 'hostname': 'host' - }], - 'prices': [{ + 'hardware': [{ + 'domain': 'test.com', + 'primaryBackendNetworkComponent': { + 'router': { + 'id': 12345 + } + }, + 'hostname': 'test-dedicated' + }], + 'useHourlyPricing': True, + 'location': 'DALLAS05', + 'packageId': 813, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', + 'prices': [{ 'id': 200269 - }], - 'location': 'DALLAS05', - 'packageId': 813, - 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', - 'useHourlyPricing': True, - 'quantity': 1},) + }], + 'quantity': 1},) self.assert_called_with('SoftLayer_Product_Order', 'placeOrder', args=args) @@ -195,30 +190,30 @@ def test_create_with_gpu(self): mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDHGpu result = self.run_command(['dedicatedhost', 'create', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_484_RAM_X_1_5_TB_X_2_GPU_P100', '--billing=hourly']) self.assert_no_fail(result) args = ({ - 'hardware': [{ - 'domain': 'example.com', - 'primaryBackendNetworkComponent': { - 'router': { - 'id': 51218 - } - }, - 'hostname': 'host' - }], - 'prices': [{ - 'id': 200269 - }], - 'location': 'DALLAS05', - 'packageId': 813, - 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', - 'useHourlyPricing': True, - 'quantity': 1},) + 'hardware': [{ + 'domain': 'test.com', + 'primaryBackendNetworkComponent': { + 'router': { + 'id': 12345 + } + }, + 'hostname': 'test-dedicated' + }], + 'prices': [{ + 'id': 200269 + }], + 'location': 'DALLAS05', + 'packageId': 813, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', + 'useHourlyPricing': True, + 'quantity': 1},) self.assert_called_with('SoftLayer_Product_Order', 'placeOrder', args=args) @@ -233,58 +228,58 @@ def test_create_verify(self): result = self.run_command(['dedicatedhost', 'create', '--verify', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=hourly']) self.assert_no_fail(result) args = ({ - 'useHourlyPricing': True, - 'hardware': [{ + 'useHourlyPricing': True, + 'hardware': [{ - 'hostname': 'host', - 'domain': 'example.com', + 'hostname': 'test-dedicated', + 'domain': 'test.com', - 'primaryBackendNetworkComponent': { - 'router': { - 'id': 51218 - } - } - }], - 'packageId': 813, 'prices': [{'id': 200269}], - 'location': 'DALLAS05', - 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', - 'quantity': 1},) + 'primaryBackendNetworkComponent': { + 'router': { + 'id': 12345 + } + } + }], + 'packageId': 813, 'prices': [{'id': 200269}], + 'location': 'DALLAS05', + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', + 'quantity': 1},) self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder', args=args) result = self.run_command(['dh', 'create', '--verify', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=monthly']) self.assert_no_fail(result) args = ({ - 'useHourlyPricing': True, - 'hardware': [{ - 'hostname': 'host', - 'domain': 'example.com', - 'primaryBackendNetworkComponent': { + 'useHourlyPricing': True, + 'hardware': [{ + 'hostname': 'test-dedicated', + 'domain': 'test.com', + 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } - } - }], - 'packageId': 813, 'prices': [{'id': 200269}], - 'location': 'DALLAS05', - 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', - 'quantity': 1},) + } + }], + 'packageId': 813, 'prices': [{'id': 200269}], + 'location': 'DALLAS05', + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', + 'quantity': 1},) self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder', args=args) @@ -296,8 +291,8 @@ def test_create_aborted(self): mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH result = self.run_command(['dh', 'create', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=monthly']) @@ -305,23 +300,6 @@ def test_create_aborted(self): self.assertEqual(result.exit_code, 2) self.assertIsInstance(result.exception, exceptions.CLIAbort) - def test_create_export(self): - mock_package_obj = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') - mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH - mock_package = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') - mock_package.return_value = SoftLayer_Product_Package.verifyOrderDH - - self.run_command(['dedicatedhost', 'create', - '--verify', - '--hostname=host', - '--domain=example.com', - '--datacenter=dal05', - '--flavor=56_CORES_X_242_RAM_X_1_4_TB', - '--billing=hourly', - '--export=test.txt']) - - self.assertEqual(os.path.exists("test.txt"), True) - def test_create_verify_no_price_or_more_than_one(self): mock_package_obj = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH @@ -332,30 +310,30 @@ def test_create_verify_no_price_or_more_than_one(self): result = self.run_command(['dedicatedhost', 'create', '--verify', - '--hostname=host', - '--domain=example.com', + '--hostname=test-dedicated', + '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', '--billing=hourly']) self.assertIsInstance(result.exception, exceptions.ArgumentError) args = ({ - 'hardware': [{ - 'domain': 'example.com', - 'primaryBackendNetworkComponent': { - 'router': { - 'id': 51218 - } - }, - 'hostname': 'host' - }], - 'prices': [{ - 'id': 200269 - }], - 'location': 'DALLAS05', - 'packageId': 813, - 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', - 'useHourlyPricing': True, - 'quantity': 1},) + 'hardware': [{ + 'domain': 'test.com', + 'primaryBackendNetworkComponent': { + 'router': { + 'id': 12345 + } + }, + 'hostname': 'test-dedicated' + }], + 'prices': [{ + 'id': 200269 + }], + 'location': 'DALLAS05', + 'packageId': 813, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', + 'useHourlyPricing': True, + 'quantity': 1},) self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder', args=args) diff --git a/tests/CLI/modules/ticket_tests.py b/tests/CLI/modules/ticket_tests.py index 817b3e71f..657953c5e 100644 --- a/tests/CLI/modules/ticket_tests.py +++ b/tests/CLI/modules/ticket_tests.py @@ -277,12 +277,12 @@ def test_ticket_summary(self): expected = [ {'Status': 'Open', 'count': [ - {'Type': 'Accounting', 'count': 7}, - {'Type': 'Billing', 'count': 3}, - {'Type': 'Sales', 'count': 5}, - {'Type': 'Support', 'count': 6}, - {'Type': 'Other', 'count': 4}, - {'Type': 'Total', 'count': 1}]}, + {'Type': 'Accounting', 'count': 7}, + {'Type': 'Billing', 'count': 3}, + {'Type': 'Sales', 'count': 5}, + {'Type': 'Support', 'count': 6}, + {'Type': 'Other', 'count': 4}, + {'Type': 'Total', 'count': 1}]}, {'Status': 'Closed', 'count': 2} ] result = self.run_command(['ticket', 'summary']) diff --git a/tests/CLI/modules/user_tests.py b/tests/CLI/modules/user_tests.py index 0222a62b8..79554fc85 100644 --- a/tests/CLI/modules/user_tests.py +++ b/tests/CLI/modules/user_tests.py @@ -94,7 +94,7 @@ def test_print_hardware_access(self): 'fullyQualifiedDomainName': 'test.test.test', 'provisionDate': '2018-05-08T15:28:32-06:00', 'primaryBackendIpAddress': '175.125.126.118', - 'primaryIpAddress': '175.125.126.118'} + 'primaryIpAddress': '175.125.126.118'} ], 'dedicatedHosts': [ {'id': 1234, @@ -129,7 +129,7 @@ def test_edit_perms_on(self): def test_edit_perms_on_bad(self): result = self.run_command(['user', 'edit-permissions', '11100', '--enable', '-p', 'TEST_NOt_exist']) - self.assertEqual(result.exit_code, -1) + self.assertEqual(result.exit_code, 1) def test_edit_perms_off(self): result = self.run_command(['user', 'edit-permissions', '11100', '--disable', '-p', 'TEST']) diff --git a/tests/CLI/modules/vs_capacity_tests.py b/tests/CLI/modules/vs_capacity_tests.py new file mode 100644 index 000000000..922bf2118 --- /dev/null +++ b/tests/CLI/modules/vs_capacity_tests.py @@ -0,0 +1,71 @@ +""" + SoftLayer.tests.CLI.modules.vs_capacity_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +from SoftLayer.fixtures import SoftLayer_Product_Order +from SoftLayer.fixtures import SoftLayer_Product_Package +from SoftLayer import testing + + +class VSCapacityTests(testing.TestCase): + + def test_list(self): + result = self.run_command(['vs', 'capacity', 'list']) + self.assert_no_fail(result) + + def test_detail(self): + result = self.run_command(['vs', 'capacity', 'detail', '1234']) + self.assert_no_fail(result) + + def test_detail_pending(self): + # Instances don't have a billing item if they haven't been approved yet. + capacity_mock = self.set_mock('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject') + get_object = { + 'name': 'test-capacity', + 'instances': [ + { + 'createDate': '2018-09-24T16:33:09-06:00', + 'guestId': 62159257, + 'id': 3501, + } + ] + } + capacity_mock.return_value = get_object + result = self.run_command(['vs', 'capacity', 'detail', '1234']) + self.assert_no_fail(result) + + def test_create_test(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + order_mock = self.set_mock('SoftLayer_Product_Order', 'verifyOrder') + order_mock.return_value = SoftLayer_Product_Order.rsc_verifyOrder + result = self.run_command(['vs', 'capacity', 'create', '--name=TEST', '--test', + '--backend_router_id=1234', '--flavor=B1_1X2_1_YEAR_TERM', '--instances=10']) + self.assert_no_fail(result) + + def test_create(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + order_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + order_mock.return_value = SoftLayer_Product_Order.rsc_placeOrder + result = self.run_command(['vs', 'capacity', 'create', '--name=TEST', '--instances=10', + '--backend_router_id=1234', '--flavor=B1_1X2_1_YEAR_TERM']) + self.assert_no_fail(result) + + def test_create_options(self): + result = self.run_command(['vs', 'capacity', 'create_options']) + self.assert_no_fail(result) + + def test_create_guest_test(self): + result = self.run_command(['vs', 'capacity', 'create-guest', '--capacity-id=3103', '--primary-disk=25', + '-H ABCDEFG', '-D test_list.com', '-o UBUNTU_LATEST_64', '-kTest 1', '--test']) + self.assert_no_fail(result) + + def test_create_guest(self): + order_mock = self.set_mock('SoftLayer_Product_Order', 'placeOrder') + order_mock.return_value = SoftLayer_Product_Order.rsi_placeOrder + result = self.run_command(['vs', 'capacity', 'create-guest', '--capacity-id=3103', '--primary-disk=25', + '-H ABCDEFG', '-D test_list.com', '-o UBUNTU_LATEST_64', '-kTest 1']) + self.assert_no_fail(result) diff --git a/tests/managers/dedicated_host_tests.py b/tests/managers/dedicated_host_tests.py index 2f21edacf..bdfa6d3f6 100644 --- a/tests/managers/dedicated_host_tests.py +++ b/tests/managers/dedicated_host_tests.py @@ -90,7 +90,7 @@ def test_place_order(self): { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': u'test.com', @@ -103,7 +103,7 @@ def test_place_order(self): 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -141,7 +141,7 @@ def test_place_order_with_gpu(self): { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': u'test.com', @@ -154,7 +154,7 @@ def test_place_order_with_gpu(self): 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -192,7 +192,7 @@ def test_verify_order(self): { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': 'test.com', @@ -205,7 +205,7 @@ def test_verify_order(self): 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -259,7 +259,7 @@ def test_generate_create_dict_without_router(self): { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': 'test.com', @@ -272,7 +272,7 @@ def test_generate_create_dict_without_router(self): 'complexType': 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -284,10 +284,10 @@ def test_generate_create_dict_with_router(self): self.dedicated_host._get_package = mock.MagicMock() self.dedicated_host._get_package.return_value = self._get_package() self.dedicated_host._get_default_router = mock.Mock() - self.dedicated_host._get_default_router.return_value = 51218 + self.dedicated_host._get_default_router.return_value = 12345 location = 'dal05' - router = 51218 + router = 12345 hostname = 'test' domain = 'test.com' hourly = True @@ -306,7 +306,7 @@ def test_generate_create_dict_with_router(self): { 'primaryBackendNetworkComponent': { 'router': { - 'id': 51218 + 'id': 12345 } }, 'domain': 'test.com', @@ -320,7 +320,7 @@ def test_generate_create_dict_with_router(self): 'SoftLayer_Container_Product_Order_Virtual_DedicatedHost', 'prices': [ { - 'id': 200269 + 'id': 12345 } ], 'quantity': 1 @@ -421,7 +421,7 @@ def test_get_create_options(self): def test_get_price(self): package = self._get_package() item = package['items'][0] - price_id = 200269 + price_id = 12345 self.assertEqual(self.dedicated_host._get_price(item), price_id) @@ -454,15 +454,15 @@ def test_get_item(self): }], 'capacity': '56', 'description': '56 Cores X 242 RAM X 1.2 TB', - 'id': 10195, + 'id': 12345, 'itemCategory': { 'categoryCode': 'dedicated_virtual_hosts' }, 'keyName': '56_CORES_X_242_RAM_X_1_4_TB', 'prices': [{ 'hourlyRecurringFee': '3.164', - 'id': 200269, - 'itemId': 10195, + 'id': 12345, + 'itemId': 12345, 'recurringFee': '2099', }] } @@ -481,7 +481,7 @@ def test_get_backend_router(self): location = [ { 'isAvailable': 1, - 'locationId': 138124, + 'locationId': 12345, 'packageId': 813 } ] @@ -528,7 +528,7 @@ def test_get_backend_router_no_routers_found(self): def test_get_default_router(self): routers = self._get_routers_sample() - router = 51218 + router = 12345 router_test = self.dedicated_host._get_default_router(routers, 'bcr01a.dal05') @@ -544,19 +544,19 @@ def _get_routers_sample(self): routers = [ { 'hostname': 'bcr01a.dal05', - 'id': 51218 + 'id': 12345 }, { 'hostname': 'bcr02a.dal05', - 'id': 83361 + 'id': 12346 }, { 'hostname': 'bcr03a.dal05', - 'id': 122762 + 'id': 12347 }, { 'hostname': 'bcr04a.dal05', - 'id': 147566 + 'id': 12348 } ] @@ -590,14 +590,14 @@ def _get_package(self): ], "prices": [ { - "itemId": 10195, + "itemId": 12345, "recurringFee": "2099", "hourlyRecurringFee": "3.164", - "id": 200269, + "id": 12345, } ], "keyName": "56_CORES_X_242_RAM_X_1_4_TB", - "id": 10195, + "id": 12345, "itemCategory": { "categoryCode": "dedicated_virtual_hosts" }, @@ -608,12 +608,12 @@ def _get_package(self): "location": { "locationPackageDetails": [ { - "locationId": 265592, + "locationId": 12345, "packageId": 813 } ], "location": { - "id": 265592, + "id": 12345, "name": "ams01", "longName": "Amsterdam 1" } @@ -627,12 +627,12 @@ def _get_package(self): "locationPackageDetails": [ { "isAvailable": 1, - "locationId": 138124, + "locationId": 12345, "packageId": 813 } ], "location": { - "id": 138124, + "id": 12345, "name": "dal05", "longName": "Dallas 5" } diff --git a/tests/managers/dns_tests.py b/tests/managers/dns_tests.py index 070eed707..6b32af918 100644 --- a/tests/managers/dns_tests.py +++ b/tests/managers/dns_tests.py @@ -97,13 +97,13 @@ def test_create_record_mx(self): self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'createObject', args=({ - 'domainId': 1, - 'ttl': 1200, - 'host': 'test', - 'type': 'MX', - 'data': 'testing', - 'mxPriority': 21 - },)) + 'domainId': 1, + 'ttl': 1200, + 'host': 'test', + 'type': 'MX', + 'data': 'testing', + 'mxPriority': 21 + },)) self.assertEqual(res, {'name': 'example.com'}) def test_create_record_srv(self): @@ -113,18 +113,18 @@ def test_create_record_srv(self): self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'createObject', args=({ - 'complexType': 'SoftLayer_Dns_Domain_ResourceRecord_SrvType', - 'domainId': 1, - 'ttl': 1200, - 'host': 'record', - 'type': 'SRV', - 'data': 'test_data', - 'priority': 21, - 'weight': 15, - 'service': 'foobar', - 'port': 8080, - 'protocol': 'SLS' - },)) + 'complexType': 'SoftLayer_Dns_Domain_ResourceRecord_SrvType', + 'domainId': 1, + 'ttl': 1200, + 'host': 'record', + 'type': 'SRV', + 'data': 'test_data', + 'priority': 21, + 'weight': 15, + 'service': 'foobar', + 'port': 8080, + 'protocol': 'SLS' + },)) self.assertEqual(res, {'name': 'example.com'}) def test_create_record_ptr(self): @@ -133,11 +133,11 @@ def test_create_record_ptr(self): self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'createObject', args=({ - 'ttl': 1200, - 'host': 'test', - 'type': 'PTR', - 'data': 'testing' - },)) + 'ttl': 1200, + 'host': 'test', + 'type': 'PTR', + 'data': 'testing' + },)) self.assertEqual(res, {'name': 'example.com'}) def test_generate_create_dict(self): diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index add6389fa..b3c95a1d2 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -288,7 +288,7 @@ def test_cancel_hardware_no_billing_item(self): ex = self.assertRaises(SoftLayer.SoftLayerError, self.hardware.cancel_hardware, 6327) - self.assertEqual("Ticket #1234 already exists for this server", str(ex)) + self.assertEqual("Ticket #1234 already exists for this server", str(ex)) def test_cancel_hardware_monthly_now(self): mock = self.set_mock('SoftLayer_Hardware_Server', 'getObject') diff --git a/tests/managers/image_tests.py b/tests/managers/image_tests.py index 5347d4e71..50a081988 100644 --- a/tests/managers/image_tests.py +++ b/tests/managers/image_tests.py @@ -145,6 +145,36 @@ def test_import_image(self): 'uri': 'someuri', 'operatingSystemReferenceCode': 'UBUNTU_LATEST'},)) + def test_import_image_cos(self): + self.image.import_image_from_uri(name='test_image', + note='testimage', + uri='cos://some_uri', + os_code='UBUNTU_LATEST', + ibm_api_key='some_ibm_key', + root_key_id='some_root_key_id', + wrapped_dek='some_dek', + kp_id='some_id', + cloud_init=False, + byol=False, + is_encrypted=False + ) + + self.assert_called_with( + IMAGE_SERVICE, + 'createFromIcos', + args=({'name': 'test_image', + 'note': 'testimage', + 'operatingSystemReferenceCode': 'UBUNTU_LATEST', + 'uri': 'cos://some_uri', + 'ibmApiKey': 'some_ibm_key', + 'rootKeyId': 'some_root_key_id', + 'wrappedDek': 'some_dek', + 'keyProtectId': 'some_id', + 'cloudInit': False, + 'byol': False, + 'isEncrypted': False + },)) + def test_export_image(self): self.image.export_image_to_uri(1234, 'someuri') @@ -153,3 +183,14 @@ def test_export_image(self): 'copyToExternalSource', args=({'uri': 'someuri'},), identifier=1234) + + def test_export_image_cos(self): + self.image.export_image_to_uri(1234, + 'cos://someuri', + ibm_api_key='someApiKey') + + self.assert_called_with( + IMAGE_SERVICE, + 'copyToIcos', + args=({'uri': 'cos://someuri', 'ibmApiKey': 'someApiKey'},), + identifier=1234) diff --git a/tests/managers/ordering_tests.py b/tests/managers/ordering_tests.py index 0ea7c7546..4b2c431cb 100644 --- a/tests/managers/ordering_tests.py +++ b/tests/managers/ordering_tests.py @@ -296,7 +296,8 @@ def test_get_preset_by_key_preset_not_found(self): def test_get_price_id_list(self): category1 = {'categoryCode': 'cat1'} - price1 = {'id': 1234, 'locationGroupId': None, 'itemCategory': [category1]} + price1 = {'id': 1234, 'locationGroupId': None, 'categories': [{"categoryCode": "guest_core"}], + 'itemCategory': [category1]} item1 = {'id': 1111, 'keyName': 'ITEM1', 'itemCategory': category1, 'prices': [price1]} category2 = {'categoryCode': 'cat2'} price2 = {'id': 5678, 'locationGroupId': None, 'categories': [category2]} @@ -305,7 +306,7 @@ def test_get_price_id_list(self): 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']) + prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM2'], "8") list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') self.assertEqual([price1['id'], price2['id']], prices) @@ -320,7 +321,7 @@ def test_get_price_id_list_item_not_found(self): exc = self.assertRaises(exceptions.SoftLayerError, self.ordering.get_price_id_list, - 'PACKAGE_KEYNAME', ['ITEM2']) + 'PACKAGE_KEYNAME', ['ITEM2'], "8") list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') self.assertEqual("Item ITEM2 does not exist for package PACKAGE_KEYNAME", str(exc)) @@ -333,7 +334,7 @@ def test_get_price_id_list_gpu_items_with_two_categories(self): with mock.patch.object(self.ordering, 'list_items') as list_mock: list_mock.return_value = [item1, item1] - prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM1']) + prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM1'], "8") list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') self.assertEqual([price2['id'], price1['id']], prices) @@ -366,7 +367,7 @@ def test_generate_order_with_preset(self): 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) + mock_get_ids.assert_called_once_with(pkg, items, 8) self.assertEqual(expected_order, order) def test_generate_order(self): @@ -388,7 +389,7 @@ def test_generate_order(self): mock_pkg.assert_called_once_with(pkg, mask='id') mock_preset.assert_not_called() - mock_get_ids.assert_called_once_with(pkg, items) + mock_get_ids.assert_called_once_with(pkg, items, None) self.assertEqual(expected_order, order) def test_verify_order(self): @@ -508,7 +509,7 @@ def test_get_location_id_keyname(self): def test_get_location_id_exception(self): locations = self.set_mock('SoftLayer_Location', 'getDatacenters') locations.return_value = [] - self.assertRaises(exceptions.SoftLayerError, self.ordering.get_location_id, "BURMUDA") + self.assertRaises(exceptions.SoftLayerError, self.ordering.get_location_id, "BURMUDA") def test_get_location_id_int(self): dc_id = self.ordering.get_location_id(1234) @@ -526,7 +527,7 @@ def test_location_group_id_none(self): 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']) + prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM2'], "8") list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') self.assertEqual([price1['id'], price2['id']], prices) @@ -543,7 +544,28 @@ def test_location_groud_id_empty(self): 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']) + prices = self.ordering.get_price_id_list('PACKAGE_KEYNAME', ['ITEM1', 'ITEM2'], "8") list_mock.assert_called_once_with('PACKAGE_KEYNAME', mask='id, itemCategory, keyName, prices[categories]') self.assertEqual([price1['id'], price2['id']], prices) + + def test_get_item_price_id_without_capacity_restriction(self): + category1 = {'categoryCode': 'cat1'} + category2 = {'categoryCode': 'cat2'} + prices = [{'id': 1234, 'locationGroupId': '', 'categories': [category1]}, + {'id': 2222, 'locationGroupId': 509, 'categories': [category2]}] + + price_id = self.ordering.get_item_price_id("8", prices) + + self.assertEqual(1234, price_id) + + def test_get_item_price_id_with_capacity_restriction(self): + category1 = {'categoryCode': 'cat1'} + price1 = [{'id': 1234, 'locationGroupId': '', "capacityRestrictionMaximum": "16", + "capacityRestrictionMinimum": "1", 'categories': [category1]}, + {'id': 2222, 'locationGroupId': '', "capacityRestrictionMaximum": "56", + "capacityRestrictionMinimum": "36", 'categories': [category1]}] + + price_id = self.ordering.get_item_price_id("8", price1) + + self.assertEqual(1234, price_id) diff --git a/tests/managers/sshkey_tests.py b/tests/managers/sshkey_tests.py index b21d0131f..19a0e2317 100644 --- a/tests/managers/sshkey_tests.py +++ b/tests/managers/sshkey_tests.py @@ -19,7 +19,7 @@ def test_add_key(self): notes='My notes') args = ({ - 'key': 'pretend this is a public SSH key', + 'key': 'pretend this is a public SSH key', 'label': 'Test label', 'notes': 'My notes', },) diff --git a/tests/managers/vs_capacity_tests.py b/tests/managers/vs_capacity_tests.py new file mode 100644 index 000000000..43db16afb --- /dev/null +++ b/tests/managers/vs_capacity_tests.py @@ -0,0 +1,185 @@ +""" + SoftLayer.tests.managers.vs_capacity_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. + +""" +import mock + +import SoftLayer +from SoftLayer import fixtures +from SoftLayer.fixtures import SoftLayer_Product_Package +from SoftLayer import testing + + +class VSCapacityTests(testing.TestCase): + + def set_up(self): + self.manager = SoftLayer.CapacityManager(self.client) + amock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + amock.return_value = fixtures.SoftLayer_Product_Package.RESERVED_CAPACITY + + def test_list(self): + self.manager.list() + self.assert_called_with('SoftLayer_Account', 'getReservedCapacityGroups') + + def test_get_object(self): + self.manager.get_object(100) + self.assert_called_with('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject', identifier=100) + + def test_get_object_mask(self): + mask = "mask[id]" + self.manager.get_object(100, mask=mask) + self.assert_called_with('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject', identifier=100, mask=mask) + + def test_get_create_options(self): + self.manager.get_create_options() + self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=1059, mask=mock.ANY) + + def test_get_available_routers(self): + + result = self.manager.get_available_routers() + package_filter = {'keyName': {'operation': 'RESERVED_CAPACITY'}} + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects', mask=mock.ANY, filter=package_filter) + self.assert_called_with('SoftLayer_Product_Package', 'getRegions', mask=mock.ANY) + self.assert_called_with('SoftLayer_Network_Pod', 'getAllObjects') + self.assertEqual(result[0]['keyname'], 'WASHINGTON07') + + def test_create(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + self.manager.create( + name='TEST', backend_router_id=1, flavor='B1_1X2_1_YEAR_TERM', instances=5) + + expected_args = { + 'orderContainers': [ + { + 'backendRouterId': 1, + 'name': 'TEST', + 'packageId': 1059, + 'location': 0, + 'quantity': 5, + 'useHourlyPricing': True, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_ReservedCapacity', + 'prices': [{'id': 217561} + ] + } + ] + } + + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects') + self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=1059) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder', args=(expected_args,)) + + def test_create_test(self): + item_mock = self.set_mock('SoftLayer_Product_Package', 'getItems') + item_mock.return_value = SoftLayer_Product_Package.getItems_RESERVED_CAPACITY + self.manager.create( + name='TEST', backend_router_id=1, flavor='B1_1X2_1_YEAR_TERM', instances=5, test=True) + + expected_args = { + 'orderContainers': [ + { + 'backendRouterId': 1, + 'name': 'TEST', + 'packageId': 1059, + 'location': 0, + 'quantity': 5, + 'useHourlyPricing': True, + 'complexType': 'SoftLayer_Container_Product_Order_Virtual_ReservedCapacity', + 'prices': [{'id': 217561}], + + } + ] + } + + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects') + self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=1059) + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder', args=(expected_args,)) + + def test_create_guest(self): + amock = self.set_mock('SoftLayer_Product_Package', 'getItems') + amock.return_value = fixtures.SoftLayer_Product_Package.getItems_1_IPV6_ADDRESS + guest_object = { + 'boot_mode': None, + 'disks': (), + 'domain': 'test.com', + 'hostname': 'A1538172419', + 'hourly': True, + 'ipv6': True, + 'local_disk': None, + 'os_code': 'UBUNTU_LATEST_64', + 'primary_disk': '25', + 'private': False, + 'private_subnet': None, + 'public_subnet': None, + 'ssh_keys': [1234] + } + self.manager.create_guest(123, False, guest_object) + expectedGenerate = { + 'startCpus': None, + 'maxMemory': None, + 'hostname': 'A1538172419', + 'domain': 'test.com', + 'localDiskFlag': None, + 'hourlyBillingFlag': True, + 'supplementalCreateObjectOptions': { + 'bootMode': None, + 'flavorKeyName': 'B1_1X2X25' + }, + 'operatingSystemReferenceCode': 'UBUNTU_LATEST_64', + 'datacenter': {'name': 'dal13'}, + 'sshKeys': [{'id': 1234}], + 'localDiskFlag': False + } + + self.assert_called_with('SoftLayer_Virtual_ReservedCapacityGroup', 'getObject', mask=mock.ANY) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=(expectedGenerate,)) + self.assert_called_with('SoftLayer_Product_Package', 'getAllObjects') + # id=1059 comes from fixtures.SoftLayer_Product_Order.RESERVED_CAPACITY, production is 859 + self.assert_called_with('SoftLayer_Product_Package', 'getItems', identifier=1059) + self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') + + def test_create_guest_no_flavor(self): + guest_object = { + 'boot_mode': None, + 'disks': (), + 'domain': 'test.com', + 'hostname': 'A1538172419', + 'hourly': True, + 'ipv6': True, + 'local_disk': None, + 'os_code': 'UBUNTU_LATEST_64', + 'private': False, + 'private_subnet': None, + 'public_subnet': None, + 'ssh_keys': [1234] + } + self.assertRaises(SoftLayer.SoftLayerError, self.manager.create_guest, 123, False, guest_object) + + def test_create_guest_testing(self): + amock = self.set_mock('SoftLayer_Product_Package', 'getItems') + amock.return_value = fixtures.SoftLayer_Product_Package.getItems_1_IPV6_ADDRESS + guest_object = { + 'boot_mode': None, + 'disks': (), + 'domain': 'test.com', + 'hostname': 'A1538172419', + 'hourly': True, + 'ipv6': True, + 'local_disk': None, + 'os_code': 'UBUNTU_LATEST_64', + 'primary_disk': '25', + 'private': False, + 'private_subnet': None, + 'public_subnet': None, + 'ssh_keys': [1234] + } + self.manager.create_guest(123, True, guest_object) + self.assert_called_with('SoftLayer_Product_Order', 'verifyOrder') + + def test_flavor_string(self): + from SoftLayer.managers.vs_capacity import _flavor_string as _flavor_string + result = _flavor_string('B1_1X2_1_YEAR_TERM', '25') + self.assertEqual('B1_1X2X25', result) diff --git a/tests/managers/vs_tests.py b/tests/managers/vs_tests.py index 97b6c5c4d..c24124dd6 100644 --- a/tests/managers/vs_tests.py +++ b/tests/managers/vs_tests.py @@ -792,10 +792,10 @@ def test_edit_full(self): self.assertEqual(result, True) args = ({ - 'hostname': 'new-host', - 'domain': 'new.sftlyr.ws', - 'notes': 'random notes', - },) + 'hostname': 'new-host', + 'domain': 'new.sftlyr.ws', + 'notes': 'random notes', + },) self.assert_called_with('SoftLayer_Virtual_Guest', 'editObject', identifier=100, args=args) diff --git a/tools/requirements.txt b/tools/requirements.txt index bed36edb5..17bba9467 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,6 +1,6 @@ -requests >= 2.18.4 +requests == 2.19.1 click >= 5, < 7 prettytable >= 0.7.0 six >= 1.7.0 prompt_toolkit -urllib3 +urllib3 == 1.22 diff --git a/tools/test-requirements.txt b/tools/test-requirements.txt index c9a94de27..138fe84db 100644 --- a/tools/test-requirements.txt +++ b/tools/test-requirements.txt @@ -4,5 +4,5 @@ pytest-cov mock sphinx testtools -urllib3 -requests >= 2.18.4 +urllib3 == 1.22 +requests == 2.19.1