From 38dd6d131d5ed9760aaddfe38b15866c4da3f45b Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Wed, 24 Aug 2016 14:20:12 -0700 Subject: [PATCH 01/49] Extend CLI commands, add new ones New commands: - nas grant-access - nas revoke-access - nas detail - hardware status (status based on orderId rather than hardwareId) Updated commands: - hardware detail: parse out specific fields json format - hardware create: create multiple servers janky --output-json flag to dump specific parts in JSON TODO: revert some/all commands to use existing --format=json flag --- SoftLayer/CLI/hardware/create.py | 29 +++++++++++++++++++---------- SoftLayer/CLI/hardware/detail.py | 25 ++++++++++++++++++++++++- SoftLayer/CLI/hardware/list.py | 7 ++++++- SoftLayer/CLI/hardware/status.py | 16 ++++++++++++++++ SoftLayer/CLI/nas/detail.py | 19 +++++++++++++++++++ SoftLayer/CLI/nas/grant_access.py | 23 +++++++++++++++++++++++ SoftLayer/CLI/nas/revoke_access.py | 23 +++++++++++++++++++++++ SoftLayer/CLI/routes.py | 3 +++ SoftLayer/managers/hardware.py | 30 ++++++++++++++++++++++++------ SoftLayer/managers/ordering.py | 6 ++++++ 10 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 SoftLayer/CLI/hardware/status.py create mode 100644 SoftLayer/CLI/nas/detail.py create mode 100644 SoftLayer/CLI/nas/grant_access.py create mode 100644 SoftLayer/CLI/nas/revoke_access.py diff --git a/SoftLayer/CLI/hardware/create.py b/SoftLayer/CLI/hardware/create.py index 472cec2e2..9b8dc6d8c 100644 --- a/SoftLayer/CLI/hardware/create.py +++ b/SoftLayer/CLI/hardware/create.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -12,8 +13,8 @@ @click.command(epilog="See 'slcli server create-options' for valid options.") -@click.option('--hostname', '-H', - help="Host portion of the FQDN", +@click.option('--hostnames', '-H', + help="Host portions of the FQDN", required=True, prompt=True) @click.option('--domain', '-D', @@ -62,6 +63,8 @@ type=click.INT, help="Wait until the server is finished provisioning for up to " "X seconds before returning") +@click.option('--quantity', default=1, type=click.INT) +@click.option('--output-json', is_flag=True) @environment.pass_env def cli(env, **args): """Order/create a dedicated server.""" @@ -75,7 +78,7 @@ def cli(env, **args): ssh_keys.append(key_id) order = { - 'hostname': args['hostname'], + 'hostnames': args['hostnames'].split(','), 'domain': args['domain'], 'size': args['size'], 'location': args.get('datacenter'), @@ -86,6 +89,7 @@ def cli(env, **args): 'port_speed': args.get('port_speed'), 'no_public': args.get('no_public') or False, 'extras': args.get('extra'), + 'quantity': args.get('quantity'), } # Do not create hardware server with --test or --export @@ -129,11 +133,16 @@ def cli(env, **args): result = mgr.place_order(**order) - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - table.add_row(['id', result['orderId']]) - table.add_row(['created', result['orderDate']]) - output = table + if args['output_json']: + env.fout(json.dumps({'orderId': result['orderId'], + 'created': result['orderDate']})) - env.fout(output) + else: + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['id', result['orderId']]) + table.add_row(['created', result['orderDate']]) + output = table + + env.fout(output) diff --git a/SoftLayer/CLI/hardware/detail.py b/SoftLayer/CLI/hardware/detail.py index 4382fc362..117ed2d1b 100644 --- a/SoftLayer/CLI/hardware/detail.py +++ b/SoftLayer/CLI/hardware/detail.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -14,8 +15,10 @@ @click.argument('identifier') @click.option('--passwords', is_flag=True, help='Show passwords (check over your shoulder!)') @click.option('--price', is_flag=True, help='Show associated prices') +@click.option('--output-json', is_flag=True, default=False) +@click.option('--verbose', is_flag=True, default=False) @environment.pass_env -def cli(env, identifier, passwords, price): +def cli(env, identifier, passwords, price, output_json, verbose): """Get details for a hardware device.""" hardware = SoftLayer.HardwareManager(env.client) @@ -28,6 +31,26 @@ def cli(env, identifier, passwords, price): result = hardware.get_hardware(hardware_id) result = utils.NestedDict(result) + if output_json: + if verbose: + env.fout(json.dumps(result)) + else: + partial = {k: result[k] for k in + ['id', + 'primaryIpAddress', + 'primaryBackendIpAddress', + 'hostname', + 'fullyQualifiedDomainName', + 'operatingSystem' + ]} + partial['osPlatform'] = partial \ + ['operatingSystem'] \ + ['softwareLicense'] \ + ['softwareDescription'] \ + ['name'] + env.fout(json.dumps(partial)) + return + operating_system = utils.lookup(result, 'operatingSystem', 'softwareLicense', 'softwareDescription') or {} memory = formatting.gb(result.get('memoryCapacity', 0)) owner = None diff --git a/SoftLayer/CLI/hardware/list.py b/SoftLayer/CLI/hardware/list.py index 1a607880f..bb2f210b7 100644 --- a/SoftLayer/CLI/hardware/list.py +++ b/SoftLayer/CLI/hardware/list.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import columns as column_helper @@ -59,8 +60,9 @@ help='How many results to get in one api call, default is 100', default=100, show_default=True) +@click.option('--output-json', is_flag=True, default=False) @environment.pass_env -def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, columns, limit): +def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, columns, limit, output_json): """List hardware servers.""" manager = SoftLayer.HardwareManager(env.client) @@ -74,6 +76,9 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, co mask="mask(SoftLayer_Hardware_Server)[%s]" % columns.mask(), limit=limit) + if output_json: + env.fout(json.dumps({'hardware': servers})) + return table = formatting.Table(columns.columns) table.sortby = sortby diff --git a/SoftLayer/CLI/hardware/status.py b/SoftLayer/CLI/hardware/status.py new file mode 100644 index 000000000..4238915eb --- /dev/null +++ b/SoftLayer/CLI/hardware/status.py @@ -0,0 +1,16 @@ +import click +import json + +import SoftLayer +from SoftLayer.CLI import environment + + +@click.command() +@click.argument('order_id', required=True) +@environment.pass_env +def cli(env, order_id): + hardware = SoftLayer.HardwareManager(env.client) + + env.fout(json.dumps({ + 'statuses': hardware.get_hardware_status_from_order(order_id) + })) diff --git a/SoftLayer/CLI/nas/detail.py b/SoftLayer/CLI/nas/detail.py new file mode 100644 index 000000000..3c5a62134 --- /dev/null +++ b/SoftLayer/CLI/nas/detail.py @@ -0,0 +1,19 @@ +import click +import json + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + +NAS_PROPERTIES = 'id,username,serviceResourceBackendIpAddress' + + +@click.command() +@click.argument('nasid', type=int) +@environment.pass_env +def cli(env, nasid): + account = env.client['Account'] + for nas in account.getNasNetworkStorage(mask=NAS_PROPERTIES): + if int(nas['id']) == nasid: + env.fout(json.dumps(nas)) + break diff --git a/SoftLayer/CLI/nas/grant_access.py b/SoftLayer/CLI/nas/grant_access.py new file mode 100644 index 000000000..8e6ad8951 --- /dev/null +++ b/SoftLayer/CLI/nas/grant_access.py @@ -0,0 +1,23 @@ +import click +import json + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import hardware + +STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' + +@click.command() +@click.option('--order-id', required=True) +@click.option('--storage-id', required=True) +@environment.pass_env +def cli(env, order_id, storage_id): + hardware = SoftLayer.HardwareManager(env.client) + + hardware_ids = hardware.get_hardware_ids(order_id) + for hardware_id in hardware_ids: + payload = {'id': hardware_id} + env.client[STORAGE_ENDPOINT].allowAccessFromHardware( + payload, id=storage_id + ) diff --git a/SoftLayer/CLI/nas/revoke_access.py b/SoftLayer/CLI/nas/revoke_access.py new file mode 100644 index 000000000..289fbfa3e --- /dev/null +++ b/SoftLayer/CLI/nas/revoke_access.py @@ -0,0 +1,23 @@ +import click +import json + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import hardware + +STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' + +@click.command() +@click.option('--order-id', required=True) +@click.option('--storage-id', required=True) +@environment.pass_env +def cli(env, order_id, storage_id): + hardware = SoftLayer.HardwareManager(env.client) + + hardware_ids = hardware.get_hardware_ids(order_id) + for hardware_id in hardware_ids: + payload = {'id': hardware_id} + env.client[STORAGE_ENDPOINT].removeAccessFromHardware( + payload, id=storage_id + ) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 196616a8e..78c08665d 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -196,6 +196,8 @@ ('nas', 'SoftLayer.CLI.nas'), ('nas:list', 'SoftLayer.CLI.nas.list:cli'), ('nas:credentials', 'SoftLayer.CLI.nas.credentials:cli'), + ('nas:grant-access', 'SoftLayer.CLI.nas.grant_access:cli'), + ('nas:detail', 'SoftLayer.CLI.nas.detail:cli'), ('object-storage', 'SoftLayer.CLI.object_storage'), ('object-storage:accounts', @@ -232,6 +234,7 @@ ('hardware:update-firmware', 'SoftLayer.CLI.hardware.update_firmware:cli'), ('hardware:rescue', 'SoftLayer.CLI.hardware.power:rescue'), ('hardware:ready', 'SoftLayer.CLI.hardware.ready:cli'), + ('hardware:status', 'SoftLayer.CLI.hardware.status:cli'), ('securitygroup', 'SoftLayer.CLI.securitygroup'), ('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'), diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index c105b3b5d..bebc70c81 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -434,6 +434,16 @@ def get_create_options(self): 'extras': extras, } + def get_hardware_status_from_order(self, order_id): + BILLING_ORDER_MASK = 'mask[orderTopLevelItems[hostName,billingItem[id,provisionTransaction[hardware[id,hardwareStatus]]]]]' + order = self.ordering_manager.get_order(order_id, BILLING_ORDER_MASK) + return [_get_hardware_status_from_billing(item) + for item in _get_billing_items(order)] + + def get_hardware_ids(self, order_id): + return [hw['hardwareId'] + for hw in self.get_hardware_status_from_order(order_id)] + @retry(logger=LOGGER) def _get_package(self): """Get the package related to simple hardware ordering.""" @@ -458,7 +468,7 @@ def _get_package(self): def _generate_create_dict(self, size=None, - hostname=None, + hostnames=None, domain=None, location=None, os=None, @@ -467,7 +477,8 @@ def _generate_create_dict(self, post_uri=None, hourly=True, no_public=False, - extras=None): + extras=None, + quantity=1): """Translates arguments into a dictionary for creating a server.""" extras = extras or [] @@ -499,19 +510,19 @@ def _generate_create_dict(self, prices.append(_get_extra_price_id(package['items'], extra, hourly, location=location)) - - hardware = { + hardware = [{ 'hostname': hostname, 'domain': domain, - } + } for hostname in hostnames] order = { - 'hardware': [hardware], + 'hardware': hardware, 'location': location['keyname'], 'prices': [{'id': price} for price in prices], 'packageId': package['id'], 'presetId': _get_preset_id(package, size), 'useHourlyPricing': hourly, + 'quantity': quantity } if post_uri: @@ -810,3 +821,10 @@ def _get_preset_id(package, size): return preset['id'] raise SoftLayer.SoftLayerError("Could not find valid size for: '%s'" % size) + +def _get_billing_items(order): + return [top['billingItem'] for top in order['orderTopLevelItems'] + if 'billingItem' in top] + +def _get_hardware_status_from_billing(billing_item): + return billing_item['provisionTransaction'] diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index 9b1fadec0..2847c6242 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -518,3 +518,9 @@ def get_location_id(self, location): if len(datacenter) != 1: raise exceptions.SoftLayerError("Unable to find location: %s" % location) return datacenter[0]['id'] + + def get_order(self, order_id, mask=None): + return self.client['Billing_Order'].getObject( + id=order_id, + mask=mask + ) From 1d58edf99079dc8c688b4999340f9403b3a33703 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Thu, 1 Sep 2016 23:25:30 -0700 Subject: [PATCH 02/49] ssh keys for all, not just the first --- SoftLayer/managers/hardware.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index bebc70c81..59081892a 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -529,7 +529,9 @@ def _generate_create_dict(self, order['provisionScripts'] = [post_uri] if ssh_keys: - order['sshKeys'] = [{'sshKeyIds': ssh_keys}] + # need a copy of the key ids for each host, otherwise it + # will only set up keys on the first host + order['sshKeys'] = [{'sshKeyIds': ssh_keys}] * quantity return order From daf67036e091f9c9883e5cbe61bd8d3bb072f58a Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Tue, 20 Sep 2016 14:20:31 -0700 Subject: [PATCH 03/49] CLI hw create command currently broken for GPUs - missing Disk Controller - add price to specify the disk controller until cli is fixed properly --- SoftLayer/managers/hardware.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 59081892a..fdc83cd22 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -510,6 +510,11 @@ def _generate_create_dict(self, prices.append(_get_extra_price_id(package['items'], extra, hourly, location=location)) + + # Hack to add Disk Controller for K80 order to go through + if size == 'D2620V4_128GB_2X800GB_SSD_RAID_1_K80_GPU2': + prices.append(22482) + hardware = [{ 'hostname': hostname, 'domain': domain, From 18cbbf6aaedaa1120a882f2b7ead039dbaf67ad4 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Thu, 27 Oct 2016 11:04:02 -0700 Subject: [PATCH 04/49] Enable the cli to create multiple vm instances in one request. --- SoftLayer/CLI/virt/create.py | 234 ++++++++++++++++++----------------- 1 file changed, 123 insertions(+), 111 deletions(-) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index a0997704e..a231aa700 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -67,96 +68,100 @@ def _update_with_like_args(ctx, _, value): def _parse_create_args(client, args): - """Converts CLI arguments to args for VSManager.create_instance. + """Converts CLI arguments to args for VSManager.create_instances. :param dict args: CLI arguments """ - data = { - "hourly": args['billing'] == 'hourly', - "domain": args['domain'], - "hostname": args['hostname'], - "private": args['private'], - "dedicated": args['dedicated'], - "disks": args['disk'], - "cpus": args.get('cpu', None), - "memory": args.get('memory', None), - "flavor": args.get('flavor', None), - "boot_mode": args.get('boot_mode', None) - } - - # The primary disk is included in the flavor and the local_disk flag is not needed - # Setting it to None prevents errors from the flag not matching the flavor - if not args.get('san') and args.get('flavor'): - data['local_disk'] = None - else: - data['local_disk'] = not args['san'] - - if args.get('os'): - data['os_code'] = args['os'] - - if args.get('image'): - if args.get('image').isdigit(): - image_mgr = SoftLayer.ImageManager(client) - image_details = image_mgr.get_image(args.get('image'), - mask="id,globalIdentifier") - data['image_id'] = image_details['globalIdentifier'] + config_list = [] + + for hostname in args['hostnames'].split(','): + data = { + "hourly": args['billing'] == 'hourly', + "domain": args['domain'], + "hostname": hostname, + "private": args['private'], + "dedicated": args['dedicated'], + "disks": args['disk'], + "cpus": args.get('cpu', None), + "memory": args.get('memory', None), + "flavor": args.get('flavor', None), + "boot_mode": args.get('boot_mode', None) + } + + # The primary disk is included in the flavor and the local_disk flag is not needed + # Setting it to None prevents errors from the flag not matching the flavor + if not args.get('san') and args.get('flavor'): + data['local_disk'] = None else: - data['image_id'] = args['image'] + data['local_disk'] = not args['san'] + + if args.get('os'): + data['os_code'] = args['os'] - if args.get('datacenter'): - data['datacenter'] = args['datacenter'] + if args.get('image'): + if args.get('image').isdigit(): + image_mgr = SoftLayer.ImageManager(client) + image_details = image_mgr.get_image(args.get('image'), + mask="id,globalIdentifier") + data['image_id'] = image_details['globalIdentifier'] + else: + data['image_id'] = args['image'] - if args.get('network'): - data['nic_speed'] = args.get('network') + if args.get('datacenter'): + data['datacenter'] = args['datacenter'] - if args.get('userdata'): - data['userdata'] = args['userdata'] - elif args.get('userfile'): - with open(args['userfile'], 'r') as userfile: - data['userdata'] = userfile.read() + if args.get('network'): + data['nic_speed'] = args.get('network') - if args.get('postinstall'): - data['post_uri'] = args.get('postinstall') + if args.get('userdata'): + data['userdata'] = args['userdata'] + elif args.get('userfile'): + with open(args['userfile'], 'r') as userfile: + data['userdata'] = userfile.read() - # Get the SSH keys - if args.get('key'): - keys = [] - for key in args.get('key'): - resolver = SoftLayer.SshKeyManager(client).resolve_ids - key_id = helpers.resolve_id(resolver, key, 'SshKey') - keys.append(key_id) - data['ssh_keys'] = keys + if args.get('postinstall'): + data['post_uri'] = args.get('postinstall') - if args.get('vlan_public'): - data['public_vlan'] = args['vlan_public'] + # Get the SSH keys + if args.get('key'): + keys = [] + for key in args.get('key'): + resolver = SoftLayer.SshKeyManager(client).resolve_ids + key_id = helpers.resolve_id(resolver, key, 'SshKey') + keys.append(key_id) + data['ssh_keys'] = keys - if args.get('vlan_private'): - data['private_vlan'] = args['vlan_private'] + if args.get('vlan_public'): + data['public_vlan'] = args['vlan_public'] - data['public_subnet'] = args.get('subnet_public', None) + if args.get('vlan_private'): + data['private_vlan'] = args['vlan_private'] - data['private_subnet'] = args.get('subnet_private', None) + data['public_subnet'] = args.get('subnet_public', None) - if args.get('public_security_group'): - pub_groups = args.get('public_security_group') - data['public_security_groups'] = [group for group in pub_groups] + data['private_subnet'] = args.get('subnet_private', None) - if args.get('private_security_group'): - priv_groups = args.get('private_security_group') - data['private_security_groups'] = [group for group in priv_groups] + if args.get('public_security_group'): + pub_groups = args.get('public_security_group') + data['public_security_groups'] = [group for group in pub_groups] - if args.get('tag'): - data['tags'] = ','.join(args['tag']) + if args.get('private_security_group'): + priv_groups = args.get('private_security_group') + data['private_security_groups'] = [group for group in priv_groups] - if args.get('host_id'): - data['host_id'] = args['host_id'] + if args.get('tag'): + data['tags'] = ','.join(args['tag']) - return data + if args.get('host_id'): + data['host_id'] = args['host_id'] + + config_list.append(data) + return config_list @click.command(epilog="See 'slcli vs create-options' for valid options") -@click.option('--hostname', '-H', - help="Host portion of the FQDN", +@click.option('--hostnames', '-H', + help="Hosts portion of the FQDN", required=True, prompt=True) @click.option('--domain', '-D', @@ -253,6 +258,7 @@ def _parse_create_args(client, args): type=click.INT, help="Wait until VS is finished provisioning for up to X " "seconds before returning") +@click.option('--output-json', is_flag=True) @environment.pass_env def cli(env, **args): """Order/create virtual servers.""" @@ -265,43 +271,44 @@ def cli(env, **args): table = formatting.Table(['Item', 'cost']) table.align['Item'] = 'r' table.align['cost'] = 'r' - data = _parse_create_args(env.client, args) + config_list = _parse_create_args(env.client, args) output = [] if args.get('test'): - result = vsi.verify_create_instance(**data) - total_monthly = 0.0 - total_hourly = 0.0 - - table = formatting.Table(['Item', 'cost']) - table.align['Item'] = 'r' - table.align['cost'] = 'r' - - for price in result['prices']: - total_monthly += float(price.get('recurringFee', 0.0)) - total_hourly += float(price.get('hourlyRecurringFee', 0.0)) + for config in config_list: + result = vsi.verify_create_instance(**config) + total_monthly = 0.0 + total_hourly = 0.0 + + table = formatting.Table(['Item', 'cost']) + table.align['Item'] = 'r' + table.align['cost'] = 'r' + + for price in result['prices']: + total_monthly += float(price.get('recurringFee', 0.0)) + total_hourly += float(price.get('hourlyRecurringFee', 0.0)) + if args.get('billing') == 'hourly': + rate = "%.2f" % float(price['hourlyRecurringFee']) + elif args.get('billing') == 'monthly': + rate = "%.2f" % float(price['recurringFee']) + + table.add_row([price['item']['description'], rate]) + + total = 0 if args.get('billing') == 'hourly': - rate = "%.2f" % float(price['hourlyRecurringFee']) + total = total_hourly elif args.get('billing') == 'monthly': - rate = "%.2f" % float(price['recurringFee']) - - table.add_row([price['item']['description'], rate]) - - total = 0 - if args.get('billing') == 'hourly': - total = total_hourly - elif args.get('billing') == 'monthly': - total = total_monthly - - billing_rate = 'monthly' - if args.get('billing') == 'hourly': - billing_rate = 'hourly' - table.add_row(['Total %s cost' % billing_rate, "%.2f" % total]) - output.append(table) - output.append(formatting.FormattedItem( - None, - ' -- ! Prices reflected here are retail and do not ' - 'take account level discounts and are not guaranteed.')) + total = total_monthly + + billing_rate = 'monthly' + if args.get('billing') == 'hourly': + billing_rate = 'hourly' + table.add_row(['Total %s cost' % billing_rate, "%.2f" % total]) + output.append(table) + output.append(formatting.FormattedItem( + None, + ' -- ! Prices reflected here are retail and do not ' + 'take account level discounts and are not guaranteed.')) if args['export']: export_file = args.pop('export') @@ -314,15 +321,7 @@ def cli(env, **args): "This action will incur charges on your account. Continue?")): raise exceptions.CLIAbort('Aborting virtual server order.') - result = vsi.create_instance(**data) - - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - table.add_row(['id', result['id']]) - table.add_row(['created', result['createDate']]) - table.add_row(['guid', result['globalIdentifier']]) - output.append(table) + result = vsi.create_instances(config_list) if args.get('wait'): ready = vsi.wait_for_ready(result['id'], args.get('wait') or 1) @@ -331,7 +330,20 @@ def cli(env, **args): env.out(env.fmt(output)) raise exceptions.CLIHalt(code=1) - env.fout(output) + if args['output_json']: + env.fout(json.dumps(result)) + else: + for instance_data in result: + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['id', instance_data['id']]) + table.add_row(['hostname', instance_data['hostname']]) + table.add_row(['created', instance_data['createDate']]) + table.add_row(['uuid', instance_data['uuid']]) + output.append(table) + + env.fout(output) def _validate_args(env, args): From f7663259cf1b3779dd5e723c655cd44f6dca1aff Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Thu, 27 Oct 2016 11:53:56 -0700 Subject: [PATCH 05/49] Add json support for instance-id ready cmd. --- SoftLayer/CLI/virt/ready.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/ready.py b/SoftLayer/CLI/virt/ready.py index c41680436..807855073 100644 --- a/SoftLayer/CLI/virt/ready.py +++ b/SoftLayer/CLI/virt/ready.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -12,13 +13,19 @@ @click.command() @click.argument('identifier') @click.option('--wait', default=0, show_default=True, type=click.INT, help="Seconds to wait") +@click.option('--output-json', is_flag=True, default=False) @environment.pass_env -def cli(env, identifier, wait): +def cli(env, identifier, wait, output_json=False): """Check if a virtual server is ready.""" vsi = SoftLayer.VSManager(env.client) vs_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS') ready = vsi.wait_for_ready(vs_id, wait) + + if output_json: + env.fout(json.dumps({'ready': bool(ready)})) + return + if ready: env.fout("READY") else: From 69a661dedeb719e269c551b04a9ee1bf5cdb1909 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Thu, 27 Oct 2016 11:58:09 -0700 Subject: [PATCH 06/49] Add json support for vm detail command. --- SoftLayer/CLI/virt/detail.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 7a418dadb..4e2692c8c 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -4,6 +4,7 @@ import logging import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -20,8 +21,11 @@ is_flag=True, help='Show passwords (check over your shoulder!)') @click.option('--price', is_flag=True, help='Show associated prices') +@click.option('--output-json', is_flag=True, default=False) +@click.option('--verbose', is_flag=True, default=False) @environment.pass_env -def cli(env, identifier, passwords=False, price=False): +def cli(env, identifier, passwords=False, price=False, output_json=False, + verbose=False): """Get details for a virtual server.""" vsi = SoftLayer.VSManager(env.client) @@ -33,6 +37,26 @@ def cli(env, identifier, passwords=False, price=False): result = vsi.get_instance(vs_id) result = utils.NestedDict(result) + if output_json: + if verbose: + env.fout(json.dumps(result)) + else: + partial = {k: result[k] for k in + ['id', + 'primaryIpAddress', + 'primaryBackendIpAddress', + 'hostname', + 'fullyQualifiedDomainName', + 'operatingSystem' + ]} + partial['osPlatform'] = partial\ + ['operatingSystem']\ + ['softwareLicense']\ + ['softwareDescription']\ + ['name'] + env.fout(json.dumps(partial)) + return + table.add_row(['id', result['id']]) table.add_row(['guid', result['globalIdentifier']]) table.add_row(['hostname', result['hostname']]) From 66fa9e2ee8ac8869d0c9af4a4a82077ae7ef8b29 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Fri, 28 Oct 2016 17:38:12 -0700 Subject: [PATCH 07/49] Add support to grant access to a vm. --- SoftLayer/CLI/nas/grant_vm_access.py | 17 +++++++++++++++++ SoftLayer/CLI/routes.py | 1 + 2 files changed, 18 insertions(+) create mode 100644 SoftLayer/CLI/nas/grant_vm_access.py diff --git a/SoftLayer/CLI/nas/grant_vm_access.py b/SoftLayer/CLI/nas/grant_vm_access.py new file mode 100644 index 000000000..8921067ff --- /dev/null +++ b/SoftLayer/CLI/nas/grant_vm_access.py @@ -0,0 +1,17 @@ +import click +import json + +import SoftLayer +from SoftLayer.CLI import environment + +STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' + +@click.command() +@click.option('--vm-id', required=True) +@click.option('--storage-id', required=True) +@environment.pass_env +def cli(env, vm_id, storage_id): + payload = {'id': vm_id} + env.client[STORAGE_ENDPOINT].allowAccessFromVirtualGuest( + payload, id=storage_id + ) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 78c08665d..cdaedfb62 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -197,6 +197,7 @@ ('nas:list', 'SoftLayer.CLI.nas.list:cli'), ('nas:credentials', 'SoftLayer.CLI.nas.credentials:cli'), ('nas:grant-access', 'SoftLayer.CLI.nas.grant_access:cli'), + ('nas:grant-vm-access', 'SoftLayer.CLI.nas.grant_vm_access:cli'), ('nas:detail', 'SoftLayer.CLI.nas.detail:cli'), ('object-storage', 'SoftLayer.CLI.object_storage'), From 98fcd5270251c49747ccd555228f4bbffa36ecf1 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Sun, 30 Oct 2016 13:29:23 -0700 Subject: [PATCH 08/49] Get rid of grant_vm_access. Move the vm operations into grant_access and remove_access. --- SoftLayer/CLI/nas/grant_access.py | 24 +++++++++++++++++------- SoftLayer/CLI/nas/grant_vm_access.py | 17 ----------------- SoftLayer/CLI/nas/revoke_access.py | 25 ++++++++++++++++++------- SoftLayer/CLI/routes.py | 2 +- 4 files changed, 36 insertions(+), 32 deletions(-) delete mode 100644 SoftLayer/CLI/nas/grant_vm_access.py diff --git a/SoftLayer/CLI/nas/grant_access.py b/SoftLayer/CLI/nas/grant_access.py index 8e6ad8951..41f57da53 100644 --- a/SoftLayer/CLI/nas/grant_access.py +++ b/SoftLayer/CLI/nas/grant_access.py @@ -9,15 +9,25 @@ STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' @click.command() -@click.option('--order-id', required=True) +@click.option('--order-id', required=False) +@click.option('--vm-id', required=False) @click.option('--storage-id', required=True) @environment.pass_env -def cli(env, order_id, storage_id): - hardware = SoftLayer.HardwareManager(env.client) +def cli(env, order_id, vm_id, storage_id): + if order_id and vm_id: + raise ValueError('Should only specify order_id or vm_id but not both.') - hardware_ids = hardware.get_hardware_ids(order_id) - for hardware_id in hardware_ids: - payload = {'id': hardware_id} - env.client[STORAGE_ENDPOINT].allowAccessFromHardware( + if order_id: + hardware = SoftLayer.HardwareManager(env.client) + hardware_ids = hardware.get_hardware_ids(order_id) + for hardware_id in hardware_ids: + payload = {'id': hardware_id} + env.client[STORAGE_ENDPOINT].allowAccessFromHardware( + payload, id=storage_id + ) + + if vm_id: + payload = {'id': vm_id} + env.client[STORAGE_ENDPOINT].allowAccessFromVirtualGuest( payload, id=storage_id ) diff --git a/SoftLayer/CLI/nas/grant_vm_access.py b/SoftLayer/CLI/nas/grant_vm_access.py deleted file mode 100644 index 8921067ff..000000000 --- a/SoftLayer/CLI/nas/grant_vm_access.py +++ /dev/null @@ -1,17 +0,0 @@ -import click -import json - -import SoftLayer -from SoftLayer.CLI import environment - -STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' - -@click.command() -@click.option('--vm-id', required=True) -@click.option('--storage-id', required=True) -@environment.pass_env -def cli(env, vm_id, storage_id): - payload = {'id': vm_id} - env.client[STORAGE_ENDPOINT].allowAccessFromVirtualGuest( - payload, id=storage_id - ) diff --git a/SoftLayer/CLI/nas/revoke_access.py b/SoftLayer/CLI/nas/revoke_access.py index 289fbfa3e..53c3ec119 100644 --- a/SoftLayer/CLI/nas/revoke_access.py +++ b/SoftLayer/CLI/nas/revoke_access.py @@ -9,15 +9,26 @@ STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' @click.command() -@click.option('--order-id', required=True) +@click.option('--order-id', required=False) +@click.option('--vm-id', required=False) @click.option('--storage-id', required=True) @environment.pass_env -def cli(env, order_id, storage_id): - hardware = SoftLayer.HardwareManager(env.client) +def cli(env, order_id, vm_id, storage_id): + if order_id and vm_id: + raise ValueError('Should only specify order_id or vm_id but not both.') - hardware_ids = hardware.get_hardware_ids(order_id) - for hardware_id in hardware_ids: - payload = {'id': hardware_id} - env.client[STORAGE_ENDPOINT].removeAccessFromHardware( + if order_id: + hardware = SoftLayer.HardwareManager(env.client) + + hardware_ids = hardware.get_hardware_ids(order_id) + for hardware_id in hardware_ids: + payload = {'id': hardware_id} + env.client[STORAGE_ENDPOINT].removeAccessFromHardware( + payload, id=storage_id + ) + + if vm_id: + payload = {'id': vm_id} + env.client[STORAGE_ENDPOINT].removeAccessFromVirtualGuest( payload, id=storage_id ) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index cdaedfb62..0e4ca6ee7 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -197,8 +197,8 @@ ('nas:list', 'SoftLayer.CLI.nas.list:cli'), ('nas:credentials', 'SoftLayer.CLI.nas.credentials:cli'), ('nas:grant-access', 'SoftLayer.CLI.nas.grant_access:cli'), - ('nas:grant-vm-access', 'SoftLayer.CLI.nas.grant_vm_access:cli'), ('nas:detail', 'SoftLayer.CLI.nas.detail:cli'), + ('nas:revoke-access', 'SoftLayer.CLI.nas.revoke_access:cli'), ('object-storage', 'SoftLayer.CLI.object_storage'), ('object-storage:accounts', From 3de124ee3b7bc93c433f00e6dde34c4c85ef3bfd Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Mon, 31 Oct 2016 16:02:45 -0700 Subject: [PATCH 09/49] Make the vm list command outputs json. --- SoftLayer/CLI/virt/list.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 3975ad333..16d6ea033 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import columns as column_helper @@ -66,9 +67,10 @@ help='How many results to get in one api call, default is 100', default=100, show_default=True) +@click.option('--output-json', is_flag=True, default=False) @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, - hourly, monthly, tag, columns, limit): + hourly, monthly, tag, columns, limit, output_json): """List virtual servers.""" vsi = SoftLayer.VSManager(env.client) @@ -84,6 +86,10 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, mask=columns.mask(), limit=limit) + if output_json: + env.fout(json.dumps({'vm': guests})) + return + table = formatting.Table(columns.columns) table.sortby = sortby for guest in guests: From 3b08008ce2ce0b2e82b389a938f22571e57d6268 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Tue, 1 Nov 2016 16:07:10 -0700 Subject: [PATCH 10/49] A list is not accepted. Json should start from {. --- SoftLayer/CLI/virt/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index a231aa700..2026909df 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -331,7 +331,7 @@ def cli(env, **args): raise exceptions.CLIHalt(code=1) if args['output_json']: - env.fout(json.dumps(result)) + env.fout(json.dumps({'statuses': result})) else: for instance_data in result: table = formatting.KeyValueTable(['name', 'value']) From 773ced9c36f8bc6ccc8cb0afc1a591b4c9e21296 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 5 May 2017 09:38:27 -0700 Subject: [PATCH 11/49] Revert "CLI hw create command currently broken for GPUs" This reverts commit f8e6ae09b41b410476d250dbf27c749eef881d2a. --- SoftLayer/managers/hardware.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index fdc83cd22..59081892a 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -510,11 +510,6 @@ def _generate_create_dict(self, prices.append(_get_extra_price_id(package['items'], extra, hourly, location=location)) - - # Hack to add Disk Controller for K80 order to go through - if size == 'D2620V4_128GB_2X800GB_SSD_RAID_1_K80_GPU2': - prices.append(22482) - hardware = [{ 'hostname': hostname, 'domain': domain, From df7e1a3c916be009ade508cf7ffff39d4e486c7f Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Wed, 7 Jun 2017 10:47:53 -0700 Subject: [PATCH 12/49] Update "nas detail" call to pull mount point field --- SoftLayer/CLI/nas/detail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/nas/detail.py b/SoftLayer/CLI/nas/detail.py index 3c5a62134..9be6abeae 100644 --- a/SoftLayer/CLI/nas/detail.py +++ b/SoftLayer/CLI/nas/detail.py @@ -5,7 +5,7 @@ from SoftLayer.CLI import formatting from SoftLayer import utils -NAS_PROPERTIES = 'id,username,serviceResourceBackendIpAddress' +NAS_PROPERTIES = 'id,username,serviceResourceBackendIpAddress,fileNetworkMountAddress' @click.command() From 07a703d2644c585d93b9327503d98518a7c3552e Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 31 Aug 2018 16:23:11 -0700 Subject: [PATCH 13/49] Copy VSI private VLAN and subnet options over to baremetal --- SoftLayer/CLI/hardware/create.py | 9 +++++++++ SoftLayer/managers/hardware.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/SoftLayer/CLI/hardware/create.py b/SoftLayer/CLI/hardware/create.py index 9b8dc6d8c..33cf6dfb7 100644 --- a/SoftLayer/CLI/hardware/create.py +++ b/SoftLayer/CLI/hardware/create.py @@ -47,6 +47,13 @@ @click.option('--no-public', is_flag=True, help="Private network only") +@click.option('--vlan-private', + help="The ID of the private VLAN on which you want the virtual " + "server placed", + type=click.INT) +@click.option('--subnet-private', + help="The ID of the private SUBNET on which you want the virtual server placed", + type=click.INT) @helpers.multi_option('--extra', '-e', help="Extra options") @click.option('--test', is_flag=True, @@ -91,6 +98,8 @@ def cli(env, **args): 'extras': args.get('extra'), 'quantity': args.get('quantity'), } + order['private_subnet'] = args.get('subnet_private', None) + order['private_vlan'] = args.get('vlan_private', None) # Do not create hardware server with --test or --export do_create = not (args['export'] or args['test']) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 59081892a..ae4d3d296 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -473,6 +473,10 @@ def _generate_create_dict(self, location=None, os=None, port_speed=None, + public_vlan=None, + private_vlan=None, + private_subnet=None, + public_subnet=None, ssh_keys=None, post_uri=None, hourly=True, @@ -525,6 +529,11 @@ def _generate_create_dict(self, 'quantity': quantity } + if private_vlan or public_vlan or private_subnet or public_subnet: + network_components = self._create_network_components(public_vlan, private_vlan, + private_subnet, public_subnet) + order.update(network_components) + if post_uri: order['provisionScripts'] = [post_uri] @@ -535,6 +544,29 @@ def _generate_create_dict(self, return order + def _create_network_components( + self, public_vlan=None, private_vlan=None, + private_subnet=None, public_subnet=None): + + parameters = {} + if private_vlan: + parameters['primaryBackendNetworkComponent'] = {"networkVlan": {"id": int(private_vlan)}} + if public_vlan: + parameters['primaryNetworkComponent'] = {"networkVlan": {"id": int(public_vlan)}} + if public_subnet: + if public_vlan is None: + raise exceptions.SoftLayerError("You need to specify a public_vlan with public_subnet") + else: + parameters['primaryNetworkComponent']['networkVlan']['primarySubnet'] = {'id': int(public_subnet)} + if private_subnet: + if private_vlan is None: + raise exceptions.SoftLayerError("You need to specify a private_vlan with private_subnet") + else: + parameters['primaryBackendNetworkComponent']['networkVlan']['primarySubnet'] = { + "id": int(private_subnet)} + + return parameters + def _get_ids_from_hostname(self, hostname): """Returns list of matching hardware IDs for a given hostname.""" results = self.list_hardware(hostname=hostname, mask="id") From b1cea6ae27c59115c43b6a79c4968d399770e643 Mon Sep 17 00:00:00 2001 From: yhuang <7468443+yhuanghamu@users.noreply.github.com> Date: Wed, 12 Sep 2018 15:34:51 -0700 Subject: [PATCH 14/49] If no vs or hw exists in current subnet, should return empty list instead of "none". --- SoftLayer/CLI/subnet/detail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/subnet/detail.py b/SoftLayer/CLI/subnet/detail.py index 1c8f7e2dc..bf46cadc4 100644 --- a/SoftLayer/CLI/subnet/detail.py +++ b/SoftLayer/CLI/subnet/detail.py @@ -55,7 +55,7 @@ def cli(env, identifier, no_vs, no_hardware): vsi.get('primaryBackendIpAddress')]) table.add_row(['vs', vs_table]) else: - table.add_row(['vs', 'none']) + table.add_row(['vs', []]) if not no_hardware: if subnet['hardware']: @@ -67,6 +67,6 @@ def cli(env, identifier, no_vs, no_hardware): hardware.get('primaryBackendIpAddress')]) table.add_row(['hardware', hw_table]) else: - table.add_row(['hardware', 'none']) + table.add_row(['hardware',[]]) env.fout(table) From ff148dbd7ef7504e216e34827af6ddda688c987c Mon Sep 17 00:00:00 2001 From: Hitesh Date: Thu, 27 Sep 2018 11:20:04 -0700 Subject: [PATCH 15/49] Update sshkey --add return value and format The "sshkey --add" does not currently return the key ID, this commit updates the CLI to return the ID. --- SoftLayer/CLI/sshkey/add.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/sshkey/add.py b/SoftLayer/CLI/sshkey/add.py index a3a3c6ccb..bffc9820d 100644 --- a/SoftLayer/CLI/sshkey/add.py +++ b/SoftLayer/CLI/sshkey/add.py @@ -5,7 +5,7 @@ import click import SoftLayer -from SoftLayer.CLI import environment +from SoftLayer.CLI import environment, formatting from SoftLayer.CLI import exceptions @@ -40,4 +40,12 @@ def cli(env, label, in_file, key, note): mgr = SoftLayer.SshKeyManager(env.client) result = mgr.add_key(key_text, label, note) - env.fout("SSH key added: %s" % result.get('fingerprint')) + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + + table.add_row(['id', result['id']]) + table.add_row(['label', result['label']]) + table.add_row(['fingerprint', result['fingerprint']]) + + env.fout(table) From bd0ccdf6f2ca68d406a05574c715adc5c860d69d Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 28 Sep 2018 08:50:09 -0700 Subject: [PATCH 16/49] Dedicated host cancellation is pretty similar to bare metal Copied in the bare metal cancellation code and moves some fields around in the request to get it to work with the API --- SoftLayer/CLI/dedicatedhost/cancel.py | 33 +++++++++++++++ SoftLayer/CLI/routes.py | 1 + SoftLayer/managers/dedicated_host.py | 58 +++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 SoftLayer/CLI/dedicatedhost/cancel.py diff --git a/SoftLayer/CLI/dedicatedhost/cancel.py b/SoftLayer/CLI/dedicatedhost/cancel.py new file mode 100644 index 000000000..9427b6f6c --- /dev/null +++ b/SoftLayer/CLI/dedicatedhost/cancel.py @@ -0,0 +1,33 @@ +"""Cancel a dedicated server.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import formatting +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@click.option('--immediate', + is_flag=True, + default=False, + help="Cancels the server immediately (instead of on the billing anniversary)") +@click.option('--comment', + help="An optional comment to add to the cancellation ticket") +@click.option('--reason', + help="An optional cancellation reason. See cancel-reasons for a list of available options") +@environment.pass_env +def cli(env, identifier, immediate, comment, reason): + """Cancel a dedicated server.""" + + mgr = SoftLayer.DedicatedHostManager(env.client) + host_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'dedicatedhost') + + if not (env.skip_confirmations or formatting.no_going_back(host_id)): + raise exceptions.CLIAbort('Aborted') + + mgr.cancel_host(host_id, reason, comment, immediate) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 0e4ca6ee7..057f34783 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -36,6 +36,7 @@ ('dedicatedhost:create', 'SoftLayer.CLI.dedicatedhost.create:cli'), ('dedicatedhost:create-options', 'SoftLayer.CLI.dedicatedhost.create_options:cli'), ('dedicatedhost:detail', 'SoftLayer.CLI.dedicatedhost.detail:cli'), + ('dedicatedhost:cancel', 'SoftLayer.CLI.dedicatedhost.cancel:cli'), ('cdn', 'SoftLayer.CLI.cdn'), ('cdn:detail', 'SoftLayer.CLI.cdn.detail:cli'), diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index 6f6fe596c..5ee61967d 100644 --- a/SoftLayer/managers/dedicated_host.py +++ b/SoftLayer/managers/dedicated_host.py @@ -37,6 +37,43 @@ def __init__(self, client, ordering_manager=None): if ordering_manager is None: self.ordering_manager = ordering.OrderingManager(client) + def cancel_host(self, host_id, reason='unneeded', comment='', immediate=True): + """Cancels the specified dedicated server. + + Example:: + + # Cancels dedicated host id 1234 + result = mgr.cancel_host(host_id=1234) + + :param int host_id: The ID of the dedicated host to be cancelled. + :param string reason: The reason code for the cancellation. This should come from + :func:`get_cancellation_reasons`. + :param string comment: An optional comment to include with the cancellation. + :param bool immediate: If set to True, will automatically update the cancelation ticket to request + the resource be reclaimed asap. This request still has to be reviewed by a human + :returns: True on success or an exception + """ + + # Get cancel reason + reasons = self.get_cancellation_reasons() + cancel_reason = reasons.get(reason, reasons['unneeded']) + ticket_mgr = SoftLayer.TicketManager(self.client) + mask = 'mask[id, billingItem[id]]' + host_billing = self.get_host(host_id, mask=mask) + + + if 'billingItem' not in host_billing: + raise SoftLayer.SoftLayerError("Ticket #%s already exists for this server" % + host_billing['openCancellationTicket']['id']) + + billing_id = host_billing['billingItem']['id'] + + result = self.client.call('Billing_Item', 'cancelItem', + immediate, False, cancel_reason, comment, id=billing_id) + host_billing = self.get_host(host_id, mask=mask) + + return result + def list_instances(self, tags=None, cpus=None, memory=None, hostname=None, disk=None, datacenter=None, **kwargs): """Retrieve a list of all dedicated hosts on the account @@ -93,6 +130,27 @@ def list_instances(self, tags=None, cpus=None, memory=None, hostname=None, kwargs['filter'] = _filter.to_dict() return self.account.getDedicatedHosts(**kwargs) + + def get_cancellation_reasons(self): + """Returns a dictionary of valid cancellation reasons. + + These can be used when cancelling a dedicated server + via :func:`cancel_host`. + """ + return { + 'unneeded': 'No longer needed', + 'closing': 'Business closing down', + 'cost': 'Server / Upgrade Costs', + 'migrate_larger': 'Migrating to larger server', + 'migrate_smaller': 'Migrating to smaller server', + 'datacenter': 'Migrating to a different SoftLayer datacenter', + 'performance': 'Network performance / latency', + 'support': 'Support response / timing', + 'sales': 'Sales process / upgrades', + 'moving': 'Moving to competitor', + } + + def get_host(self, host_id, **kwargs): """Get details about a dedicated host. From e9c357d447d23b5b7b2b85cd50b55249b7c47a84 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 28 Sep 2018 13:11:58 -0700 Subject: [PATCH 17/49] enable bulk orders of dedicatedhosts if multiple hostnames specified --- SoftLayer/CLI/dedicatedhost/create.py | 4 ++-- SoftLayer/managers/dedicated_host.py | 34 +++++++++++++++------------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/create.py b/SoftLayer/CLI/dedicatedhost/create.py index 491da2110..b50bf9cc6 100644 --- a/SoftLayer/CLI/dedicatedhost/create.py +++ b/SoftLayer/CLI/dedicatedhost/create.py @@ -12,7 +12,7 @@ @click.command( epilog="See 'slcli dedicatedhost create-options' for valid options.") -@click.option('--hostname', '-H', +@click.option('--hostnames', '-H', help="Host portion of the FQDN", required=True, prompt=True) @@ -51,7 +51,7 @@ def cli(env, **kwargs): mgr = SoftLayer.DedicatedHostManager(env.client) order = { - 'hostname': kwargs['hostname'], + 'hostnames': kwargs['hostnames'].split(','), 'domain': kwargs['domain'], 'flavor': kwargs['flavor'], 'location': kwargs['datacenter'], diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index 5ee61967d..0b1d63149 100644 --- a/SoftLayer/managers/dedicated_host.py +++ b/SoftLayer/managers/dedicated_host.py @@ -213,7 +213,7 @@ def get_host(self, host_id, **kwargs): return self.host.getObject(id=host_id, **kwargs) - def place_order(self, hostname, domain, location, flavor, hourly, router=None): + def place_order(self, hostnames, domain, location, flavor, hourly, router=None): """Places an order for a dedicated host. See get_create_options() for valid arguments. @@ -225,7 +225,7 @@ def place_order(self, hostname, domain, location, flavor, hourly, router=None): False for monthly. :param int router: an optional value for selecting a backend router """ - create_options = self._generate_create_dict(hostname=hostname, + create_options = self._generate_create_dict(hostnames=hostnames, router=router, domain=domain, flavor=flavor, @@ -234,14 +234,15 @@ def place_order(self, hostname, domain, location, flavor, hourly, router=None): return self.client['Product_Order'].placeOrder(create_options) - def verify_order(self, hostname, domain, location, hourly, flavor, router=None): + def verify_order(self, hostnames, domain, location, hourly, flavor, router=None): """Verifies an order for a dedicated host. See :func:`place_order` for a list of available options. """ - create_options = self._generate_create_dict(hostname=hostname, - router=router, + for hostname in hostnames: + create_options = self._generate_create_dict(hostnames=[hostname], + router=router, domain=domain, flavor=flavor, datacenter=location, @@ -250,7 +251,7 @@ def verify_order(self, hostname, domain, location, hourly, flavor, router=None): return self.client['Product_Order'].verifyOrder(create_options) def _generate_create_dict(self, - hostname=None, + hostnames=None, domain=None, flavor=None, router=None, @@ -268,25 +269,28 @@ def _generate_create_dict(self, router = self._get_default_router(routers, router) - hardware = { - 'hostname': hostname, - 'domain': domain, - 'primaryBackendNetworkComponent': { - 'router': { - 'id': router + hardwares = [] + for hostname in hostnames: + hardware = { + 'hostname': hostname, + 'domain': domain, + 'primaryBackendNetworkComponent': { + 'router': { + 'id': router + } } } - } + hardwares.append(hardware) complex_type = "SoftLayer_Container_Product_Order_Virtual_DedicatedHost" order = { "complexType": complex_type, - "quantity": 1, + "quantity": len(hardwares), 'location': location['keyname'], 'packageId': package['id'], 'prices': [{'id': price}], - 'hardware': [hardware], + 'hardware': hardwares, 'useHourlyPricing': hourly, } return order From 7baa57853ce4bece95f1d79701a953fed4a71b0f Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 28 Sep 2018 15:19:46 -0700 Subject: [PATCH 18/49] Pull hostIds upon creation of a dedicatedhost After creating a dedicatedhost, poll for all hostIds with a matching orderId by listing all dedicatedhosts and filtering by new order_id --- SoftLayer/CLI/dedicatedhost/create.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/create.py b/SoftLayer/CLI/dedicatedhost/create.py index 491da2110..726b79328 100644 --- a/SoftLayer/CLI/dedicatedhost/create.py +++ b/SoftLayer/CLI/dedicatedhost/create.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import time import SoftLayer from SoftLayer.CLI import environment @@ -84,11 +85,6 @@ def cli(env, **kwargs): table.add_row(['Total monthly cost', "%.2f" % total]) output = [] - output.append(table) - output.append(formatting.FormattedItem( - '', - ' -- ! Prices reflected here are retail and do not ' - 'take account level discounts and are not guaranteed.')) if kwargs['export']: export_file = kwargs.pop('export') @@ -104,11 +100,31 @@ def cli(env, **kwargs): result = mgr.place_order(**order) + host_ids = _wait_for_host_ids(result['orderId'], mgr) + table = formatting.KeyValueTable(['name', 'value']) table.align['name'] = 'r' table.align['value'] = 'l' table.add_row(['id', result['orderId']]) table.add_row(['created', result['orderDate']]) + table.add_row(['hostIds', host_ids]) output.append(table) env.fout(output) + + +def _wait_for_host_ids(order_id, mgr): + host_ids = [] + while not host_ids: + host_ids = _extract_host_ids(order_id, mgr) + time.sleep(60) + return host_ids + + +def _extract_host_ids(order_id, mgr): + instances = mgr.list_instances(mask='mask[id,billingItem[orderItem[order]]]') + return [instance['id'] for instance in instances + if int(order_id) == instance.get('billingItem', {})\ + .get('orderItem', {})\ + .get('order', {})\ + .get('id', None)] From 7ff220de0174ecb3e2f55e4a7ade1f72d0308589 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Mon, 1 Oct 2018 13:28:20 -0700 Subject: [PATCH 19/49] Update dedicatedhost create response with new fields: hosts, datacenter ``` $ slcli -y --format=json dedicatedhost create -H dedtest3 -D rescale.com -d wdc07 -f 56_CORES_X_242_RAM_X_1_4_TB --billing hourly { "hosts": [ { "datacenter": "wdc07", "hostName": "dedtest3", "hostId": 247017 } ], "id": 29794663, "created": "2018-10-01T14:23:46-06:00" } ``` --- SoftLayer/CLI/dedicatedhost/create.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/create.py b/SoftLayer/CLI/dedicatedhost/create.py index 77a5077bf..f3de7c7c0 100644 --- a/SoftLayer/CLI/dedicatedhost/create.py +++ b/SoftLayer/CLI/dedicatedhost/create.py @@ -100,14 +100,14 @@ def cli(env, **kwargs): result = mgr.place_order(**order) - host_ids = _wait_for_host_ids(result['orderId'], mgr) + hosts = _wait_for_host_ids(result['orderId'], mgr) table = formatting.KeyValueTable(['name', 'value']) table.align['name'] = 'r' table.align['value'] = 'l' table.add_row(['id', result['orderId']]) table.add_row(['created', result['orderDate']]) - table.add_row(['hostIds', host_ids]) + table.add_row(['hosts', hosts]) output.append(table) env.fout(output) @@ -122,9 +122,13 @@ def _wait_for_host_ids(order_id, mgr): def _extract_host_ids(order_id, mgr): - instances = mgr.list_instances(mask='mask[id,billingItem[orderItem[order]]]') - return [instance['id'] for instance in instances - if int(order_id) == instance.get('billingItem', {})\ - .get('orderItem', {})\ - .get('order', {})\ - .get('id', None)] + instances = mgr.list_instances(mask='mask[id,name,datacenter[name],' + 'billingItem[orderItem[order]]]') + return [{'hostName': instance.get('billingItem', {})['hostName'], + 'hostId': instance['id'], + 'datacenter': instance.get('datacenter', {})['name']} + for instance in instances + if order_id == instance.get('billingItem', {})\ + .get('orderItem', {})\ + .get('order', {})\ + .get('id', None)] From 483e6f20238ad59e7cdce0a8e357387bf7b1b004 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Mon, 1 Oct 2018 15:22:54 -0700 Subject: [PATCH 20/49] some boilerplate cancel confirmation was causing an abort for dedicatedhost cancel just remove the confirmation and other checks, we just want to kill the thing --- SoftLayer/CLI/dedicatedhost/cancel.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/cancel.py b/SoftLayer/CLI/dedicatedhost/cancel.py index 9427b6f6c..ac5788016 100644 --- a/SoftLayer/CLI/dedicatedhost/cancel.py +++ b/SoftLayer/CLI/dedicatedhost/cancel.py @@ -26,8 +26,4 @@ def cli(env, identifier, immediate, comment, reason): mgr = SoftLayer.DedicatedHostManager(env.client) host_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'dedicatedhost') - - if not (env.skip_confirmations or formatting.no_going_back(host_id)): - raise exceptions.CLIAbort('Aborted') - mgr.cancel_host(host_id, reason, comment, immediate) From 3d7388fece71913bef803bfbb1f81671e336377f Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Wed, 3 Oct 2018 09:14:25 -0700 Subject: [PATCH 21/49] Remove more junk from dedicatedhost cancel command not sure what resolve_ids function was doing besides throwing exceptions but cancel seems to work fine without it --- SoftLayer/CLI/dedicatedhost/cancel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/cancel.py b/SoftLayer/CLI/dedicatedhost/cancel.py index ac5788016..ff423c079 100644 --- a/SoftLayer/CLI/dedicatedhost/cancel.py +++ b/SoftLayer/CLI/dedicatedhost/cancel.py @@ -25,5 +25,4 @@ def cli(env, identifier, immediate, comment, reason): """Cancel a dedicated server.""" mgr = SoftLayer.DedicatedHostManager(env.client) - host_id = helpers.resolve_id(mgr.resolve_ids, identifier, 'dedicatedhost') - mgr.cancel_host(host_id, reason, comment, immediate) + mgr.cancel_host(identifier, reason, comment, immediate) From bdf117d22c049b27d3a23977734fc18b55128d3c Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 5 Oct 2018 09:37:40 -0700 Subject: [PATCH 22/49] fixup! Remove more junk from dedicatedhost cancel command --- SoftLayer/managers/dedicated_host.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index 0b1d63149..1da1731a2 100644 --- a/SoftLayer/managers/dedicated_host.py +++ b/SoftLayer/managers/dedicated_host.py @@ -70,7 +70,6 @@ def cancel_host(self, host_id, reason='unneeded', comment='', immediate=True): result = self.client.call('Billing_Item', 'cancelItem', immediate, False, cancel_reason, comment, id=billing_id) - host_billing = self.get_host(host_id, mask=mask) return result From 0d2b81d0725843b230db3bb612cbdda84def3608 Mon Sep 17 00:00:00 2001 From: Yingchao Huang Date: Wed, 10 Oct 2018 17:46:20 -0700 Subject: [PATCH 23/49] click7.0 has errors --- tools/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements.txt b/tools/requirements.txt index d28b39b94..bed36edb5 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,5 +1,5 @@ requests >= 2.18.4 -click >= 5 +click >= 5, < 7 prettytable >= 0.7.0 six >= 1.7.0 prompt_toolkit From c77286ec4180ebd629ec1fd4ab5b8894d4fa752a Mon Sep 17 00:00:00 2001 From: Yingchao Huang Date: Tue, 9 Oct 2018 17:48:47 -0700 Subject: [PATCH 24/49] dedicatedhost Edit and show tags 1. Add new cli command to edit tags of dedicatedhost 2. Show tags in dedicatedhost detail e.g. ``` slcli dedicatedhost edit --tag nicetag 258803 slcli --format=json dedicatedhost detail 258803 slcli --format=json dedicatedhost detail 258803 { "modify date": "", "name": "dev-ryans-cluster0650-cluster-service-compute0-0", "cpu count": 56, "router hostname": "bcr01a.dal10", "memory capacity": 242, "tags": [ "nicetag" ], "disk capacity": 1200, "guest count": 0, "create date": "2018-10-10T12:46:35-06:00", "router id": 843613, "owner": "1703415_mark+ibmdev@rescale.com_2018-08-02-13.24.01", "datacenter": "dal10", "id": 258803 } ``` --- SoftLayer/CLI/dedicatedhost/cancel.py | 4 +- SoftLayer/CLI/dedicatedhost/detail.py | 1 + SoftLayer/CLI/dedicatedhost/edit.py | 61 +++++++++++++++++++++++++++ SoftLayer/CLI/routes.py | 1 + SoftLayer/managers/dedicated_host.py | 61 ++++++++++++++++++++++++++- tools/requirements.txt | 2 +- 6 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 SoftLayer/CLI/dedicatedhost/edit.py diff --git a/SoftLayer/CLI/dedicatedhost/cancel.py b/SoftLayer/CLI/dedicatedhost/cancel.py index ff423c079..e2a6f8e7e 100644 --- a/SoftLayer/CLI/dedicatedhost/cancel.py +++ b/SoftLayer/CLI/dedicatedhost/cancel.py @@ -14,14 +14,14 @@ @click.argument('identifier') @click.option('--immediate', is_flag=True, - default=False, + default=True, help="Cancels the server immediately (instead of on the billing anniversary)") @click.option('--comment', help="An optional comment to add to the cancellation ticket") @click.option('--reason', help="An optional cancellation reason. See cancel-reasons for a list of available options") @environment.pass_env -def cli(env, identifier, immediate, comment, reason): +def cli(env, identifier, comment, reason, immediate=True): """Cancel a dedicated server.""" mgr = SoftLayer.DedicatedHostManager(env.client) diff --git a/SoftLayer/CLI/dedicatedhost/detail.py b/SoftLayer/CLI/dedicatedhost/detail.py index e1c46b962..913bb3d94 100644 --- a/SoftLayer/CLI/dedicatedhost/detail.py +++ b/SoftLayer/CLI/dedicatedhost/detail.py @@ -40,6 +40,7 @@ def cli(env, identifier, price=False, guests=False): table.add_row(['router hostname', result['backendRouter']['hostname']]) table.add_row(['owner', formatting.FormattedItem( utils.lookup(result, 'billingItem', 'orderItem', 'order', 'userRecord', 'username') or formatting.blank(),)]) + table.add_row(['tags', formatting.tags(result['tagReferences'])]) if price: total_price = utils.lookup(result, diff --git a/SoftLayer/CLI/dedicatedhost/edit.py b/SoftLayer/CLI/dedicatedhost/edit.py new file mode 100644 index 000000000..3f87457ec --- /dev/null +++ b/SoftLayer/CLI/dedicatedhost/edit.py @@ -0,0 +1,61 @@ +"""Edit dedicated host details.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@click.option('--domain', '-D', help="Domain portion of the FQDN") +@click.option('--userfile', '-F', + help="Read userdata from file", + type=click.Path(exists=True, readable=True, resolve_path=True)) +@click.option('--tag', '-g', + multiple=True, + help="Tags to set or empty string to remove all") +@click.option('--hostname', '-H', help="Host portion of the FQDN") +@click.option('--userdata', '-u', help="User defined metadata string") +@click.option('--public-speed', + help="Public port speed.", + default=None, + type=click.Choice(['0', '10', '100', '1000', '10000'])) +@click.option('--private-speed', + help="Private port speed.", + default=None, + type=click.Choice(['0', '10', '100', '1000', '10000'])) +@environment.pass_env +def cli(env, identifier, domain, userfile, tag, hostname, userdata, + public_speed, private_speed): + """Edit dedicated host details.""" + + if userdata and userfile: + raise exceptions.ArgumentError( + '[-u | --userdata] not allowed with [-F | --userfile]') + + data = { + 'hostname': hostname, + 'domain': domain, + } + if userdata: + data['userdata'] = userdata + elif userfile: + with open(userfile, 'r') as userfile_obj: + data['userdata'] = userfile_obj.read() + if tag: + data['tags'] = ','.join(tag) + + mgr = SoftLayer.DedicatedHostManager(env.client) + + if not mgr.edit(identifier, **data): + raise exceptions.CLIAbort("Failed to update dedicated host") + + if public_speed is not None: + mgr.change_port_speed(identifier, True, int(public_speed)) + + if private_speed is not None: + mgr.change_port_speed(identifier, False, int(private_speed)) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 057f34783..8279b6246 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -37,6 +37,7 @@ ('dedicatedhost:create-options', 'SoftLayer.CLI.dedicatedhost.create_options:cli'), ('dedicatedhost:detail', 'SoftLayer.CLI.dedicatedhost.detail:cli'), ('dedicatedhost:cancel', 'SoftLayer.CLI.dedicatedhost.cancel:cli'), + ('dedicatedhost:edit', 'SoftLayer.CLI.dedicatedhost.edit:cli'), ('cdn', 'SoftLayer.CLI.cdn'), ('cdn:detail', 'SoftLayer.CLI.cdn.detail:cli'), diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index 1da1731a2..b5e36613b 100644 --- a/SoftLayer/managers/dedicated_host.py +++ b/SoftLayer/managers/dedicated_host.py @@ -73,6 +73,50 @@ def cancel_host(self, host_id, reason='unneeded', comment='', immediate=True): return result + def edit(self, host_id, userdata=None, hostname=None, domain=None, + notes=None, tags=None): + """Edit hostname, domain name, notes, user data of the dedicated host. + + Parameters set to None will be ignored and not attempted to be updated. + + :param integer host_id: the instance ID to edit + :param string userdata: user data on the dedicated host to edit. + If none exist it will be created + :param string hostname: valid hostname + :param string domain: valid domain name + :param string notes: notes about this particular dedicated host + :param string tags: tags to set on the dedicated host as a comma + separated list. Use the empty string to remove all + tags. + + Example:: + + # Change the hostname on instance 12345 to 'something' + result = mgr.edit(host_id=12345 , hostname="something") + #result will be True or an Exception + """ + + obj = {} + if userdata: + self.host.setUserMetadata([userdata], id=host_id) + + if tags is not None: + self.host.setTags(tags, id=host_id) + + if hostname: + obj['hostname'] = hostname + + if domain: + obj['domain'] = domain + + if notes: + obj['notes'] = notes + + if not obj: + return True + + return self.host.editObject(obj, id=host_id) + def list_instances(self, tags=None, cpus=None, memory=None, hostname=None, disk=None, datacenter=None, **kwargs): """Retrieve a list of all dedicated hosts on the account @@ -207,7 +251,14 @@ def get_host(self, host_id, **kwargs): domain, uuid ], - guestCount + guestCount, + tagReferences[ + id, + tag[ + name, + id + ] + ] ''') return self.host.getObject(id=host_id, **kwargs) @@ -416,3 +467,11 @@ def get_router_options(self, datacenter=None, flavor=None): item = self._get_item(package, flavor) return self._get_backend_router(location['location']['locationPackageDetails'], item) + + # @retry(logger=LOGGER) + def set_tags(self, tags, host_id): + """Sets tags on a dedicated_host with a retry decorator + + Just calls guest.setTags, but if it fails from an APIError will retry + """ + self.host.setTags(tags, id=host_id) diff --git a/tools/requirements.txt b/tools/requirements.txt index d28b39b94..bed36edb5 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,5 +1,5 @@ requests >= 2.18.4 -click >= 5 +click >= 5, < 7 prettytable >= 0.7.0 six >= 1.7.0 prompt_toolkit From b773b59b947e390cf1977a7781f5dfe33987101e Mon Sep 17 00:00:00 2001 From: Yingchao Huang Date: Wed, 17 Oct 2018 13:37:53 -0700 Subject: [PATCH 25/49] Dedicatedhost#Cancel: cli bug with --immediate flag We found weird behavior with cancellation if we set `--immediate default=True` in decorator. `slcli dedicatedhost cancel ID` => will create immediate cancellation ticket `slcli dedicatedhost cancel ID --immediate` => will create non-immediate cancellation ticket Have no idea what is the root cause, fix is to enforce `immediate = True` in function body. --- SoftLayer/CLI/dedicatedhost/cancel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/cancel.py b/SoftLayer/CLI/dedicatedhost/cancel.py index e2a6f8e7e..a01016ec4 100644 --- a/SoftLayer/CLI/dedicatedhost/cancel.py +++ b/SoftLayer/CLI/dedicatedhost/cancel.py @@ -21,8 +21,8 @@ @click.option('--reason', help="An optional cancellation reason. See cancel-reasons for a list of available options") @environment.pass_env -def cli(env, identifier, comment, reason, immediate=True): +def cli(env, identifier, immediate, comment, reason): """Cancel a dedicated server.""" - + immediate = True # Enforce immediate cancellation mgr = SoftLayer.DedicatedHostManager(env.client) mgr.cancel_host(identifier, reason, comment, immediate) From ca68e1e1f155e841e834095018aea8756c114a61 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Wed, 24 Aug 2016 14:20:12 -0700 Subject: [PATCH 26/49] Extend CLI commands, add new ones New commands: - nas grant-access - nas revoke-access - nas detail - hardware status (status based on orderId rather than hardwareId) Updated commands: - hardware detail: parse out specific fields json format - hardware create: create multiple servers janky --output-json flag to dump specific parts in JSON TODO: revert some/all commands to use existing --format=json flag --- SoftLayer/CLI/hardware/create.py | 29 +++++++++++++++++++---------- SoftLayer/CLI/hardware/detail.py | 25 ++++++++++++++++++++++++- SoftLayer/CLI/hardware/list.py | 7 ++++++- SoftLayer/CLI/hardware/status.py | 16 ++++++++++++++++ SoftLayer/CLI/nas/detail.py | 19 +++++++++++++++++++ SoftLayer/CLI/nas/grant_access.py | 23 +++++++++++++++++++++++ SoftLayer/CLI/nas/revoke_access.py | 23 +++++++++++++++++++++++ SoftLayer/CLI/routes.py | 3 +++ SoftLayer/managers/hardware.py | 30 ++++++++++++++++++++++++------ SoftLayer/managers/ordering.py | 6 ++++++ 10 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 SoftLayer/CLI/hardware/status.py create mode 100644 SoftLayer/CLI/nas/detail.py create mode 100644 SoftLayer/CLI/nas/grant_access.py create mode 100644 SoftLayer/CLI/nas/revoke_access.py diff --git a/SoftLayer/CLI/hardware/create.py b/SoftLayer/CLI/hardware/create.py index 472cec2e2..9b8dc6d8c 100644 --- a/SoftLayer/CLI/hardware/create.py +++ b/SoftLayer/CLI/hardware/create.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -12,8 +13,8 @@ @click.command(epilog="See 'slcli server create-options' for valid options.") -@click.option('--hostname', '-H', - help="Host portion of the FQDN", +@click.option('--hostnames', '-H', + help="Host portions of the FQDN", required=True, prompt=True) @click.option('--domain', '-D', @@ -62,6 +63,8 @@ type=click.INT, help="Wait until the server is finished provisioning for up to " "X seconds before returning") +@click.option('--quantity', default=1, type=click.INT) +@click.option('--output-json', is_flag=True) @environment.pass_env def cli(env, **args): """Order/create a dedicated server.""" @@ -75,7 +78,7 @@ def cli(env, **args): ssh_keys.append(key_id) order = { - 'hostname': args['hostname'], + 'hostnames': args['hostnames'].split(','), 'domain': args['domain'], 'size': args['size'], 'location': args.get('datacenter'), @@ -86,6 +89,7 @@ def cli(env, **args): 'port_speed': args.get('port_speed'), 'no_public': args.get('no_public') or False, 'extras': args.get('extra'), + 'quantity': args.get('quantity'), } # Do not create hardware server with --test or --export @@ -129,11 +133,16 @@ def cli(env, **args): result = mgr.place_order(**order) - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - table.add_row(['id', result['orderId']]) - table.add_row(['created', result['orderDate']]) - output = table + if args['output_json']: + env.fout(json.dumps({'orderId': result['orderId'], + 'created': result['orderDate']})) - env.fout(output) + else: + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['id', result['orderId']]) + table.add_row(['created', result['orderDate']]) + output = table + + env.fout(output) diff --git a/SoftLayer/CLI/hardware/detail.py b/SoftLayer/CLI/hardware/detail.py index e254ad33e..ea36fb6dd 100644 --- a/SoftLayer/CLI/hardware/detail.py +++ b/SoftLayer/CLI/hardware/detail.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -14,8 +15,10 @@ @click.argument('identifier') @click.option('--passwords', is_flag=True, help='Show passwords (check over your shoulder!)') @click.option('--price', is_flag=True, help='Show associated prices') +@click.option('--output-json', is_flag=True, default=False) +@click.option('--verbose', is_flag=True, default=False) @environment.pass_env -def cli(env, identifier, passwords, price): +def cli(env, identifier, passwords, price, output_json, verbose): """Get details for a hardware device.""" hardware = SoftLayer.HardwareManager(env.client) @@ -28,6 +31,26 @@ def cli(env, identifier, passwords, price): result = hardware.get_hardware(hardware_id) result = utils.NestedDict(result) + if output_json: + if verbose: + env.fout(json.dumps(result)) + else: + partial = {k: result[k] for k in + ['id', + 'primaryIpAddress', + 'primaryBackendIpAddress', + 'hostname', + 'fullyQualifiedDomainName', + 'operatingSystem' + ]} + partial['osPlatform'] = partial \ + ['operatingSystem'] \ + ['softwareLicense'] \ + ['softwareDescription'] \ + ['name'] + env.fout(json.dumps(partial)) + return + operating_system = utils.lookup(result, 'operatingSystem', 'softwareLicense', 'softwareDescription') or {} memory = formatting.gb(result.get('memoryCapacity', 0)) owner = None diff --git a/SoftLayer/CLI/hardware/list.py b/SoftLayer/CLI/hardware/list.py index 54bc4f823..366524264 100644 --- a/SoftLayer/CLI/hardware/list.py +++ b/SoftLayer/CLI/hardware/list.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import columns as column_helper @@ -58,8 +59,9 @@ help='How many results to get in one api call, default is 100', default=100, show_default=True) +@click.option('--output-json', is_flag=True, default=False) @environment.pass_env -def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, columns, limit): +def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, columns, limit, output_json): """List hardware servers.""" manager = SoftLayer.HardwareManager(env.client) @@ -73,6 +75,9 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, tag, co mask="mask(SoftLayer_Hardware_Server)[%s]" % columns.mask(), limit=limit) + if output_json: + env.fout(json.dumps({'hardware': servers})) + return table = formatting.Table(columns.columns) table.sortby = sortby diff --git a/SoftLayer/CLI/hardware/status.py b/SoftLayer/CLI/hardware/status.py new file mode 100644 index 000000000..4238915eb --- /dev/null +++ b/SoftLayer/CLI/hardware/status.py @@ -0,0 +1,16 @@ +import click +import json + +import SoftLayer +from SoftLayer.CLI import environment + + +@click.command() +@click.argument('order_id', required=True) +@environment.pass_env +def cli(env, order_id): + hardware = SoftLayer.HardwareManager(env.client) + + env.fout(json.dumps({ + 'statuses': hardware.get_hardware_status_from_order(order_id) + })) diff --git a/SoftLayer/CLI/nas/detail.py b/SoftLayer/CLI/nas/detail.py new file mode 100644 index 000000000..3c5a62134 --- /dev/null +++ b/SoftLayer/CLI/nas/detail.py @@ -0,0 +1,19 @@ +import click +import json + +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + +NAS_PROPERTIES = 'id,username,serviceResourceBackendIpAddress' + + +@click.command() +@click.argument('nasid', type=int) +@environment.pass_env +def cli(env, nasid): + account = env.client['Account'] + for nas in account.getNasNetworkStorage(mask=NAS_PROPERTIES): + if int(nas['id']) == nasid: + env.fout(json.dumps(nas)) + break diff --git a/SoftLayer/CLI/nas/grant_access.py b/SoftLayer/CLI/nas/grant_access.py new file mode 100644 index 000000000..8e6ad8951 --- /dev/null +++ b/SoftLayer/CLI/nas/grant_access.py @@ -0,0 +1,23 @@ +import click +import json + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import hardware + +STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' + +@click.command() +@click.option('--order-id', required=True) +@click.option('--storage-id', required=True) +@environment.pass_env +def cli(env, order_id, storage_id): + hardware = SoftLayer.HardwareManager(env.client) + + hardware_ids = hardware.get_hardware_ids(order_id) + for hardware_id in hardware_ids: + payload = {'id': hardware_id} + env.client[STORAGE_ENDPOINT].allowAccessFromHardware( + payload, id=storage_id + ) diff --git a/SoftLayer/CLI/nas/revoke_access.py b/SoftLayer/CLI/nas/revoke_access.py new file mode 100644 index 000000000..289fbfa3e --- /dev/null +++ b/SoftLayer/CLI/nas/revoke_access.py @@ -0,0 +1,23 @@ +import click +import json + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer.CLI import hardware + +STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' + +@click.command() +@click.option('--order-id', required=True) +@click.option('--storage-id', required=True) +@environment.pass_env +def cli(env, order_id, storage_id): + hardware = SoftLayer.HardwareManager(env.client) + + hardware_ids = hardware.get_hardware_ids(order_id) + for hardware_id in hardware_ids: + payload = {'id': hardware_id} + env.client[STORAGE_ENDPOINT].removeAccessFromHardware( + payload, id=storage_id + ) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 5524b6948..ba1c53078 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -189,6 +189,8 @@ ('nas', 'SoftLayer.CLI.nas'), ('nas:list', 'SoftLayer.CLI.nas.list:cli'), ('nas:credentials', 'SoftLayer.CLI.nas.credentials:cli'), + ('nas:grant-access', 'SoftLayer.CLI.nas.grant_access:cli'), + ('nas:detail', 'SoftLayer.CLI.nas.detail:cli'), ('object-storage', 'SoftLayer.CLI.object_storage'), @@ -232,6 +234,7 @@ ('hardware:rescue', 'SoftLayer.CLI.hardware.power:rescue'), ('hardware:ready', 'SoftLayer.CLI.hardware.ready:cli'), ('hardware:toggle-ipmi', 'SoftLayer.CLI.hardware.toggle_ipmi:cli'), + ('hardware:status', 'SoftLayer.CLI.hardware.status:cli'), ('securitygroup', 'SoftLayer.CLI.securitygroup'), ('securitygroup:list', 'SoftLayer.CLI.securitygroup.list:cli'), diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 4a52a5236..07d9e7bec 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -434,6 +434,16 @@ def get_create_options(self): 'extras': extras, } + def get_hardware_status_from_order(self, order_id): + BILLING_ORDER_MASK = 'mask[orderTopLevelItems[hostName,billingItem[id,provisionTransaction[hardware[id,hardwareStatus]]]]]' + order = self.ordering_manager.get_order(order_id, BILLING_ORDER_MASK) + return [_get_hardware_status_from_billing(item) + for item in _get_billing_items(order)] + + def get_hardware_ids(self, order_id): + return [hw['hardwareId'] + for hw in self.get_hardware_status_from_order(order_id)] + @retry(logger=LOGGER) def _get_package(self): """Get the package related to simple hardware ordering.""" @@ -458,7 +468,7 @@ def _get_package(self): def _generate_create_dict(self, size=None, - hostname=None, + hostnames=None, domain=None, location=None, os=None, @@ -467,7 +477,8 @@ def _generate_create_dict(self, post_uri=None, hourly=True, no_public=False, - extras=None): + extras=None, + quantity=1): """Translates arguments into a dictionary for creating a server.""" extras = extras or [] @@ -499,19 +510,19 @@ def _generate_create_dict(self, prices.append(_get_extra_price_id(package['items'], extra, hourly, location=location)) - - hardware = { + hardware = [{ 'hostname': hostname, 'domain': domain, - } + } for hostname in hostnames] order = { - 'hardware': [hardware], + 'hardware': hardware, 'location': location['keyname'], 'prices': [{'id': price} for price in prices], 'packageId': package['id'], 'presetId': _get_preset_id(package, size), 'useHourlyPricing': hourly, + 'quantity': quantity } if post_uri: @@ -869,3 +880,10 @@ def _get_preset_id(package, size): return preset['id'] raise SoftLayer.SoftLayerError("Could not find valid size for: '%s'" % size) + +def _get_billing_items(order): + return [top['billingItem'] for top in order['orderTopLevelItems'] + if 'billingItem' in top] + +def _get_hardware_status_from_billing(billing_item): + return billing_item['provisionTransaction'] diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py index e20378914..f38cb34de 100644 --- a/SoftLayer/managers/ordering.py +++ b/SoftLayer/managers/ordering.py @@ -619,3 +619,9 @@ def get_location_id(self, location): if len(datacenter) != 1: raise exceptions.SoftLayerError("Unable to find location: %s" % location) return datacenter[0]['id'] + + def get_order(self, order_id, mask=None): + return self.client['Billing_Order'].getObject( + id=order_id, + mask=mask + ) From 01c11c24fbdfaac5ef724702b6e428a3ae8d4e29 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Thu, 1 Sep 2016 23:25:30 -0700 Subject: [PATCH 27/49] ssh keys for all, not just the first --- SoftLayer/managers/hardware.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 07d9e7bec..966253500 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -529,7 +529,9 @@ def _generate_create_dict(self, order['provisionScripts'] = [post_uri] if ssh_keys: - order['sshKeys'] = [{'sshKeyIds': ssh_keys}] + # need a copy of the key ids for each host, otherwise it + # will only set up keys on the first host + order['sshKeys'] = [{'sshKeyIds': ssh_keys}] * quantity return order From dec35162b1b24ebdd05f493cdf0ed6c6165d2f11 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Tue, 20 Sep 2016 14:20:31 -0700 Subject: [PATCH 28/49] CLI hw create command currently broken for GPUs - missing Disk Controller - add price to specify the disk controller until cli is fixed properly --- SoftLayer/managers/hardware.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 966253500..7c7e710c6 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -510,6 +510,11 @@ def _generate_create_dict(self, prices.append(_get_extra_price_id(package['items'], extra, hourly, location=location)) + + # Hack to add Disk Controller for K80 order to go through + if size == 'D2620V4_128GB_2X800GB_SSD_RAID_1_K80_GPU2': + prices.append(22482) + hardware = [{ 'hostname': hostname, 'domain': domain, From d5c6b5cc847a35c9c2c9bf516b317bc9b41d2f6a Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Thu, 27 Oct 2016 11:04:02 -0700 Subject: [PATCH 29/49] Enable the cli to create multiple vm instances in one request. --- SoftLayer/CLI/virt/create.py | 79 +++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index 70430bc8f..d26bfd46c 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -161,6 +162,39 @@ def _parse_create_args(client, args): @click.option('--boot-mode', type=click.STRING, help="Specify the mode to boot the OS in. Supported modes are HVM and PV.") @click.option('--billing', type=click.Choice(['hourly', 'monthly']), default='hourly', show_default=True, +@click.command(epilog="See 'slcli vs create-options' for valid options") +@click.option('--hostnames', '-H', + help="Hosts portion of the FQDN", + required=True, + prompt=True) +@click.option('--domain', '-D', + help="Domain portion of the FQDN", + required=True, + prompt=True) +@click.option('--cpu', '-c', + help="Number of CPU cores (not available with flavors)", + type=click.INT) +@click.option('--memory', '-m', + help="Memory in mebibytes (not available with flavors)", + type=virt.MEM_TYPE) +@click.option('--flavor', '-f', + help="Public Virtual Server flavor key name", + type=click.STRING) +@click.option('--datacenter', '-d', + help="Datacenter shortname", + required=True, + prompt=True) +@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', + help="Specify the mode to boot the OS in. Supported modes are HVM and PV.", + type=click.STRING) +@click.option('--billing', + type=click.Choice(['hourly', 'monthly']), + default='hourly', + show_default=True, help="Billing rate") @click.option('--dedicated/--public', is_flag=True, help="Create a Dedicated Virtual Server") @click.option('--host-id', type=click.INT, help="Host Id to provision a Dedicated Host Virtual Server onto") @@ -203,6 +237,36 @@ def _parse_create_args(client, args): @click.option('--ipv6', is_flag=True, help="Adds an IPv6 address to this guest") @click.option('--transient', is_flag=True, help="Create a transient virtual server") +@click.option('--userfile', '-F', + help="Read userdata from file", + type=click.Path(exists=True, readable=True, resolve_path=True)) +@click.option('--vlan-public', + help="The ID of the public VLAN on which you want the virtual " + "server placed", + type=click.INT) +@click.option('--vlan-private', + help="The ID of the private VLAN on which you want the virtual " + "server placed", + type=click.INT) +@click.option('--subnet-public', + help="The ID of the public SUBNET on which you want the virtual server placed", + type=click.INT) +@click.option('--subnet-private', + help="The ID of the private SUBNET on which you want the virtual server placed", + type=click.INT) +@helpers.multi_option('--public-security-group', + '-S', + help=('Security group ID to associate with ' + 'the public interface')) +@helpers.multi_option('--private-security-group', + '-s', + help=('Security group ID to associate with ' + 'the private interface')) +@click.option('--wait', + type=click.INT, + help="Wait until VS is finished provisioning for up to X " + "seconds before returning") +@click.option('--output-json', is_flag=True) @environment.pass_env def cli(env, **args): """Order/create virtual servers.""" @@ -239,7 +303,20 @@ def cli(env, **args): if ready is False: env.out(env.fmt(output)) raise exceptions.CLIHalt(code=1) - + if args['output_json']: + env.fout(json.dumps(result)) + else: + for instance_data in result: + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + table.add_row(['id', instance_data['id']]) + table.add_row(['hostname', instance_data['hostname']]) + table.add_row(['created', instance_data['createDate']]) + table.add_row(['uuid', instance_data['uuid']]) + output.append(table) + + env.fout(output) def _build_receipt_table(result, billing="hourly", test=False): """Retrieve the total recurring fee of the items prices""" From 8abc051c579471a2479a13eb4da2ede13dd679e8 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Thu, 27 Oct 2016 11:53:56 -0700 Subject: [PATCH 30/49] Add json support for instance-id ready cmd. --- SoftLayer/CLI/virt/ready.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/ready.py b/SoftLayer/CLI/virt/ready.py index c41680436..807855073 100644 --- a/SoftLayer/CLI/virt/ready.py +++ b/SoftLayer/CLI/virt/ready.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -12,13 +13,19 @@ @click.command() @click.argument('identifier') @click.option('--wait', default=0, show_default=True, type=click.INT, help="Seconds to wait") +@click.option('--output-json', is_flag=True, default=False) @environment.pass_env -def cli(env, identifier, wait): +def cli(env, identifier, wait, output_json=False): """Check if a virtual server is ready.""" vsi = SoftLayer.VSManager(env.client) vs_id = helpers.resolve_id(vsi.resolve_ids, identifier, 'VS') ready = vsi.wait_for_ready(vs_id, wait) + + if output_json: + env.fout(json.dumps({'ready': bool(ready)})) + return + if ready: env.fout("READY") else: From ee39995836c4aa74beaef3dc4871d768b6bd5a70 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Thu, 27 Oct 2016 11:58:09 -0700 Subject: [PATCH 31/49] Add json support for vm detail command. --- SoftLayer/CLI/virt/detail.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 53ae7e04d..28ab46b48 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -4,6 +4,7 @@ import logging import click +import json import SoftLayer from SoftLayer.CLI import environment @@ -20,8 +21,11 @@ is_flag=True, help='Show passwords (check over your shoulder!)') @click.option('--price', is_flag=True, help='Show associated prices') +@click.option('--output-json', is_flag=True, default=False) +@click.option('--verbose', is_flag=True, default=False) @environment.pass_env -def cli(env, identifier, passwords=False, price=False): +def cli(env, identifier, passwords=False, price=False, output_json=False, + verbose=False): """Get details for a virtual server.""" vsi = SoftLayer.VSManager(env.client) @@ -33,6 +37,26 @@ def cli(env, identifier, passwords=False, price=False): result = vsi.get_instance(vs_id) result = utils.NestedDict(result) + if output_json: + if verbose: + env.fout(json.dumps(result)) + else: + partial = {k: result[k] for k in + ['id', + 'primaryIpAddress', + 'primaryBackendIpAddress', + 'hostname', + 'fullyQualifiedDomainName', + 'operatingSystem' + ]} + partial['osPlatform'] = partial\ + ['operatingSystem']\ + ['softwareLicense']\ + ['softwareDescription']\ + ['name'] + env.fout(json.dumps(partial)) + return + table.add_row(['id', result['id']]) table.add_row(['guid', result['globalIdentifier']]) table.add_row(['hostname', result['hostname']]) From 3ac7aaa095ac0463ab7f811446dd1e2de8091705 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Fri, 28 Oct 2016 17:38:12 -0700 Subject: [PATCH 32/49] Add support to grant access to a vm. --- SoftLayer/CLI/nas/grant_vm_access.py | 17 +++++++++++++++++ SoftLayer/CLI/routes.py | 1 + 2 files changed, 18 insertions(+) create mode 100644 SoftLayer/CLI/nas/grant_vm_access.py diff --git a/SoftLayer/CLI/nas/grant_vm_access.py b/SoftLayer/CLI/nas/grant_vm_access.py new file mode 100644 index 000000000..8921067ff --- /dev/null +++ b/SoftLayer/CLI/nas/grant_vm_access.py @@ -0,0 +1,17 @@ +import click +import json + +import SoftLayer +from SoftLayer.CLI import environment + +STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' + +@click.command() +@click.option('--vm-id', required=True) +@click.option('--storage-id', required=True) +@environment.pass_env +def cli(env, vm_id, storage_id): + payload = {'id': vm_id} + env.client[STORAGE_ENDPOINT].allowAccessFromVirtualGuest( + payload, id=storage_id + ) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index ba1c53078..6e94e9d9f 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -190,6 +190,7 @@ ('nas:list', 'SoftLayer.CLI.nas.list:cli'), ('nas:credentials', 'SoftLayer.CLI.nas.credentials:cli'), ('nas:grant-access', 'SoftLayer.CLI.nas.grant_access:cli'), + ('nas:grant-vm-access', 'SoftLayer.CLI.nas.grant_vm_access:cli'), ('nas:detail', 'SoftLayer.CLI.nas.detail:cli'), ('object-storage', 'SoftLayer.CLI.object_storage'), From 5f4157402325d42ba326a428a2760f6c4cd810d7 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Sun, 30 Oct 2016 13:29:23 -0700 Subject: [PATCH 33/49] Get rid of grant_vm_access. Move the vm operations into grant_access and remove_access. --- SoftLayer/CLI/nas/grant_access.py | 24 +++++++++++++++++------- SoftLayer/CLI/nas/grant_vm_access.py | 17 ----------------- SoftLayer/CLI/nas/revoke_access.py | 25 ++++++++++++++++++------- SoftLayer/CLI/routes.py | 2 +- 4 files changed, 36 insertions(+), 32 deletions(-) delete mode 100644 SoftLayer/CLI/nas/grant_vm_access.py diff --git a/SoftLayer/CLI/nas/grant_access.py b/SoftLayer/CLI/nas/grant_access.py index 8e6ad8951..41f57da53 100644 --- a/SoftLayer/CLI/nas/grant_access.py +++ b/SoftLayer/CLI/nas/grant_access.py @@ -9,15 +9,25 @@ STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' @click.command() -@click.option('--order-id', required=True) +@click.option('--order-id', required=False) +@click.option('--vm-id', required=False) @click.option('--storage-id', required=True) @environment.pass_env -def cli(env, order_id, storage_id): - hardware = SoftLayer.HardwareManager(env.client) +def cli(env, order_id, vm_id, storage_id): + if order_id and vm_id: + raise ValueError('Should only specify order_id or vm_id but not both.') - hardware_ids = hardware.get_hardware_ids(order_id) - for hardware_id in hardware_ids: - payload = {'id': hardware_id} - env.client[STORAGE_ENDPOINT].allowAccessFromHardware( + if order_id: + hardware = SoftLayer.HardwareManager(env.client) + hardware_ids = hardware.get_hardware_ids(order_id) + for hardware_id in hardware_ids: + payload = {'id': hardware_id} + env.client[STORAGE_ENDPOINT].allowAccessFromHardware( + payload, id=storage_id + ) + + if vm_id: + payload = {'id': vm_id} + env.client[STORAGE_ENDPOINT].allowAccessFromVirtualGuest( payload, id=storage_id ) diff --git a/SoftLayer/CLI/nas/grant_vm_access.py b/SoftLayer/CLI/nas/grant_vm_access.py deleted file mode 100644 index 8921067ff..000000000 --- a/SoftLayer/CLI/nas/grant_vm_access.py +++ /dev/null @@ -1,17 +0,0 @@ -import click -import json - -import SoftLayer -from SoftLayer.CLI import environment - -STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' - -@click.command() -@click.option('--vm-id', required=True) -@click.option('--storage-id', required=True) -@environment.pass_env -def cli(env, vm_id, storage_id): - payload = {'id': vm_id} - env.client[STORAGE_ENDPOINT].allowAccessFromVirtualGuest( - payload, id=storage_id - ) diff --git a/SoftLayer/CLI/nas/revoke_access.py b/SoftLayer/CLI/nas/revoke_access.py index 289fbfa3e..53c3ec119 100644 --- a/SoftLayer/CLI/nas/revoke_access.py +++ b/SoftLayer/CLI/nas/revoke_access.py @@ -9,15 +9,26 @@ STORAGE_ENDPOINT = 'SoftLayer_Network_Storage' @click.command() -@click.option('--order-id', required=True) +@click.option('--order-id', required=False) +@click.option('--vm-id', required=False) @click.option('--storage-id', required=True) @environment.pass_env -def cli(env, order_id, storage_id): - hardware = SoftLayer.HardwareManager(env.client) +def cli(env, order_id, vm_id, storage_id): + if order_id and vm_id: + raise ValueError('Should only specify order_id or vm_id but not both.') - hardware_ids = hardware.get_hardware_ids(order_id) - for hardware_id in hardware_ids: - payload = {'id': hardware_id} - env.client[STORAGE_ENDPOINT].removeAccessFromHardware( + if order_id: + hardware = SoftLayer.HardwareManager(env.client) + + hardware_ids = hardware.get_hardware_ids(order_id) + for hardware_id in hardware_ids: + payload = {'id': hardware_id} + env.client[STORAGE_ENDPOINT].removeAccessFromHardware( + payload, id=storage_id + ) + + if vm_id: + payload = {'id': vm_id} + env.client[STORAGE_ENDPOINT].removeAccessFromVirtualGuest( payload, id=storage_id ) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 6e94e9d9f..25faf0d3a 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -190,8 +190,8 @@ ('nas:list', 'SoftLayer.CLI.nas.list:cli'), ('nas:credentials', 'SoftLayer.CLI.nas.credentials:cli'), ('nas:grant-access', 'SoftLayer.CLI.nas.grant_access:cli'), - ('nas:grant-vm-access', 'SoftLayer.CLI.nas.grant_vm_access:cli'), ('nas:detail', 'SoftLayer.CLI.nas.detail:cli'), + ('nas:revoke-access', 'SoftLayer.CLI.nas.revoke_access:cli'), ('object-storage', 'SoftLayer.CLI.object_storage'), From 140f37da9fd976b9e4c0a59e84b37d8eb09bdeb4 Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Mon, 31 Oct 2016 16:02:45 -0700 Subject: [PATCH 34/49] Make the vm list command outputs json. --- SoftLayer/CLI/virt/list.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/list.py b/SoftLayer/CLI/virt/list.py index 6bf9e6bb6..88e89fff9 100644 --- a/SoftLayer/CLI/virt/list.py +++ b/SoftLayer/CLI/virt/list.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import json import SoftLayer from SoftLayer.CLI import columns as column_helper @@ -67,9 +68,10 @@ help='How many results to get in one api call, default is 100', default=100, show_default=True) +@click.option('--output-json', is_flag=True, default=False) @environment.pass_env def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, - hourly, monthly, tag, columns, limit, transient): + hourly, monthly, tag, columns, limit, transient, output_json): """List virtual servers.""" vsi = SoftLayer.VSManager(env.client) @@ -86,6 +88,10 @@ def cli(env, sortby, cpu, domain, datacenter, hostname, memory, network, mask=columns.mask(), limit=limit) + if output_json: + env.fout(json.dumps({'vm': guests})) + return + table = formatting.Table(columns.columns) table.sortby = sortby for guest in guests: From 390540362e8d1b3b85a1dee460cb1181d05ce73f Mon Sep 17 00:00:00 2001 From: Irwen Song Date: Tue, 1 Nov 2016 16:07:10 -0700 Subject: [PATCH 35/49] A list is not accepted. Json should start from {. --- SoftLayer/CLI/virt/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index d26bfd46c..9d9de86f3 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -304,7 +304,7 @@ def cli(env, **args): env.out(env.fmt(output)) raise exceptions.CLIHalt(code=1) if args['output_json']: - env.fout(json.dumps(result)) + env.fout(json.dumps({'statuses': result})) else: for instance_data in result: table = formatting.KeyValueTable(['name', 'value']) From 6ff4291a03ed189dd3843302be5ff200a2ca10ae Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 5 May 2017 09:38:27 -0700 Subject: [PATCH 36/49] Revert "CLI hw create command currently broken for GPUs" This reverts commit f8e6ae09b41b410476d250dbf27c749eef881d2a. --- SoftLayer/managers/hardware.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 7c7e710c6..966253500 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -510,11 +510,6 @@ def _generate_create_dict(self, prices.append(_get_extra_price_id(package['items'], extra, hourly, location=location)) - - # Hack to add Disk Controller for K80 order to go through - if size == 'D2620V4_128GB_2X800GB_SSD_RAID_1_K80_GPU2': - prices.append(22482) - hardware = [{ 'hostname': hostname, 'domain': domain, From 269fe6d07aaa7d7ee4a1c53f0ec5005f48ea9768 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Wed, 7 Jun 2017 10:47:53 -0700 Subject: [PATCH 37/49] Update "nas detail" call to pull mount point field --- SoftLayer/CLI/nas/detail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/CLI/nas/detail.py b/SoftLayer/CLI/nas/detail.py index 3c5a62134..9be6abeae 100644 --- a/SoftLayer/CLI/nas/detail.py +++ b/SoftLayer/CLI/nas/detail.py @@ -5,7 +5,7 @@ from SoftLayer.CLI import formatting from SoftLayer import utils -NAS_PROPERTIES = 'id,username,serviceResourceBackendIpAddress' +NAS_PROPERTIES = 'id,username,serviceResourceBackendIpAddress,fileNetworkMountAddress' @click.command() From 185755558bfc1e2e824c09771a6eb5179dee62be Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 31 Aug 2018 16:23:11 -0700 Subject: [PATCH 38/49] Copy VSI private VLAN and subnet options over to baremetal --- SoftLayer/CLI/hardware/create.py | 9 +++++++++ SoftLayer/managers/hardware.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/SoftLayer/CLI/hardware/create.py b/SoftLayer/CLI/hardware/create.py index 9b8dc6d8c..33cf6dfb7 100644 --- a/SoftLayer/CLI/hardware/create.py +++ b/SoftLayer/CLI/hardware/create.py @@ -47,6 +47,13 @@ @click.option('--no-public', is_flag=True, help="Private network only") +@click.option('--vlan-private', + help="The ID of the private VLAN on which you want the virtual " + "server placed", + type=click.INT) +@click.option('--subnet-private', + help="The ID of the private SUBNET on which you want the virtual server placed", + type=click.INT) @helpers.multi_option('--extra', '-e', help="Extra options") @click.option('--test', is_flag=True, @@ -91,6 +98,8 @@ def cli(env, **args): 'extras': args.get('extra'), 'quantity': args.get('quantity'), } + order['private_subnet'] = args.get('subnet_private', None) + order['private_vlan'] = args.get('vlan_private', None) # Do not create hardware server with --test or --export do_create = not (args['export'] or args['test']) diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 966253500..a1337c202 100644 --- a/SoftLayer/managers/hardware.py +++ b/SoftLayer/managers/hardware.py @@ -473,6 +473,10 @@ def _generate_create_dict(self, location=None, os=None, port_speed=None, + public_vlan=None, + private_vlan=None, + private_subnet=None, + public_subnet=None, ssh_keys=None, post_uri=None, hourly=True, @@ -525,6 +529,11 @@ def _generate_create_dict(self, 'quantity': quantity } + if private_vlan or public_vlan or private_subnet or public_subnet: + network_components = self._create_network_components(public_vlan, private_vlan, + private_subnet, public_subnet) + order.update(network_components) + if post_uri: order['provisionScripts'] = [post_uri] @@ -535,6 +544,29 @@ def _generate_create_dict(self, return order + def _create_network_components( + self, public_vlan=None, private_vlan=None, + private_subnet=None, public_subnet=None): + + parameters = {} + if private_vlan: + parameters['primaryBackendNetworkComponent'] = {"networkVlan": {"id": int(private_vlan)}} + if public_vlan: + parameters['primaryNetworkComponent'] = {"networkVlan": {"id": int(public_vlan)}} + if public_subnet: + if public_vlan is None: + raise exceptions.SoftLayerError("You need to specify a public_vlan with public_subnet") + else: + parameters['primaryNetworkComponent']['networkVlan']['primarySubnet'] = {'id': int(public_subnet)} + if private_subnet: + if private_vlan is None: + raise exceptions.SoftLayerError("You need to specify a private_vlan with private_subnet") + else: + parameters['primaryBackendNetworkComponent']['networkVlan']['primarySubnet'] = { + "id": int(private_subnet)} + + return parameters + def _get_ids_from_hostname(self, hostname): """Returns list of matching hardware IDs for a given hostname.""" results = self.list_hardware(hostname=hostname, mask="id") From aef4a15ef51bcb1105bdc0fa2bf965a2ef34085b Mon Sep 17 00:00:00 2001 From: yhuang <7468443+yhuanghamu@users.noreply.github.com> Date: Wed, 12 Sep 2018 15:34:51 -0700 Subject: [PATCH 39/49] If no vs or hw exists in current subnet, should return empty list instead of "none". --- SoftLayer/CLI/subnet/detail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/subnet/detail.py b/SoftLayer/CLI/subnet/detail.py index 1c8f7e2dc..bf46cadc4 100644 --- a/SoftLayer/CLI/subnet/detail.py +++ b/SoftLayer/CLI/subnet/detail.py @@ -55,7 +55,7 @@ def cli(env, identifier, no_vs, no_hardware): vsi.get('primaryBackendIpAddress')]) table.add_row(['vs', vs_table]) else: - table.add_row(['vs', 'none']) + table.add_row(['vs', []]) if not no_hardware: if subnet['hardware']: @@ -67,6 +67,6 @@ def cli(env, identifier, no_vs, no_hardware): hardware.get('primaryBackendIpAddress')]) table.add_row(['hardware', hw_table]) else: - table.add_row(['hardware', 'none']) + table.add_row(['hardware',[]]) env.fout(table) From 32679d3bc629d59356e03daa17a5d7271410c263 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Thu, 27 Sep 2018 11:20:04 -0700 Subject: [PATCH 40/49] Update sshkey --add return value and format The "sshkey --add" does not currently return the key ID, this commit updates the CLI to return the ID. --- SoftLayer/CLI/sshkey/add.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/SoftLayer/CLI/sshkey/add.py b/SoftLayer/CLI/sshkey/add.py index a3a3c6ccb..bffc9820d 100644 --- a/SoftLayer/CLI/sshkey/add.py +++ b/SoftLayer/CLI/sshkey/add.py @@ -5,7 +5,7 @@ import click import SoftLayer -from SoftLayer.CLI import environment +from SoftLayer.CLI import environment, formatting from SoftLayer.CLI import exceptions @@ -40,4 +40,12 @@ def cli(env, label, in_file, key, note): mgr = SoftLayer.SshKeyManager(env.client) result = mgr.add_key(key_text, label, note) - env.fout("SSH key added: %s" % result.get('fingerprint')) + table = formatting.KeyValueTable(['name', 'value']) + table.align['name'] = 'r' + table.align['value'] = 'l' + + table.add_row(['id', result['id']]) + table.add_row(['label', result['label']]) + table.add_row(['fingerprint', result['fingerprint']]) + + env.fout(table) From 66f42c364ec81ed577864b699154d61b2e7a2d2f Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 28 Sep 2018 08:50:09 -0700 Subject: [PATCH 41/49] Dedicated host cancellation is pretty similar to bare metal Copied in the bare metal cancellation code and moves some fields around in the request to get it to work with the API --- SoftLayer/managers/dedicated_host.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index 86258416b..f8de7bf71 100644 --- a/SoftLayer/managers/dedicated_host.py +++ b/SoftLayer/managers/dedicated_host.py @@ -227,6 +227,27 @@ def list_instances(self, tags=None, cpus=None, memory=None, hostname=None, kwargs['filter'] = _filter.to_dict() return self.account.getDedicatedHosts(**kwargs) + + def get_cancellation_reasons(self): + """Returns a dictionary of valid cancellation reasons. + + These can be used when cancelling a dedicated server + via :func:`cancel_host`. + """ + return { + 'unneeded': 'No longer needed', + 'closing': 'Business closing down', + 'cost': 'Server / Upgrade Costs', + 'migrate_larger': 'Migrating to larger server', + 'migrate_smaller': 'Migrating to smaller server', + 'datacenter': 'Migrating to a different SoftLayer datacenter', + 'performance': 'Network performance / latency', + 'support': 'Support response / timing', + 'sales': 'Sales process / upgrades', + 'moving': 'Moving to competitor', + } + + def get_host(self, host_id, **kwargs): """Get details about a dedicated host. From c5f92ce6a82b18d32cff4efb117a5a6c9e4b50c1 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 28 Sep 2018 13:11:58 -0700 Subject: [PATCH 42/49] enable bulk orders of dedicatedhosts if multiple hostnames specified --- SoftLayer/CLI/dedicatedhost/create.py | 4 ++-- SoftLayer/managers/dedicated_host.py | 34 +++++++++++++++------------ 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/create.py b/SoftLayer/CLI/dedicatedhost/create.py index 491da2110..b50bf9cc6 100644 --- a/SoftLayer/CLI/dedicatedhost/create.py +++ b/SoftLayer/CLI/dedicatedhost/create.py @@ -12,7 +12,7 @@ @click.command( epilog="See 'slcli dedicatedhost create-options' for valid options.") -@click.option('--hostname', '-H', +@click.option('--hostnames', '-H', help="Host portion of the FQDN", required=True, prompt=True) @@ -51,7 +51,7 @@ def cli(env, **kwargs): mgr = SoftLayer.DedicatedHostManager(env.client) order = { - 'hostname': kwargs['hostname'], + 'hostnames': kwargs['hostnames'].split(','), 'domain': kwargs['domain'], 'flavor': kwargs['flavor'], 'location': kwargs['datacenter'], diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index f8de7bf71..6957b3f0f 100644 --- a/SoftLayer/managers/dedicated_host.py +++ b/SoftLayer/managers/dedicated_host.py @@ -310,7 +310,7 @@ def get_host(self, host_id, **kwargs): return self.host.getObject(id=host_id, **kwargs) - def place_order(self, hostname, domain, location, flavor, hourly, router=None): + def place_order(self, hostnames, domain, location, flavor, hourly, router=None): """Places an order for a dedicated host. See get_create_options() for valid arguments. @@ -322,7 +322,7 @@ def place_order(self, hostname, domain, location, flavor, hourly, router=None): False for monthly. :param int router: an optional value for selecting a backend router """ - create_options = self._generate_create_dict(hostname=hostname, + create_options = self._generate_create_dict(hostnames=hostnames, router=router, domain=domain, flavor=flavor, @@ -331,14 +331,15 @@ def place_order(self, hostname, domain, location, flavor, hourly, router=None): return self.client['Product_Order'].placeOrder(create_options) - def verify_order(self, hostname, domain, location, hourly, flavor, router=None): + def verify_order(self, hostnames, domain, location, hourly, flavor, router=None): """Verifies an order for a dedicated host. See :func:`place_order` for a list of available options. """ - create_options = self._generate_create_dict(hostname=hostname, - router=router, + for hostname in hostnames: + create_options = self._generate_create_dict(hostnames=[hostname], + router=router, domain=domain, flavor=flavor, datacenter=location, @@ -347,7 +348,7 @@ def verify_order(self, hostname, domain, location, hourly, flavor, router=None): return self.client['Product_Order'].verifyOrder(create_options) def _generate_create_dict(self, - hostname=None, + hostnames=None, domain=None, flavor=None, router=None, @@ -365,25 +366,28 @@ def _generate_create_dict(self, router = self._get_default_router(routers, router) - hardware = { - 'hostname': hostname, - 'domain': domain, - 'primaryBackendNetworkComponent': { - 'router': { - 'id': router + hardwares = [] + for hostname in hostnames: + hardware = { + 'hostname': hostname, + 'domain': domain, + 'primaryBackendNetworkComponent': { + 'router': { + 'id': router + } } } - } + hardwares.append(hardware) complex_type = "SoftLayer_Container_Product_Order_Virtual_DedicatedHost" order = { "complexType": complex_type, - "quantity": 1, + "quantity": len(hardwares), 'location': location['keyname'], 'packageId': package['id'], 'prices': [{'id': price}], - 'hardware': [hardware], + 'hardware': hardwares, 'useHourlyPricing': hourly, } return order From 0fdaa1263305020a7ba60cbf2e61dcdfd7c6aa7e Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Fri, 28 Sep 2018 15:19:46 -0700 Subject: [PATCH 43/49] Pull hostIds upon creation of a dedicatedhost After creating a dedicatedhost, poll for all hostIds with a matching orderId by listing all dedicatedhosts and filtering by new order_id --- SoftLayer/CLI/dedicatedhost/create.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/create.py b/SoftLayer/CLI/dedicatedhost/create.py index b50bf9cc6..77a5077bf 100644 --- a/SoftLayer/CLI/dedicatedhost/create.py +++ b/SoftLayer/CLI/dedicatedhost/create.py @@ -2,6 +2,7 @@ # :license: MIT, see LICENSE for more details. import click +import time import SoftLayer from SoftLayer.CLI import environment @@ -84,11 +85,6 @@ def cli(env, **kwargs): table.add_row(['Total monthly cost', "%.2f" % total]) output = [] - output.append(table) - output.append(formatting.FormattedItem( - '', - ' -- ! Prices reflected here are retail and do not ' - 'take account level discounts and are not guaranteed.')) if kwargs['export']: export_file = kwargs.pop('export') @@ -104,11 +100,31 @@ def cli(env, **kwargs): result = mgr.place_order(**order) + host_ids = _wait_for_host_ids(result['orderId'], mgr) + table = formatting.KeyValueTable(['name', 'value']) table.align['name'] = 'r' table.align['value'] = 'l' table.add_row(['id', result['orderId']]) table.add_row(['created', result['orderDate']]) + table.add_row(['hostIds', host_ids]) output.append(table) env.fout(output) + + +def _wait_for_host_ids(order_id, mgr): + host_ids = [] + while not host_ids: + host_ids = _extract_host_ids(order_id, mgr) + time.sleep(60) + return host_ids + + +def _extract_host_ids(order_id, mgr): + instances = mgr.list_instances(mask='mask[id,billingItem[orderItem[order]]]') + return [instance['id'] for instance in instances + if int(order_id) == instance.get('billingItem', {})\ + .get('orderItem', {})\ + .get('order', {})\ + .get('id', None)] From 3cb29170afef340c9853f96fd51e67c8c58844f1 Mon Sep 17 00:00:00 2001 From: Mark Whitney Date: Mon, 1 Oct 2018 13:28:20 -0700 Subject: [PATCH 44/49] Update dedicatedhost create response with new fields: hosts, datacenter ``` $ slcli -y --format=json dedicatedhost create -H dedtest3 -D rescale.com -d wdc07 -f 56_CORES_X_242_RAM_X_1_4_TB --billing hourly { "hosts": [ { "datacenter": "wdc07", "hostName": "dedtest3", "hostId": 247017 } ], "id": 29794663, "created": "2018-10-01T14:23:46-06:00" } ``` --- SoftLayer/CLI/dedicatedhost/create.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/SoftLayer/CLI/dedicatedhost/create.py b/SoftLayer/CLI/dedicatedhost/create.py index 77a5077bf..f3de7c7c0 100644 --- a/SoftLayer/CLI/dedicatedhost/create.py +++ b/SoftLayer/CLI/dedicatedhost/create.py @@ -100,14 +100,14 @@ def cli(env, **kwargs): result = mgr.place_order(**order) - host_ids = _wait_for_host_ids(result['orderId'], mgr) + hosts = _wait_for_host_ids(result['orderId'], mgr) table = formatting.KeyValueTable(['name', 'value']) table.align['name'] = 'r' table.align['value'] = 'l' table.add_row(['id', result['orderId']]) table.add_row(['created', result['orderDate']]) - table.add_row(['hostIds', host_ids]) + table.add_row(['hosts', hosts]) output.append(table) env.fout(output) @@ -122,9 +122,13 @@ def _wait_for_host_ids(order_id, mgr): def _extract_host_ids(order_id, mgr): - instances = mgr.list_instances(mask='mask[id,billingItem[orderItem[order]]]') - return [instance['id'] for instance in instances - if int(order_id) == instance.get('billingItem', {})\ - .get('orderItem', {})\ - .get('order', {})\ - .get('id', None)] + instances = mgr.list_instances(mask='mask[id,name,datacenter[name],' + 'billingItem[orderItem[order]]]') + return [{'hostName': instance.get('billingItem', {})['hostName'], + 'hostId': instance['id'], + 'datacenter': instance.get('datacenter', {})['name']} + for instance in instances + if order_id == instance.get('billingItem', {})\ + .get('orderItem', {})\ + .get('order', {})\ + .get('id', None)] From ae99ac11f4b65b21521f143680baff5bbc830212 Mon Sep 17 00:00:00 2001 From: Yingchao Huang Date: Tue, 9 Oct 2018 17:48:47 -0700 Subject: [PATCH 45/49] dedicatedhost Edit and show tags 1. Add new cli command to edit tags of dedicatedhost 2. Show tags in dedicatedhost detail e.g. ``` slcli dedicatedhost edit --tag nicetag 258803 slcli --format=json dedicatedhost detail 258803 slcli --format=json dedicatedhost detail 258803 { "modify date": "", "name": "dev-ryans-cluster0650-cluster-service-compute0-0", "cpu count": 56, "router hostname": "bcr01a.dal10", "memory capacity": 242, "tags": [ "nicetag" ], "disk capacity": 1200, "guest count": 0, "create date": "2018-10-10T12:46:35-06:00", "router id": 843613, "owner": "1703415_mark+ibmdev@rescale.com_2018-08-02-13.24.01", "datacenter": "dal10", "id": 258803 } ``` --- SoftLayer/CLI/dedicatedhost/detail.py | 1 + SoftLayer/CLI/dedicatedhost/edit.py | 61 +++++++++++++++++++++++++++ SoftLayer/CLI/routes.py | 1 + SoftLayer/managers/dedicated_host.py | 61 ++++++++++++++++++++++++++- tools/requirements.txt | 1 + 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 SoftLayer/CLI/dedicatedhost/edit.py diff --git a/SoftLayer/CLI/dedicatedhost/detail.py b/SoftLayer/CLI/dedicatedhost/detail.py index e1c46b962..913bb3d94 100644 --- a/SoftLayer/CLI/dedicatedhost/detail.py +++ b/SoftLayer/CLI/dedicatedhost/detail.py @@ -40,6 +40,7 @@ def cli(env, identifier, price=False, guests=False): table.add_row(['router hostname', result['backendRouter']['hostname']]) table.add_row(['owner', formatting.FormattedItem( utils.lookup(result, 'billingItem', 'orderItem', 'order', 'userRecord', 'username') or formatting.blank(),)]) + table.add_row(['tags', formatting.tags(result['tagReferences'])]) if price: total_price = utils.lookup(result, diff --git a/SoftLayer/CLI/dedicatedhost/edit.py b/SoftLayer/CLI/dedicatedhost/edit.py new file mode 100644 index 000000000..3f87457ec --- /dev/null +++ b/SoftLayer/CLI/dedicatedhost/edit.py @@ -0,0 +1,61 @@ +"""Edit dedicated host details.""" +# :license: MIT, see LICENSE for more details. + +import click + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +from SoftLayer.CLI import helpers + + +@click.command() +@click.argument('identifier') +@click.option('--domain', '-D', help="Domain portion of the FQDN") +@click.option('--userfile', '-F', + help="Read userdata from file", + type=click.Path(exists=True, readable=True, resolve_path=True)) +@click.option('--tag', '-g', + multiple=True, + help="Tags to set or empty string to remove all") +@click.option('--hostname', '-H', help="Host portion of the FQDN") +@click.option('--userdata', '-u', help="User defined metadata string") +@click.option('--public-speed', + help="Public port speed.", + default=None, + type=click.Choice(['0', '10', '100', '1000', '10000'])) +@click.option('--private-speed', + help="Private port speed.", + default=None, + type=click.Choice(['0', '10', '100', '1000', '10000'])) +@environment.pass_env +def cli(env, identifier, domain, userfile, tag, hostname, userdata, + public_speed, private_speed): + """Edit dedicated host details.""" + + if userdata and userfile: + raise exceptions.ArgumentError( + '[-u | --userdata] not allowed with [-F | --userfile]') + + data = { + 'hostname': hostname, + 'domain': domain, + } + if userdata: + data['userdata'] = userdata + elif userfile: + with open(userfile, 'r') as userfile_obj: + data['userdata'] = userfile_obj.read() + if tag: + data['tags'] = ','.join(tag) + + mgr = SoftLayer.DedicatedHostManager(env.client) + + if not mgr.edit(identifier, **data): + raise exceptions.CLIAbort("Failed to update dedicated host") + + if public_speed is not None: + mgr.change_port_speed(identifier, True, int(public_speed)) + + if private_speed is not None: + mgr.change_port_speed(identifier, False, int(private_speed)) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 25faf0d3a..1eb054bd6 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -50,6 +50,7 @@ ('dedicatedhost:cancel', 'SoftLayer.CLI.dedicatedhost.cancel:cli'), ('dedicatedhost:cancel-guests', 'SoftLayer.CLI.dedicatedhost.cancel_guests:cli'), ('dedicatedhost:list-guests', 'SoftLayer.CLI.dedicatedhost.list_guests:cli'), + ('dedicatedhost:edit', 'SoftLayer.CLI.dedicatedhost.edit:cli'), ('cdn', 'SoftLayer.CLI.cdn'), ('cdn:detail', 'SoftLayer.CLI.cdn.detail:cli'), diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index 6957b3f0f..9fa48822d 100644 --- a/SoftLayer/managers/dedicated_host.py +++ b/SoftLayer/managers/dedicated_host.py @@ -171,6 +171,50 @@ def list_guests(self, host_id, tags=None, cpus=None, memory=None, hostname=None, kwargs['iter'] = True return self.host.getGuests(id=host_id, **kwargs) + def edit(self, host_id, userdata=None, hostname=None, domain=None, + notes=None, tags=None): + """Edit hostname, domain name, notes, user data of the dedicated host. + + Parameters set to None will be ignored and not attempted to be updated. + + :param integer host_id: the instance ID to edit + :param string userdata: user data on the dedicated host to edit. + If none exist it will be created + :param string hostname: valid hostname + :param string domain: valid domain name + :param string notes: notes about this particular dedicated host + :param string tags: tags to set on the dedicated host as a comma + separated list. Use the empty string to remove all + tags. + + Example:: + + # Change the hostname on instance 12345 to 'something' + result = mgr.edit(host_id=12345 , hostname="something") + #result will be True or an Exception + """ + + obj = {} + if userdata: + self.host.setUserMetadata([userdata], id=host_id) + + if tags is not None: + self.host.setTags(tags, id=host_id) + + if hostname: + obj['hostname'] = hostname + + if domain: + obj['domain'] = domain + + if notes: + obj['notes'] = notes + + if not obj: + return True + + return self.host.editObject(obj, id=host_id) + def list_instances(self, tags=None, cpus=None, memory=None, hostname=None, disk=None, datacenter=None, **kwargs): """Retrieve a list of all dedicated hosts on the account @@ -305,7 +349,14 @@ def get_host(self, host_id, **kwargs): domain, uuid ], - guestCount + guestCount, + tagReferences[ + id, + tag[ + name, + id + ] + ] ''') return self.host.getObject(id=host_id, **kwargs) @@ -553,3 +604,11 @@ def _delete_guest(self, guest_id): msg = 'Exception: ' + e.faultString return msg + + # @retry(logger=LOGGER) + def set_tags(self, tags, host_id): + """Sets tags on a dedicated_host with a retry decorator + + Just calls guest.setTags, but if it fails from an APIError will retry + """ + self.host.setTags(tags, id=host_id) diff --git a/tools/requirements.txt b/tools/requirements.txt index 0d7746444..880810646 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,3 +1,4 @@ +prettytable >= 0.7.0 six >= 1.7.0 ptable >= 0.9.2 click >= 7 From 82e24c77b3a804eb53eb8f744ab16a645ab32e04 Mon Sep 17 00:00:00 2001 From: Yingchao Huang Date: Tue, 23 Jul 2019 12:34:43 -0700 Subject: [PATCH 46/49] update cli option --- SoftLayer/CLI/virt/create.py | 32 +------------------------------- 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index 9d9de86f3..a3c2c6658 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -161,41 +161,11 @@ def _parse_create_args(client, args): @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('--billing', type=click.Choice(['hourly', 'monthly']), default='hourly', show_default=True, -@click.command(epilog="See 'slcli vs create-options' for valid options") +@click.option('--billing', type=click.Choice(['hourly', 'monthly']), default='hourly', show_default=True, help="Billing rate") @click.option('--hostnames', '-H', help="Hosts portion of the FQDN", required=True, prompt=True) -@click.option('--domain', '-D', - help="Domain portion of the FQDN", - required=True, - prompt=True) -@click.option('--cpu', '-c', - help="Number of CPU cores (not available with flavors)", - type=click.INT) -@click.option('--memory', '-m', - help="Memory in mebibytes (not available with flavors)", - type=virt.MEM_TYPE) -@click.option('--flavor', '-f', - help="Public Virtual Server flavor key name", - type=click.STRING) -@click.option('--datacenter', '-d', - help="Datacenter shortname", - required=True, - prompt=True) -@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', - help="Specify the mode to boot the OS in. Supported modes are HVM and PV.", - type=click.STRING) -@click.option('--billing', - type=click.Choice(['hourly', 'monthly']), - default='hourly', - show_default=True, - help="Billing rate") @click.option('--dedicated/--public', is_flag=True, help="Create a Dedicated Virtual Server") @click.option('--host-id', type=click.INT, help="Host Id to provision a Dedicated Host Virtual Server onto") @click.option('--san', is_flag=True, help="Use SAN storage instead of local disk.") From 258f5f7a43150bee822cfa4d461fc4d05846c65d Mon Sep 17 00:00:00 2001 From: Yingchao Huang Date: Tue, 23 Jul 2019 13:32:23 -0700 Subject: [PATCH 47/49] Update virtual servers creation 1. Remove out of date customized format. 2. Use vsi.create_instances batch instances creation instead of for-loop vsi.order_guest --- SoftLayer/CLI/virt/create.py | 39 +++++++----------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index e446649a1..578f5d523 100644 --- a/SoftLayer/CLI/virt/create.py +++ b/SoftLayer/CLI/virt/create.py @@ -90,7 +90,7 @@ def _parse_create_args(client, args): "host_id": args.get('host_id', None), "private": args.get('private', None), "transient": args.get('transient', None), - "hostname": args.get('hostname', None), + "hostname": hostname, "nic_speed": args.get('network', None), "boot_mode": args.get('boot_mode', None), "dedicated": args.get('dedicated', None), @@ -126,7 +126,7 @@ def _parse_create_args(client, args): # Get the SSH keys if args.get('key'): - keys = [] + keys = [] for key in args.get('key'): resolver = SoftLayer.SshKeyManager(client).resolve_ids key_id = helpers.resolve_id(resolver, key, 'SshKey') @@ -152,7 +152,6 @@ def _parse_create_args(client, args): @click.command(epilog="See 'slcli vs create-options' for valid options") -@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('--cpu', '-c', type=click.INT, help="Number of CPU cores (not available with flavors)") @click.option('--memory', '-m', type=virt.MEM_TYPE, help="Memory in mebibytes (not available with flavors)") @@ -244,7 +243,7 @@ def cli(env, **args): vsi = SoftLayer.VSManager(env.client) _validate_args(env, args) - create_args = _parse_create_args(env.client, args) + config_list = _parse_create_args(env.client, args) test = args.get('test', False) do_create = not (args.get('export') or test) @@ -259,37 +258,12 @@ def cli(env, **args): env.fout('Successfully exported options to a template file.') else: - for create_args in config_list: - result = vsi.order_guest(create_args, test) - output = _build_receipt_table(result, args.get('billing'), test) - - if do_create: - env.fout(_build_guest_table(result)) - env.fout(output) - - if args.get('wait'): - virtual_guests = utils.lookup(result, 'orderDetails', 'virtualGuests') - guest_id = virtual_guests[0]['id'] - click.secho("Waiting for %s to finish provisioning..." % guest_id, fg='green') - ready = vsi.wait_for_ready(guest_id, args.get('wait') or 1) - if ready is False: - env.out(env.fmt(output)) - raise exceptions.CLIHalt(code=1) - + result = vsi.create_instances(config_list) if args['output_json']: env.fout(json.dumps({'statuses': result})) else: - for instance_data in result: - table = formatting.KeyValueTable(['name', 'value']) - table.align['name'] = 'r' - table.align['value'] = 'l' - table.add_row(['id', instance_data['id']]) - table.add_row(['hostname', instance_data['hostname']]) - table.add_row(['created', instance_data['createDate']]) - table.add_row(['uuid', instance_data['uuid']]) - output.append(table) - - env.fout(output) + env.fout(result) + def _build_receipt_table(result, billing="hourly", test=False): """Retrieve the total recurring fee of the items prices""" @@ -314,6 +288,7 @@ def _build_receipt_table(result, billing="hourly", test=False): table.add_row(["%.3f" % total, "Total %s cost" % billing]) return table + def _build_guest_table(result): table = formatting.Table(['ID', 'FQDN', 'guid', 'Order Date']) table.align['name'] = 'r' From 2bd5ceef3fe86245a9f75fa73d7769975fd7c0c0 Mon Sep 17 00:00:00 2001 From: Yingchao Huang Date: Wed, 24 Jul 2019 20:52:17 -0700 Subject: [PATCH 48/49] Update test cases Minium update, Not all tests passed. --- tests/CLI/modules/dedicatedhost_tests.py | 18 +++++++++-------- tests/CLI/modules/server_tests.py | 16 +++++++++------ tests/CLI/modules/sshkey_tests.py | 4 ++-- tests/CLI/modules/subnet_tests.py | 2 +- tests/CLI/modules/vs/vs_create_tests.py | 4 ++-- tests/managers/dedicated_host_tests.py | 25 +++++++++++++++--------- tests/managers/hardware_tests.py | 7 ++++--- 7 files changed, 45 insertions(+), 31 deletions(-) diff --git a/tests/CLI/modules/dedicatedhost_tests.py b/tests/CLI/modules/dedicatedhost_tests.py index 077c3f033..d20ce5355 100644 --- a/tests/CLI/modules/dedicatedhost_tests.py +++ b/tests/CLI/modules/dedicatedhost_tests.py @@ -61,7 +61,8 @@ def test_details(self): 'owner': 'test-dedicated', 'price_rate': 1515.556, 'router hostname': 'bcr01a.dal05', - 'router id': 12345} + 'router id': 12345, + 'tags': None} ) def test_details_no_owner(self): @@ -91,7 +92,8 @@ def test_details_no_owner(self): 'owner': None, 'price_rate': 0, 'router hostname': 'bcr01a.dal05', - 'router id': 12345} + 'router id': 12345, + 'tags': None} ) def test_create_options(self): @@ -154,7 +156,7 @@ def test_create(self): mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH result = self.run_command(['dedicatedhost', 'create', - '--hostname=test-dedicated', + '--hostnames=test-dedicated', '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', @@ -190,7 +192,7 @@ def test_create_with_gpu(self): mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDHGpu result = self.run_command(['dedicatedhost', 'create', - '--hostname=test-dedicated', + '--hostnames=test-dedicated', '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_484_RAM_X_1_5_TB_X_2_GPU_P100', @@ -228,7 +230,7 @@ def test_create_verify(self): result = self.run_command(['dedicatedhost', 'create', '--verify', - '--hostname=test-dedicated', + '--hostnames=test-dedicated', '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', @@ -258,7 +260,7 @@ def test_create_verify(self): result = self.run_command(['dh', 'create', '--verify', - '--hostname=test-dedicated', + '--hostnames=test-dedicated', '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', @@ -291,7 +293,7 @@ def test_create_aborted(self): mock_package_obj.return_value = SoftLayer_Product_Package.getAllObjectsDH result = self.run_command(['dh', 'create', - '--hostname=test-dedicated', + '--hostnames=test-dedicated', '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', @@ -310,7 +312,7 @@ def test_create_verify_no_price_or_more_than_one(self): result = self.run_command(['dedicatedhost', 'create', '--verify', - '--hostname=test-dedicated', + '--hostnames=test-dedicated', '--domain=test.com', '--datacenter=dal05', '--flavor=56_CORES_X_242_RAM_X_1_4_TB', diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index f14118bc1..61ab12a20 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -293,7 +293,7 @@ def test_create_server_test_flag(self, verify_mock): result = self.run_command(['--really', 'server', 'create', '--size=S1270_8GB_2X1TBSATA_NORAID', - '--hostname=test', + '--hostnames=test', '--domain=example.com', '--datacenter=TEST00', '--port-speed=100', @@ -332,7 +332,7 @@ def test_create_server(self, order_mock): result = self.run_command(['--really', 'server', 'create', '--size=S1270_8GB_2X1TBSATA_NORAID', - '--hostname=test', + '--hostnames=test', '--domain=example.com', '--datacenter=TEST00', '--port-speed=100', @@ -350,7 +350,7 @@ def test_create_server_missing_required(self): # This is missing a required argument result = self.run_command(['server', 'create', # Note: no chassis id - '--hostname=test', + '--hostnames=test', '--domain=example.com', '--datacenter=TEST00', '--network=100', @@ -366,7 +366,7 @@ def test_create_server_with_export(self, export_mock): self.skipTest("Test doesn't work in Windows") result = self.run_command(['--really', 'server', 'create', '--size=S1270_8GB_2X1TBSATA_NORAID', - '--hostname=test', + '--hostnames=test', '--domain=example.com', '--datacenter=TEST00', '--port-speed=100', @@ -383,7 +383,7 @@ def test_create_server_with_export(self, export_mock): 'datacenter': 'TEST00', 'domain': 'example.com', 'extra': (), - 'hostname': 'test', + 'hostnames': 'test', 'key': (), 'os': 'UBUNTU_12_64', 'port_speed': 100, @@ -392,7 +392,11 @@ def test_create_server_with_export(self, export_mock): 'test': False, 'no_public': True, 'wait': None, - 'template': None}, + 'template': None, + 'output_json': False, + 'subnet_private': None, + 'vlan_private': None, + 'quantity': 1,}, exclude=['wait', 'test']) def test_edit_server_userdata_and_file(self): diff --git a/tests/CLI/modules/sshkey_tests.py b/tests/CLI/modules/sshkey_tests.py index 253309c08..3fea5ce68 100644 --- a/tests/CLI/modules/sshkey_tests.py +++ b/tests/CLI/modules/sshkey_tests.py @@ -41,7 +41,7 @@ def test_add_by_option(self): self.assert_no_fail(result) self.assertEqual(json.loads(result.output), - "SSH key added: aa:bb:cc:dd") + {'fingerprint': 'aa:bb:cc:dd', 'id': 1234, 'label': 'label'}) self.assert_called_with('SoftLayer_Security_Ssh_Key', 'createObject', args=({'notes': 'my key', 'key': mock_key, @@ -55,7 +55,7 @@ def test_add_by_file(self): self.assert_no_fail(result) self.assertEqual(json.loads(result.output), - "SSH key added: aa:bb:cc:dd") + {'fingerprint': 'aa:bb:cc:dd', 'id': 1234, 'label': 'label'}) service = self.client['Security_Ssh_Key'] mock_key = service.getObject()['key'] self.assert_called_with('SoftLayer_Security_Ssh_Key', 'createObject', diff --git a/tests/CLI/modules/subnet_tests.py b/tests/CLI/modules/subnet_tests.py index 1971aa420..295964758 100644 --- a/tests/CLI/modules/subnet_tests.py +++ b/tests/CLI/modules/subnet_tests.py @@ -36,7 +36,7 @@ def test_detail(self): 'private_ip': '10.0.1.2' } ], - 'hardware': 'none', + 'hardware': [], 'usable ips': 22 }, json.loads(result.output)) diff --git a/tests/CLI/modules/vs/vs_create_tests.py b/tests/CLI/modules/vs/vs_create_tests.py index 761778db6..46d33057e 100644 --- a/tests/CLI/modules/vs/vs_create_tests.py +++ b/tests/CLI/modules/vs/vs_create_tests.py @@ -20,7 +20,7 @@ def test_create(self, confirm_mock): result = self.run_command(['vs', 'create', '--cpu=2', '--domain=example.com', - '--hostname=host', + '--hostnames=host', '--os=UBUNTU_LATEST', '--memory=1', '--network=100', @@ -441,7 +441,7 @@ def test_create_like_image(self, confirm_mock): 'blockDeviceTemplateGroup': {'globalIdentifier': 'aaa1xxx1122233'}, 'networkComponents': [{'maxSpeed': 100}], 'supplementalCreateObjectOptions': {'bootMode': None}},) - self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=args) + self.assert_called_with('SoftLayer_Virtual_Guest', 'generateOrderTemplate', args=args) # @mock.patch('SoftLayer.CLI.formatting.confirm') def test_create_like_flavor(self, confirm_mock): diff --git a/tests/managers/dedicated_host_tests.py b/tests/managers/dedicated_host_tests.py index 6888db3ce..de376c007 100644 --- a/tests/managers/dedicated_host_tests.py +++ b/tests/managers/dedicated_host_tests.py @@ -78,7 +78,14 @@ def test_get_host(self): domain, uuid ], - guestCount + guestCount, + tagReferences[ + id, + tag[ + name, + id + ] + ] ''') self.dedicated_host.host.getObject.assert_called_once_with(id=12345, mask=mask) @@ -116,13 +123,13 @@ def test_place_order(self): hourly = True flavor = '56_CORES_X_242_RAM_X_1_4_TB' - self.dedicated_host.place_order(hostname=hostname, + self.dedicated_host.place_order(hostnames=[hostname], domain=domain, location=location, flavor=flavor, hourly=hourly) - create_dict.assert_called_once_with(hostname=hostname, + create_dict.assert_called_once_with(hostnames=[hostname], router=None, domain=domain, datacenter=location, @@ -167,13 +174,13 @@ def test_place_order_with_gpu(self): hourly = True flavor = '56_CORES_X_484_RAM_X_1_5_TB_X_2_GPU_P100' - self.dedicated_host.place_order(hostname=hostname, + self.dedicated_host.place_order(hostnames=[hostname], domain=domain, location=location, flavor=flavor, hourly=hourly) - create_dict.assert_called_once_with(hostname=hostname, + create_dict.assert_called_once_with(hostnames=[hostname], router=None, domain=domain, datacenter=location, @@ -218,13 +225,13 @@ def test_verify_order(self): hourly = True flavor = '56_CORES_X_242_RAM_X_1_4_TB' - self.dedicated_host.verify_order(hostname=hostname, + self.dedicated_host.verify_order(hostnames=[hostname], domain=domain, location=location, flavor=flavor, hourly=hourly) - create_dict.assert_called_once_with(hostname=hostname, + create_dict.assert_called_once_with(hostnames=[hostname], router=None, domain=domain, datacenter=location, @@ -248,7 +255,7 @@ def test_generate_create_dict_without_router(self): hourly = True flavor = '56_CORES_X_242_RAM_X_1_4_TB' - results = self.dedicated_host._generate_create_dict(hostname=hostname, + results = self.dedicated_host._generate_create_dict(hostnames=[hostname], domain=domain, datacenter=location, flavor=flavor, @@ -294,7 +301,7 @@ def test_generate_create_dict_with_router(self): flavor = '56_CORES_X_242_RAM_X_1_4_TB' results = self.dedicated_host._generate_create_dict( - hostname=hostname, + hostnames=[hostname], router=router, domain=domain, datacenter=location, diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py index 461094be6..5d9a9c597 100644 --- a/tests/managers/hardware_tests.py +++ b/tests/managers/hardware_tests.py @@ -18,7 +18,7 @@ MINIMAL_TEST_CREATE_ARGS = { 'size': 'S1270_8GB_2X1TBSATA_NORAID', - 'hostname': 'unicorn', + 'hostnames': ['unicorn'], 'domain': 'giggles.woo', 'location': 'wdc01', 'os': 'UBUNTU_14_64', @@ -177,7 +177,7 @@ def test_generate_create_dict_no_regions(self): def test_generate_create_dict_invalid_size(self): args = { 'size': 'UNKNOWN_SIZE', - 'hostname': 'unicorn', + 'hostnames': ['unicorn'], 'domain': 'giggles.woo', 'location': 'wdc01', 'os': 'UBUNTU_14_64', @@ -191,7 +191,7 @@ def test_generate_create_dict_invalid_size(self): def test_generate_create_dict(self): args = { 'size': 'S1270_8GB_2X1TBSATA_NORAID', - 'hostname': 'unicorn', + 'hostnames': ['unicorn'], 'domain': 'giggles.woo', 'location': 'wdc01', 'os': 'UBUNTU_14_64', @@ -220,6 +220,7 @@ def test_generate_create_dict(self): 'useHourlyPricing': True, 'provisionScripts': ['http://example.com/script.php'], 'sshKeys': [{'sshKeyIds': [10]}], + 'quantity': 1, } data = self.hardware._generate_create_dict(**args) From 1cdf2f761d286cc759fbaae35c539f28d5744c65 Mon Sep 17 00:00:00 2001 From: Yingchao Huang Date: Thu, 8 Aug 2019 10:50:37 -0700 Subject: [PATCH 49/49] Bug in v5.7.2 release If vm/hw in reclaim status, there will be an error --- SoftLayer/CLI/hardware/detail.py | 7 ++++--- SoftLayer/CLI/virt/detail.py | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/SoftLayer/CLI/hardware/detail.py b/SoftLayer/CLI/hardware/detail.py index ea36fb6dd..d9a68f8c1 100644 --- a/SoftLayer/CLI/hardware/detail.py +++ b/SoftLayer/CLI/hardware/detail.py @@ -80,9 +80,10 @@ def cli(env, identifier, passwords, price, output_json, verbose): table.add_row(['vlans', vlan_table]) - bandwidth = hardware.get_bandwidth_allocation(hardware_id) - bw_table = _bw_table(bandwidth) - table.add_row(['Bandwidth', bw_table]) + # Bug in v5.7.2 + # bandwidth = hardware.get_bandwidth_allocation(hardware_id) + # bw_table = _bw_table(bandwidth) + # table.add_row(['Bandwidth', bw_table]) if result.get('notes'): table.add_row(['notes', result['notes']]) diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 28ab46b48..83b49c445 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -92,8 +92,9 @@ def cli(env, identifier, passwords=False, price=False, output_json=False, table.add_row(_get_owner_row(result)) table.add_row(_get_vlan_table(result)) - bandwidth = vsi.get_bandwidth_allocation(vs_id) - table.add_row(['Bandwidth', _bw_table(bandwidth)]) + # Bug in v5.7.2 + # bandwidth = vsi.get_bandwidth_allocation(vs_id) + # table.add_row(['Bandwidth', _bw_table(bandwidth)]) security_table = _get_security_table(result) if security_table is not None: