diff --git a/SoftLayer/CLI/dedicatedhost/create.py b/SoftLayer/CLI/dedicatedhost/create.py index 491da2110..f3de7c7c0 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 @@ -12,7 +13,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 +52,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'], @@ -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,35 @@ def cli(env, **kwargs): result = mgr.place_order(**order) + 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(['hosts', hosts]) 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,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)] 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/hardware/create.py b/SoftLayer/CLI/hardware/create.py index 472cec2e2..33cf6dfb7 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', @@ -46,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, @@ -62,6 +70,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 +85,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,7 +96,10 @@ 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'), } + 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']) @@ -129,11 +142,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..d9a68f8c1 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 @@ -57,9 +80,10 @@ def cli(env, identifier, passwords, price): 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/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..9be6abeae --- /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,fileNetworkMountAddress' + + +@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..41f57da53 --- /dev/null +++ b/SoftLayer/CLI/nas/grant_access.py @@ -0,0 +1,33 @@ +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=False) +@click.option('--vm-id', required=False) +@click.option('--storage-id', required=True) +@environment.pass_env +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.') + + 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/revoke_access.py b/SoftLayer/CLI/nas/revoke_access.py new file mode 100644 index 000000000..53c3ec119 --- /dev/null +++ b/SoftLayer/CLI/nas/revoke_access.py @@ -0,0 +1,34 @@ +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=False) +@click.option('--vm-id', required=False) +@click.option('--storage-id', required=True) +@environment.pass_env +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.') + + 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 5524b6948..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'), @@ -189,6 +190,9 @@ ('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'), + ('nas:revoke-access', 'SoftLayer.CLI.nas.revoke_access:cli'), ('object-storage', 'SoftLayer.CLI.object_storage'), @@ -232,6 +236,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/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) 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) diff --git a/SoftLayer/CLI/virt/create.py b/SoftLayer/CLI/virt/create.py index 70430bc8f..578f5d523 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 @@ -69,88 +70,88 @@ 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.get('billing', 'hourly') == 'hourly', - "cpus": args.get('cpu', None), - "ipv6": args.get('ipv6', None), - "disks": args.get('disk', None), - "os_code": args.get('os', None), - "memory": args.get('memory', None), - "flavor": args.get('flavor', None), - "domain": args.get('domain', None), - "host_id": args.get('host_id', None), - "private": args.get('private', None), - "transient": args.get('transient', None), - "hostname": args.get('hostname', None), - "nic_speed": args.get('network', None), - "boot_mode": args.get('boot_mode', None), - "dedicated": args.get('dedicated', None), - "post_uri": args.get('postinstall', None), - "datacenter": args.get('datacenter', None), - "public_vlan": args.get('vlan_public', None), - "private_vlan": args.get('vlan_private', None), - "public_subnet": args.get('subnet_public', None), - "private_subnet": args.get('subnet_private', 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.get('san') - - 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.get('billing', 'hourly') == 'hourly', + "cpus": args.get('cpu', None), + "ipv6": args.get('ipv6', None), + "disks": args.get('disk', None), + "os_code": args.get('os', None), + "memory": args.get('memory', None), + "flavor": args.get('flavor', None), + "domain": args.get('domain', None), + "host_id": args.get('host_id', None), + "private": args.get('private', None), + "transient": args.get('transient', None), + "hostname": hostname, + "nic_speed": args.get('network', None), + "boot_mode": args.get('boot_mode', None), + "dedicated": args.get('dedicated', None), + "post_uri": args.get('postinstall', None), + "datacenter": args.get('datacenter', None), + "public_vlan": args.get('vlan_public', None), + "private_vlan": args.get('vlan_private', None), + "public_subnet": args.get('subnet_public', None), + "private_subnet": args.get('subnet_private', 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'] - - if args.get('userdata'): - data['userdata'] = args['userdata'] - elif args.get('userfile'): - with open(args['userfile'], 'r') as userfile: - data['userdata'] = userfile.read() + data['local_disk'] = not args.get('san') + + 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['local_disk'] = not args['san'] + + 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('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('public_security_group'): - pub_groups = args.get('public_security_group') - data['public_security_groups'] = [group for group in pub_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('private_security_group'): + priv_groups = args.get('private_security_group') + data['private_security_groups'] = [group for group in priv_groups] - 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('tag'): + data['tags'] = ','.join(args['tag']) - if args.get('tag', False): - data['tags'] = ','.join(args['tag']) + if args.get('placementgroup'): + resolver = SoftLayer.managers.PlacementManager(client).resolve_ids + data['placement_id'] = helpers.resolve_id(resolver, args.get('placementgroup'), 'PlacementGroup') - if args.get('host_id'): - data['host_id'] = args['host_id'] - - if args.get('placementgroup'): - resolver = SoftLayer.managers.PlacementManager(client).resolve_ids - data['placement_id'] = helpers.resolve_id(resolver, args.get('placementgroup'), 'PlacementGroup') - - return data + config_list.append(data) + return config_list @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)") @@ -160,8 +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, - help="Billing rate") +@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('--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.") @@ -203,13 +207,43 @@ 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.""" 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) @@ -224,21 +258,11 @@ def cli(env, **args): env.fout('Successfully exported options to a template file.') else: - 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: + env.fout(result) def _build_receipt_table(result, billing="hourly", test=False): diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 53ae7e04d..83b49c445 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']]) @@ -68,8 +92,9 @@ def cli(env, identifier, passwords=False, price=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: 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: 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: diff --git a/SoftLayer/managers/dedicated_host.py b/SoftLayer/managers/dedicated_host.py index 86258416b..db8c6d8e1 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 @@ -227,6 +271,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. @@ -284,12 +349,19 @@ def get_host(self, host_id, **kwargs): domain, uuid ], - guestCount + guestCount, + tagReferences[ + id, + tag[ + name, + id + ] + ] ''') 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. @@ -301,7 +373,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, @@ -310,14 +382,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, @@ -326,7 +399,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, @@ -344,25 +417,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 @@ -526,5 +602,12 @@ def _delete_guest(self, guest_id): self.guest.deleteObject(id=guest_id) except SoftLayer.SoftLayerAPIError as e: 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/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py index 4a52a5236..a1337c202 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,16 +468,21 @@ def _get_package(self): def _generate_create_dict(self, size=None, - hostname=None, + hostnames=None, domain=None, 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, no_public=False, - extras=None): + extras=None, + quantity=1): """Translates arguments into a dictionary for creating a server.""" extras = extras or [] @@ -499,29 +514,59 @@ 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 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] 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 + 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") @@ -869,3 +914,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 + ) 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) 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