diff --git a/CHANGELOG b/CHANGELOG index 39c1ee470..60b8a22a0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +5.1.0 + + * Added block storage functionality. You can order, list, detail, cancel volumes. You can list and delete snapshots. You can also list ACLs for volumes. + + * Added functionality to attach/detach devices to tickets + + * Virtual list now lists users and passwords for all known software + + * Fixes bug with `vlan detail` CLI command + 5.0.1 * Adds missing depdendency that was previously pulled in by prompt_toolkit diff --git a/README.rst b/README.rst index d8949f2f0..31da9b61b 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,7 @@ This library relies on the `requests `_ librar System Requirements ------------------- -* Python 2.7, 3.3, or 3.4. +* Python 2.7, 3.3 or higher. * A valid SoftLayer API username and key. * A connection to SoftLayer's private network is required to use our private network API endpoints. diff --git a/SoftLayer/CLI/block/__init__.py b/SoftLayer/CLI/block/__init__.py new file mode 100644 index 000000000..7ccee03c6 --- /dev/null +++ b/SoftLayer/CLI/block/__init__.py @@ -0,0 +1 @@ +"""Block Storage.""" diff --git a/SoftLayer/CLI/block/access_list.py b/SoftLayer/CLI/block/access_list.py new file mode 100644 index 000000000..4b45a54b7 --- /dev/null +++ b/SoftLayer/CLI/block/access_list.py @@ -0,0 +1,123 @@ +"""List hosts with access to volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + + +def _format_name(obj): + if obj['type'] == 'VIRTUAL': + return "{0}.{1}".format(obj['hostname'], obj['domain']) + + elif obj['type'] == 'HARDWARE': + return "{0}.{1}".format(obj['hostname'], obj['domain']) + + elif obj['type'] == 'SUBNET': + name = "{0}/{1}".format( + obj['networkIdentifier'], + obj['cidr'] + ) + if 'note' in obj.keys(): + name = "{0} ({1})".format(name, obj['note']) + + return name + + elif obj['type'] == 'IP': + name = obj['ipAddress'] + if 'note' in obj.keys(): + name = "{0} ({1})".format(name, obj['note']) + + return name + else: + raise Exception('Unknown type %s' % obj['type']) + + +COLUMNS = [ + column_helper.Column('id', ('id',)), + column_helper.Column('name', _format_name, """ +allowedVirtualGuests[hostname,domain], +allowedHardware[hostname,domain], +allowedSubnets[networkIdentifier,cidr,note], +allowedIpAddresses[ipAddress,note], +"""), + column_helper.Column('type', ('type',)), + column_helper.Column( + 'private_ip_address', + ('primaryBackendIpAddress',), + """ +allowedVirtualGuests.primaryBackendIpAddress +allowedHardware.primaryBackendIpAddress +allowedSubnets.primaryBackendIpAddress +allowedIpAddresses.primaryBackendIpAddress +"""), + column_helper.Column( + 'host_iqn', + ('allowedHost', 'name',), + """ +allowedVirtualGuests.allowedHost.name +allowedHardware.allowedHost.name +allowedSubnets.allowedHost.name +allowedIpAddresses.allowedHost.name +"""), + column_helper.Column( + 'username', + ('allowedHost', 'credential', 'username',), + """ +allowedVirtualGuests.allowedHost.credential.username +allowedHardware.allowedHost.credential.username +allowedSubnets.allowedHost.credential.username +allowedIpAddresses.allowedHost.credential.username +"""), + column_helper.Column( + 'password', + ('allowedHost', 'credential', 'password',), + """ +allowedVirtualGuests.allowedHost.credential.password +allowedHardware.allowedHost.credential.password +allowedSubnets.allowedHost.credential.password +allowedIpAddresses.allowedHost.credential.password +"""), +] + + +DEFAULT_COLUMNS = [ + 'id', + 'name', + 'type', + 'private_ip_address', + 'host_iqn', + 'username', + 'password', +] + + +@click.command() +@click.argument('volume_id') +@click.option('--sortby', help='Column to sort by', default='name') +@click.option('--columns', + callback=column_helper.get_formatter(COLUMNS), + help='Columns to display. Options: {0}'.format( + ', '.join(column.name for column in COLUMNS)), + default=','.join(DEFAULT_COLUMNS)) +@environment.pass_env +def cli(env, columns, sortby, volume_id): + """List ACLs.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + access_list = block_manager.get_block_volume_access_list( + volume_id=volume_id) + table = formatting.Table(columns.columns) + table.sortby = sortby + + for key, type_name in [('allowedVirtualGuests', 'VIRTUAL'), + ('allowedHardware', 'HARDWARE'), + ('allowedSubnets', 'SUBNET'), + ('allowedIpAddresses', 'IP')]: + for obj in access_list.get(key, []): + obj['type'] = type_name + table.add_row([value or formatting.blank() + for value in columns.row(obj)]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/cancel.py b/SoftLayer/CLI/block/cancel.py new file mode 100644 index 000000000..a23cee003 --- /dev/null +++ b/SoftLayer/CLI/block/cancel.py @@ -0,0 +1,28 @@ +"""Cancel an existing iSCSI account.""" +# :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 + + +@click.command() +@click.argument('volume-id') +@click.option('--reason', help="An optional reason for cancellation") +@click.option('--immediate', + is_flag=True, + help="Cancels the block storage volume immediately instead " + "of on the billing anniversary") +@environment.pass_env +def cli(env, volume_id, reason, immediate): + """Cancel an existing block storage volume.""" + + block_storage_manager = SoftLayer.BlockStorageManager(env.client) + + if not (env.skip_confirmations or formatting.no_going_back(volume_id)): + raise exceptions.CLIAbort('Aborted') + + block_storage_manager.cancel_block_volume(volume_id, reason, immediate) diff --git a/SoftLayer/CLI/block/detail.py b/SoftLayer/CLI/block/detail.py new file mode 100644 index 000000000..c98736ed9 --- /dev/null +++ b/SoftLayer/CLI/block/detail.py @@ -0,0 +1,59 @@ +"""Display details for a specified volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + + +@click.command() +@click.argument('volume_id') +@environment.pass_env +def cli(env, volume_id): + """Display details for a specified volume.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + block_volume = block_manager.get_block_volume_details(volume_id) + block_volume = utils.NestedDict(block_volume) + + table = formatting.KeyValueTable(['Name', 'Value']) + table.align['Name'] = 'r' + table.align['Value'] = 'l' + + storage_type = block_volume['storageType']['keyName'].split('_').pop(0) + table.add_row(['ID', block_volume['id']]) + table.add_row(['Username', block_volume['username']]) + table.add_row(['Type', storage_type]) + table.add_row(['Capacity (GB)', "%iGB" % block_volume['capacityGb']]) + table.add_row(['LUN Id', "%s" % block_volume['lunId']]) + + if block_volume.get('iops'): + table.add_row(['IOPs', block_volume['iops']]) + + if block_volume.get('storageTierLevel'): + table.add_row([ + 'Endurance Tier', + block_volume['storageTierLevel']['description'], + ]) + + table.add_row([ + 'Data Center', + block_volume['serviceResource']['datacenter']['name'], + ]) + table.add_row([ + 'Target IP', + block_volume['serviceResourceBackendIpAddress'], + ]) + + if block_volume['snapshotCapacityGb']: + table.add_row([ + 'Snapshot Capacity (GB)', + block_volume['snapshotCapacityGb'], + ]) + table.add_row([ + 'Snapshot Used (Bytes)', + block_volume['parentVolume']['snapshotSizeBytes'], + ]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/list.py b/SoftLayer/CLI/block/list.py new file mode 100644 index 000000000..40edcb7c8 --- /dev/null +++ b/SoftLayer/CLI/block/list.py @@ -0,0 +1,66 @@ +"""List block storage volumes.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + + +COLUMNS = [ + column_helper.Column('id', ('id',), mask="id"), + column_helper.Column('username', ('username',), mask="username"), + column_helper.Column('datacenter', + ('serviceResource', 'datacenter', 'name'), + mask="serviceResource.datacenter.name"), + column_helper.Column( + 'storage_type', + lambda b: b['storageType']['keyName'].split('_').pop(0), + mask="storageType.keyName"), + column_helper.Column('capacity_gb', ('capacityGb',), mask="capacityGb"), + column_helper.Column('bytes_used', ('bytesUsed',), mask="bytesUsed"), + column_helper.Column('ip_addr', ('serviceResourceBackendIpAddress',), + mask="serviceResourceBackendIpAddress"), +] + +DEFAULT_COLUMNS = [ + 'id', + 'username', + 'datacenter', + 'storage_type', + 'capacity_gb', + 'bytes_used', + 'ip_addr' +] + + +@click.command() +@click.option('--username', '-u', help='Volume username') +@click.option('--datacenter', '-d', help='Datacenter shortname') +@click.option('--storage-type', + help='Type of storage volume', + type=click.Choice(['performance', 'endurance'])) +@click.option('--sortby', help='Column to sort by', default='username') +@click.option('--columns', + callback=column_helper.get_formatter(COLUMNS), + help='Columns to display. Options: {0}'.format( + ', '.join(column.name for column in COLUMNS)), + default=','.join(DEFAULT_COLUMNS)) +@environment.pass_env +def cli(env, sortby, columns, datacenter, username, storage_type): + """List block storage.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + block_volumes = block_manager.list_block_volumes(datacenter=datacenter, + username=username, + storage_type=storage_type, + mask=columns.mask()) + + table = formatting.Table(columns.columns) + table.sortby = sortby + + for block_volume in block_volumes: + table.add_row([value or formatting.blank() + for value in columns.row(block_volume)]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/order.py b/SoftLayer/CLI/block/order.py new file mode 100644 index 000000000..f1840078c --- /dev/null +++ b/SoftLayer/CLI/block/order.py @@ -0,0 +1,99 @@ +"""Order a block storage volume.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions + + +CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.upper()) + + +@click.command(context_settings=CONTEXT_SETTINGS) +@click.option('--storage-type', + help='Type of storage volume', + type=click.Choice(['performance', 'endurance']), + required=True) +@click.option('--size', + type=int, + help='Size of storage volume in GB', + required=True) +@click.option('--iops', + type=int, + help='Performance Storage IOPs,' + ' between 100 and 6000 in multiples of 100' + ' [required for storage-type performance]') +@click.option('--tier', + help='Endurance Storage Tier (IOP per GB)' + ' [required for storage-type endurance]', + type=click.Choice(['0.25', '2', '4'])) +@click.option('--os-type', + help='Operating System', + type=click.Choice([ + 'HYPER_V', + 'LINUX', + 'VMWARE', + 'WINDOWS_2008', + 'WINDOWS_GPT', + 'WINDOWS', + 'XEN']), + required=True) +@click.option('--location', + help='Datacenter short name (e.g.: dal09)', + required=True) +@environment.pass_env +def cli(env, storage_type, size, iops, tier, os_type, location): + """Order a block storage volume.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + storage_type = storage_type.lower() + + if storage_type == 'performance': + if iops is None: + raise exceptions.CLIAbort( + 'Option --iops required with Performance') + + if iops < 100 or iops > 6000: + raise exceptions.CLIAbort( + 'Option --iops must be between 100 and 6000, inclusive') + + if iops % 100 != 0: + raise exceptions.CLIAbort( + 'Option --iops must be a multiple of 100' + ) + + try: + order = block_manager.order_block_volume( + storage_type='performance_storage_iscsi', + location=location, + size=size, + iops=iops, + os_type=os_type + ) + except ValueError as ex: + raise exceptions.ArgumentError(str(ex)) + + if storage_type == 'endurance': + if tier is None: + raise exceptions.CLIAbort( + 'Option --tier required with Endurance in IOPS/GB [0.25,2,4]') + + try: + order = block_manager.order_block_volume( + storage_type='storage_service_enterprise', + location=location, + size=size, + tier_level=float(tier), + os_type=os_type + ) + except ValueError as ex: + raise exceptions.ArgumentError(str(ex)) + + if 'placedOrder' in order.keys(): + click.echo("Order #{0} placed successfully!".format( + order['placedOrder']['id'])) + for item in order['placedOrder']['items']: + click.echo(" > %s" % item['description']) + else: + click.echo("Order could not be placed! Please verify your options " + + "and try again.") diff --git a/SoftLayer/CLI/block/snapshot_delete.py b/SoftLayer/CLI/block/snapshot_delete.py new file mode 100644 index 000000000..6daa3f4d6 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot_delete.py @@ -0,0 +1,15 @@ +"""Create a block storage snapshot.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import environment + + +@click.command() +@click.argument('snapshot_id') +@environment.pass_env +def cli(env, snapshot_id): + """Deletes a snapshot on a given volume""" + block_manager = SoftLayer.BlockStorageManager(env.client) + block_manager.delete_snapshot(snapshot_id) diff --git a/SoftLayer/CLI/block/snapshot_list.py b/SoftLayer/CLI/block/snapshot_list.py new file mode 100644 index 000000000..7ffea3f56 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot_list.py @@ -0,0 +1,58 @@ +"""List block storage snapshots.""" +# :license: MIT, see LICENSE for more details. + +import click +import SoftLayer +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + + +COLUMNS = [ + column_helper.Column( + 'id', + ('snapshots', 'id',), + mask='snapshots.id'), + column_helper.Column('name', ('snapshots', 'notes',), + mask='snapshots.notes'), + column_helper.Column('created', + ('snapshots', 'snapshotCreationTimestamp',), + mask='snapshots.snapshotCreationTimestamp'), + column_helper.Column('size_bytes', ('snapshots', 'snapshotSizeBytes',), + mask='snapshots.snapshotSizeBytes'), +] + +DEFAULT_COLUMNS = [ + 'id', + 'name', + 'created', + 'size_bytes' +] + + +@click.command() +@click.argument('volume_id') +@click.option('--sortby', help='Column to sort by', + default='created') +@click.option('--columns', + callback=column_helper.get_formatter(COLUMNS), + help='Columns to display. Options: {0}'.format( + ', '.join(column.name for column in COLUMNS)), + default=','.join(DEFAULT_COLUMNS)) +@environment.pass_env +def cli(env, sortby, columns, volume_id): + """List block storage snapshots.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + snapshots = block_manager.get_block_volume_snapshot_list( + volume_id=volume_id, + mask=columns.mask(), + ) + + table = formatting.Table(columns.columns) + table.sortby = sortby + + for snapshot in snapshots: + table.add_row([value or formatting.blank() + for value in columns.row(snapshot)]) + + env.fout(table) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 0d93f2ec7..a9aec3c95 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -56,6 +56,15 @@ ('dns:zone-list', 'SoftLayer.CLI.dns.zone_list:cli'), ('dns:zone-print', 'SoftLayer.CLI.dns.zone_print:cli'), + ('block', 'SoftLayer.CLI.block'), + ('block:volume-list', 'SoftLayer.CLI.block.list:cli'), + ('block:volume-detail', 'SoftLayer.CLI.block.detail:cli'), + ('block:volume-cancel', 'SoftLayer.CLI.block.cancel:cli'), + ('block:volume-order', 'SoftLayer.CLI.block.order:cli'), + ('block:snapshot-list', 'SoftLayer.CLI.block.snapshot_list:cli'), + ('block:snapshot-delete', 'SoftLayer.CLI.block.snapshot_delete:cli'), + ('block:access-list', 'SoftLayer.CLI.block.access_list:cli'), + ('firewall', 'SoftLayer.CLI.firewall'), ('firewall:add', 'SoftLayer.CLI.firewall.add:cli'), ('firewall:cancel', 'SoftLayer.CLI.firewall.cancel:cli'), @@ -188,6 +197,8 @@ ('ticket:update', 'SoftLayer.CLI.ticket.update:cli'), ('ticket:subjects', 'SoftLayer.CLI.ticket.subjects:cli'), ('ticket:summary', 'SoftLayer.CLI.ticket.summary:cli'), + ('ticket:attach', 'SoftLayer.CLI.ticket.attach:cli'), + ('ticket:detach', 'SoftLayer.CLI.ticket.detach:cli'), ('vlan', 'SoftLayer.CLI.vlan'), ('vlan:detail', 'SoftLayer.CLI.vlan.detail:cli'), diff --git a/SoftLayer/CLI/ticket/attach.py b/SoftLayer/CLI/ticket/attach.py new file mode 100644 index 000000000..98adaa65b --- /dev/null +++ b/SoftLayer/CLI/ticket/attach.py @@ -0,0 +1,42 @@ +"""Attach devices to a ticket.""" +# :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', type=int) +@click.option('--hardware', + 'hardware_identifier', + help="The identifier for hardware to attach") +@click.option('--virtual', + 'virtual_identifier', + help="The identifier for a virtual server to attach") +@environment.pass_env +def cli(env, identifier, hardware_identifier, virtual_identifier): + """Attach devices to a ticket.""" + ticket_mgr = SoftLayer.TicketManager(env.client) + + if hardware_identifier and virtual_identifier: + raise exceptions.ArgumentError( + "Cannot attach hardware and a virtual server at the same time") + elif hardware_identifier: + hardware_mgr = SoftLayer.HardwareManager(env.client) + hardware_id = helpers.resolve_id(hardware_mgr.resolve_ids, + hardware_identifier, + 'hardware') + ticket_mgr.attach_hardware(identifier, hardware_id) + elif virtual_identifier: + vs_mgr = SoftLayer.VSManager(env.client) + vs_id = helpers.resolve_id(vs_mgr.resolve_ids, + virtual_identifier, + 'VS') + ticket_mgr.attach_virtual_server(identifier, vs_id) + else: + raise exceptions.ArgumentError( + "Must have a hardware or virtual server identifier to attach") diff --git a/SoftLayer/CLI/ticket/create.py b/SoftLayer/CLI/ticket/create.py index 82f81ee83..71ebd26e5 100644 --- a/SoftLayer/CLI/ticket/create.py +++ b/SoftLayer/CLI/ticket/create.py @@ -5,25 +5,49 @@ import SoftLayer from SoftLayer.CLI import environment +from SoftLayer.CLI import helpers from SoftLayer.CLI import ticket @click.command() @click.option('--title', required=True, help="The title of the ticket") @click.option('--subject-id', + type=int, required=True, help="""The subject id to use for the ticket, issue 'slcli ticket subjects' to get the list""") @click.option('--body', help="The ticket body") +@click.option('--hardware', + 'hardware_identifier', + help="The identifier for hardware to attach") +@click.option('--virtual', + 'virtual_identifier', + help="The identifier for a virtual server to attach") @environment.pass_env -def cli(env, title, subject_id, body): +def cli(env, title, subject_id, body, hardware_identifier, virtual_identifier): """Create a support ticket.""" - mgr = SoftLayer.TicketManager(env.client) + ticket_mgr = SoftLayer.TicketManager(env.client) if body is None: body = click.edit('\n\n' + ticket.TEMPLATE_MSG) - created_ticket = mgr.create_ticket(title=title, - body=body, - subject=subject_id) - env.fout(ticket.get_ticket_results(mgr, created_ticket['id'])) + created_ticket = ticket_mgr.create_ticket( + title=title, + body=body, + subject=subject_id) + + if hardware_identifier: + hardware_mgr = SoftLayer.HardwareManager(env.client) + hardware_id = helpers.resolve_id(hardware_mgr.resolve_ids, + hardware_identifier, + 'hardware') + ticket_mgr.attach_hardware(created_ticket['id'], hardware_id) + + if virtual_identifier: + vs_mgr = SoftLayer.VSManager(env.client) + vs_id = helpers.resolve_id(vs_mgr.resolve_ids, + virtual_identifier, + 'VS') + ticket_mgr.attach_virtual_server(created_ticket['id'], vs_id) + + env.fout(ticket.get_ticket_results(ticket_mgr, created_ticket['id'])) diff --git a/SoftLayer/CLI/ticket/detach.py b/SoftLayer/CLI/ticket/detach.py new file mode 100644 index 000000000..8c8cae058 --- /dev/null +++ b/SoftLayer/CLI/ticket/detach.py @@ -0,0 +1,42 @@ +"""Detach devices from a ticket.""" +# :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', type=int) +@click.option('--hardware', + 'hardware_identifier', + help="The identifier for hardware to detach") +@click.option('--virtual', + 'virtual_identifier', + help="The identifier for a virtual server to detach") +@environment.pass_env +def cli(env, identifier, hardware_identifier, virtual_identifier): + """Detach devices from a ticket.""" + ticket_mgr = SoftLayer.TicketManager(env.client) + + if hardware_identifier and virtual_identifier: + raise exceptions.ArgumentError( + "Cannot detach hardware and a virtual server at the same time") + elif hardware_identifier: + hardware_mgr = SoftLayer.HardwareManager(env.client) + hardware_id = helpers.resolve_id(hardware_mgr.resolve_ids, + hardware_identifier, + 'hardware') + ticket_mgr.detach_hardware(identifier, hardware_id) + elif virtual_identifier: + vs_mgr = SoftLayer.VSManager(env.client) + vs_id = helpers.resolve_id(vs_mgr.resolve_ids, + virtual_identifier, + 'VS') + ticket_mgr.detach_virtual_server(identifier, vs_id) + else: + raise exceptions.ArgumentError( + "Must have a hardware or virtual server identifier to detach") diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py index 957cce83e..a4464e61a 100644 --- a/SoftLayer/CLI/virt/detail.py +++ b/SoftLayer/CLI/virt/detail.py @@ -90,9 +90,19 @@ def cli(env, identifier, passwords=False, price=False): result['billingItem']['recurringFee']]) if passwords: - pass_table = formatting.Table(['username', 'password']) - for item in result['operatingSystem']['passwords']: - pass_table.add_row([item['username'], item['password']]) + pass_table = formatting.Table(['software', 'username', 'password']) + + for component in result['softwareComponents']: + for item in component['passwords']: + pass_table.add_row([ + utils.lookup(component, + 'softwareLicense', + 'softwareDescription', + 'name'), + item['username'], + item['password'], + ]) + table.add_row(['users', pass_table]) table.add_row(['tags', formatting.tags(result['tagReferences'])]) diff --git a/SoftLayer/CLI/vlan/detail.py b/SoftLayer/CLI/vlan/detail.py index f97928a28..59a086558 100644 --- a/SoftLayer/CLI/vlan/detail.py +++ b/SoftLayer/CLI/vlan/detail.py @@ -39,7 +39,7 @@ def cli(env, identifier, no_vs, no_hardware): table.add_row(['firewall', 'Yes' if vlan['firewallInterfaces'] else 'No']) subnets = [] - for subnet in vlan['subnets']: + for subnet in vlan.get('subnets', []): subnet_table = formatting.KeyValueTable(['name', 'value']) subnet_table.align['name'] = 'r' subnet_table.align['value'] = 'l' @@ -57,7 +57,7 @@ def cli(env, identifier, no_vs, no_hardware): server_columns = ['hostname', 'domain', 'public_ip', 'private_ip'] if not no_vs: - if vlan['virtualGuests']: + if vlan.get('virtualGuests'): vs_table = formatting.KeyValueTable(server_columns) for vsi in vlan['virtualGuests']: vs_table.add_row([vsi['hostname'], @@ -69,7 +69,7 @@ def cli(env, identifier, no_vs, no_hardware): table.add_row(['vs', 'none']) if not no_hardware: - if vlan['hardware']: + if vlan.get('hardware'): hw_table = formatting.Table(server_columns) for hardware in vlan['hardware']: hw_table.add_row([hardware['hostname'], diff --git a/SoftLayer/consts.py b/SoftLayer/consts.py index abcd90c7e..184730e17 100644 --- a/SoftLayer/consts.py +++ b/SoftLayer/consts.py @@ -5,7 +5,7 @@ :license: MIT, see LICENSE for more details. """ -VERSION = 'v5.0.1' +VERSION = 'v5.1.0' API_PUBLIC_ENDPOINT = 'https://api.softlayer.com/xmlrpc/v3.1/' API_PRIVATE_ENDPOINT = 'https://api.service.softlayer.com/xmlrpc/v3.1/' API_PUBLIC_ENDPOINT_REST = 'https://api.softlayer.com/rest/v3.1/' diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index a1fde3c6a..b03c26d00 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -509,3 +509,23 @@ getHubNetworkStorage = [{'id': 12345, 'username': 'SLOS12345-1'}, {'id': 12346, 'username': 'SLOS12345-2'}] + +getIscsiNetworkStorage = [{ + 'accountId': 1234, + 'billingItem': {'id': 449}, + 'capacityGb': 20, + 'createDate': '2015:50:15-04:00', + 'guestId': '', + 'hardwareId': '', + 'hostId': '', + 'id': 100, + 'nasType': 'ISCSI', + 'notes': """{'status': 'available'}""", + 'password': '', + 'serviceProviderId': 1, + 'serviceResource': {'datacenter': {'id': 449500}}, + 'serviceResourceBackendIpAddress': '10.1.2.3', + 'serviceResourceName': 'Storage Type 01 Aggregate staaspar0101_pc01', + 'username': 'username', + 'storageType': {'keyName': 'ENDURANCE_STORAGE'}, +}] diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage.py b/SoftLayer/fixtures/SoftLayer_Network_Storage.py new file mode 100644 index 000000000..0257b1678 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Network_Storage.py @@ -0,0 +1,65 @@ +getObject = { + 'accountId': 1234, + 'billingItem': {'id': 449}, + 'capacityGb': 20, + 'createDate': '2015:50:15-04:00', + 'guestId': '', + 'hardwareId': '', + 'hostId': '', + 'id': 100, + 'nasType': 'ISCSI', + 'notes': """{'status': 'available'}""", + 'password': '', + 'serviceProviderId': 1, + 'iops': 1000, + 'storageTierLevel': {'description': 'Tier 1'}, + 'snapshotCapacityGb': 10, + 'parentVolume': {'snapshotSizeBytes': 1024}, + 'serviceResource': {'datacenter': {'id': 449500, 'name': 'dal05'}}, + 'serviceResourceBackendIpAddress': '10.1.2.3', + 'serviceResourceName': 'Storage Type 01 Aggregate staaspar0101_pc01', + 'username': 'username', + 'storageType': {'keyName': 'ENDURANCE_STORAGE'}, + 'allowedVirtualGuests': [{ + 'id': 1234, + 'hostname': 'test-server', + 'domain': 'example.com', + 'primaryBackendIpAddress': '10.0.0.1', + 'allowedHost': { + 'name': 'test-server', + 'credential': {'username': 'joe', 'password': '12345'}, + }, + }], + 'lunId': 2, + 'allowedHardware': [{ + 'id': 1234, + 'hostname': 'test-server', + 'domain': 'example.com', + 'primaryBackendIpAddress': '10.0.0.2', + 'allowedHost': { + 'name': 'test-server', + 'credential': {'username': 'joe', 'password': '12345'}, + }, + }], + 'allowedSubnets': [{ + 'id': 1234, + 'networkIdentifier': '10.0.0.1', + 'cidr': '24', + 'note': 'backend subnet', + 'allowedHost': { + 'name': 'test-server', + 'credential': {'username': 'joe', 'password': '12345'}, + }, + }], + 'allowedIpAddresses': [{ + 'id': 1234, + 'ipAddress': '10.0.0.1', + 'note': 'backend ip', + 'allowedHost': { + 'name': 'test-server', + 'credential': {'username': 'joe', 'password': '12345'}, + }, + }], +} +getSnapshots = [] +deleteObject = True diff --git a/SoftLayer/fixtures/SoftLayer_Ticket.py b/SoftLayer/fixtures/SoftLayer_Ticket.py index 4933cc62e..c85deda85 100644 --- a/SoftLayer/fixtures/SoftLayer_Ticket.py +++ b/SoftLayer/fixtures/SoftLayer_Ticket.py @@ -33,3 +33,20 @@ } edit = True addUpdate = {} + +addAttachedHardware = { + "id": 123, + "createDate": "2013-08-01T14:14:04-07:00", + "hardwareId": 1, + "ticketId": 100 +} + +addAttachedVirtualGuest = { + "id": 123, + "createDate": "2013-08-01T14:14:04-07:00", + "virtualGuestId": 1, + "ticketId": 100 +} + +removeAttachedHardware = True +removeAttachedVirtualGuest = True diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py index 438009620..cb067b447 100644 --- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py +++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py @@ -40,6 +40,11 @@ 'softwareDescription': {'version': '12.04-64 Minimal for VSI', 'name': 'Ubuntu'}} }, + 'softwareComponents': [{ + 'passwords': [{'username': 'user', 'password': 'pass'}], + 'softwareLicense': { + 'softwareDescription': {'name': 'Ubuntu'}} + }], 'tagReferences': [{'tag': {'name': 'production'}}], } diff --git a/SoftLayer/managers/__init__.py b/SoftLayer/managers/__init__.py index ff8b040f5..cab39fbbd 100644 --- a/SoftLayer/managers/__init__.py +++ b/SoftLayer/managers/__init__.py @@ -7,6 +7,7 @@ :license: MIT, see LICENSE for more details. """ +from SoftLayer.managers.block import BlockStorageManager from SoftLayer.managers.cdn import CDNManager from SoftLayer.managers.dns import DNSManager from SoftLayer.managers.firewall import FirewallManager @@ -25,6 +26,7 @@ from SoftLayer.managers.vs import VSManager __all__ = [ + 'BlockStorageManager', 'CDNManager', 'DNSManager', 'FirewallManager', diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py new file mode 100644 index 000000000..906496ee2 --- /dev/null +++ b/SoftLayer/managers/block.py @@ -0,0 +1,403 @@ +""" + SoftLayer.block + ~~~~~~~~~~~~~~~ + Block Storage Manager + + :license: MIT, see LICENSE for more details. +""" +from SoftLayer import exceptions +from SoftLayer import utils + + +ENDURANCE_TIERS = { + 0.25: 100, + 2: 200, + 4: 300, +} + + +class BlockStorageManager(utils.IdentifierMixin, object): + """Manages Block Storage volumes.""" + + def __init__(self, client): + self.configuration = {} + self.client = client + + def list_block_volumes(self, datacenter=None, username=None, + storage_type=None, **kwargs): + """Returns a list of block volumes. + + :param datacenter: Datacenter short name (e.g.: dal09) + :param username: Name of volume. + :param storage_type: Type of volume: Endurance or Performance + :param kwargs: + :return: Returns a list of block volumes. + """ + if 'mask' not in kwargs: + items = [ + 'id', + 'username', + 'capacityGb', + 'bytesUsed', + 'serviceResource.datacenter[name]', + 'serviceResourceBackendIpAddress' + ] + kwargs['mask'] = ','.join(items) + + _filter = utils.NestedDict(kwargs.get('filter') or {}) + + _filter['iscsiNetworkStorage']['serviceResource']['type']['type'] = \ + (utils.query_filter('!~ ISCSI')) + + _filter['iscsiNetworkStorage']['storageType']['keyName'] = ( + utils.query_filter('*BLOCK_STORAGE')) + if storage_type: + _filter['iscsiNetworkStorage']['storageType']['keyName'] = ( + utils.query_filter('%s_BLOCK_STORAGE' % storage_type.upper())) + + if datacenter: + _filter['iscsiNetworkStorage']['serviceResource']['datacenter'][ + 'name'] = (utils.query_filter(datacenter)) + + if username: + _filter['iscsiNetworkStorage']['username'] = \ + (utils.query_filter(username)) + + kwargs['filter'] = _filter.to_dict() + return self.client.call('Account', 'getIscsiNetworkStorage', **kwargs) + + def get_block_volume_details(self, volume_id, **kwargs): + """Returns details about the specified volume. + + :param volume_id: ID of volume. + :param kwargs: + :return: Returns details about the specified volume. + """ + + if 'mask' not in kwargs: + items = [ + 'id', + 'username', + 'password', + 'capacityGb', + 'snapshotCapacityGb', + 'parentVolume.snapshotSizeBytes', + 'storageType.keyName', + 'serviceResource.datacenter[name]', + 'serviceResourceBackendIpAddress', + 'storageTierLevel', + 'iops', + 'lunId', + ] + kwargs['mask'] = ','.join(items) + return self.client.call('Network_Storage', 'getObject', + id=volume_id, **kwargs) + + def get_block_volume_access_list(self, volume_id, **kwargs): + """Returns a list of authorized hosts for a specified volume. + + :param volume_id: ID of volume. + :param kwargs: + :return: Returns a list of authorized hosts for a specified volume. + """ + if 'mask' not in kwargs: + items = [ + 'id', + 'allowedVirtualGuests[allowedHost[credential]]', + 'allowedHardware[allowedHost[credential]]', + 'allowedSubnets[allowedHost[credential]]', + 'allowedIpAddresses[allowedHost[credential]]', + ] + kwargs['mask'] = ','.join(items) + return self.client.call('Network_Storage', 'getObject', + id=volume_id, **kwargs) + + def get_block_volume_snapshot_list(self, volume_id, **kwargs): + """Returns a list of snapshots for the specified volume. + + :param volume_id: ID of volume. + :param kwargs: + :return: Returns a list of snapshots for the specified volume. + """ + if 'mask' not in kwargs: + items = '''snapshots[ + id, + notes, + snapshotSizeBytes, + storageType[keyName], + snapshotCreationTimestamp + hourlySchedule, + dailySchedule, + weeklySchedule +]''' + kwargs['mask'] = ','.join(items) + + return self.client.call('Network_Storage', 'getSnapshots', + id=volume_id, **kwargs) + + def delete_snapshot(self, snapshot_id): + """Deletes the specified snapshot object. + + :param snapshot_id: The ID of the snapshot object to delete. + """ + return self.client.call('Network_Storage', 'deleteObject', + id=snapshot_id) + + def order_block_volume(self, storage_type, location, size, os_type, + iops=None, tier_level=None): + """Places an order for a block volume. + + :param storage_type: "performance_storage_iscsi" (performance) + or "storage_service_enterprise" (endurance) + :param location: Datacenter in which to order iSCSI volume + :param size: Size of the desired volume, in GB + :param os_type: OS Type to use for volume alignment, see help for list + :param iops: Number of IOPs for a "Performance" order + :param tier_level: Tier level to use for an "Endurance" order + """ + + try: + location_id = self._get_location_id(location) + except ValueError: + raise exceptions.SoftLayerError( + "Invalid datacenter name specified. " + "Please provide the lower case short name (e.g.: dal09)") + + base_type_name = 'SoftLayer_Container_Product_Order_Network_' + package = self._get_package(storage_type) + if storage_type == 'performance_storage_iscsi': + complex_type = base_type_name + 'PerformanceStorage_Iscsi' + prices = [ + _find_performance_block_price(package), + _find_performance_space_price(package, iops), + _find_performance_iops_price(package, size, iops), + ] + elif storage_type == 'storage_service_enterprise': + complex_type = base_type_name + 'Storage_Enterprise' + prices = [ + _find_endurance_block_price(package), + _find_endurance_price(package), + _find_endurance_space_price(package, size, tier_level), + _find_endurance_tier_price(package, tier_level), + ] + else: + raise exceptions.SoftLayerError( + "storage_type must be either Performance or Endurance") + + order = { + 'complexType': complex_type, + 'packageId': package['id'], + 'osFormatType': {'keyName': os_type}, + 'prices': prices, + 'quantity': 1, + 'location': location_id, + } + + return self.client.call('Product_Order', 'placeOrder', order) + + def _get_package(self, category_code): + """Returns a product packaged based on type of storage. + + :param category_code: Category code of product package. + :return: Returns a packaged based on type of storage. + """ + + _filter = utils.NestedDict({}) + _filter['categories']['categoryCode'] = ( + utils.query_filter(category_code)) + _filter['statusCode'] = (utils.query_filter('ACTIVE')) + + packages = self.client.call('Product_Package', 'getAllObjects', + filter=_filter.to_dict(), + mask=""" +id, +name, +items[prices[categories],attributes] +""") + if len(packages) == 0: + raise ValueError('No packages were found for %s' % category_code) + if len(packages) > 1: + raise ValueError('More than one package was found for %s' + % category_code) + + return packages[0] + + def _get_location_id(self, location): + """Returns location id + + :param location: Datacenter short name + :return: Returns location id + """ + loc_svc = self.client['Location_Datacenter'] + datacenters = loc_svc.getDatacenters(mask='mask[longName,id,name]') + for datacenter in datacenters: + if datacenter['name'] == location: + location = datacenter['id'] + return location + raise ValueError('Invalid datacenter name specified.') + + def cancel_block_volume(self, volume_id, + reason='No longer needed', + immediate=False): + """Cancels the given block storage volume. + + :param integer volume_id: the volume ID + :param string reason: The reason for cancellation + :param boolean immediate_flag: Cancel immediately or + on anniversary date + """ + block_volume = self.get_block_volume_details( + volume_id, + mask='mask[id,billingItem[id]]') + billing_item_id = block_volume['billingItem']['id'] + + self.client['Billing_Item'].cancelItem( + immediate, + True, + reason, + id=billing_item_id) + + +def _find_endurance_block_price(package): + for item in package['items']: + for price in item['prices']: + if price['locationGroupId'] != '': + continue + + if not _has_category(price['categories'], 'storage_block'): + continue + + return price + + raise ValueError("Could not find price for block storage") + + +def _find_endurance_price(package): + for item in package['items']: + for price in item['prices']: + if price['locationGroupId'] != '': + continue + + if not _has_category(price['categories'], + 'storage_service_enterprise'): + continue + + return price + + raise ValueError("Could not find price for endurance block storage") + + +def _find_endurance_space_price(package, size, tier_level): + for item in package['items']: + if int(item['capacity']) != size: + continue + + for price in item['prices']: + # Only collect prices from valid location groups. + if price['locationGroupId'] != '': + continue + + if not _has_category(price['categories'], + 'performance_storage_space'): + continue + + level = ENDURANCE_TIERS.get(tier_level) + if level < int(price['capacityRestrictionMinimum']): + continue + + if level > int(price['capacityRestrictionMaximum']): + continue + + return price + + raise ValueError("Could not find price for disk space") + + +def _find_endurance_tier_price(package, tier_level): + for item in package['items']: + for attribute in item.get('attributes', []): + if int(attribute['value']) == ENDURANCE_TIERS.get(tier_level): + break + else: + continue + + for price in item['prices']: + # Only collect prices from valid location groups. + if price['locationGroupId'] != '': + continue + + if not _has_category(price['categories'], 'storage_tier_level'): + continue + + return price + + raise ValueError("Could not find price for tier") + + +def _find_performance_block_price(package): + for item in package['items']: + for price in item['prices']: + # Only collect prices from valid location groups. + if price['locationGroupId'] != '': + continue + + if not _has_category(price['categories'], + 'performance_storage_iscsi'): + continue + + return price + + raise ValueError("Could not find price for performance storage") + + +def _find_performance_space_price(package, size): + for item in package['items']: + if int(item['capacity']) != size: + continue + + for price in item['prices']: + # Only collect prices from valid location groups. + if price['locationGroupId'] != '': + continue + + if not _has_category(price['categories'], + 'performance_storage_space'): + continue + + return price + + raise ValueError("Could not find price for disk space") + + +def _find_performance_iops_price(package, size, iops): + for item in package['items']: + if int(item['capacity']) != int(iops): + continue + + for price in item['prices']: + # Only collect prices from valid location groups. + if price['locationGroupId'] != '': + continue + + if not _has_category(price['categories'], + 'performance_storage_iops'): + continue + + if size < int(price['capacityRestrictionMinimum']): + continue + + if size > int(price['capacityRestrictionMaximum']): + continue + + return price + + raise ValueError("Could not find price for iops") + + +def _has_category(categories, category_code): + return any( + True + for category + in categories + if category['categoryCode'] == category_code + ) diff --git a/SoftLayer/managers/network.py b/SoftLayer/managers/network.py index 0b4fc3816..50f1121bb 100644 --- a/SoftLayer/managers/network.py +++ b/SoftLayer/managers/network.py @@ -23,6 +23,15 @@ 'virtualGuestCount', 'networkSpace', ]) +DEFAULT_GET_VLAN_MASK = ','.join([ + 'firewallInterfaces', + 'primaryRouter[id, fullyQualifiedDomainName, datacenter]', + 'totalPrimaryIpAddressCount', + 'networkSpace', + 'hardware', + 'subnets', + 'virtualGuests', +]) class NetworkManager(object): @@ -207,7 +216,7 @@ def get_vlan(self, vlan_id): the specified VLAN. """ - return self.vlan.getObject(id=vlan_id, mask=DEFAULT_VLAN_MASK) + return self.vlan.getObject(id=vlan_id, mask=DEFAULT_GET_VLAN_MASK) def list_global_ips(self, version=None, identifier=None, **kwargs): """Returns a list of all global IP address records on the account. diff --git a/SoftLayer/managers/ticket.py b/SoftLayer/managers/ticket.py index dd1be176c..eb5a12a67 100644 --- a/SoftLayer/managers/ticket.py +++ b/SoftLayer/managers/ticket.py @@ -39,13 +39,13 @@ def list_tickets(self, open_status=True, closed_status=True): return self.client.call('Account', call, mask=mask) def list_subjects(self): - """List all tickets.""" + """List all ticket subjects.""" return self.client['Ticket_Subject'].getAllObjects() def get_ticket(self, ticket_id): """Get details about a ticket. - :param integer id: the ticket ID + :param integer ticket_id: the ticket ID :returns: A dictionary containing a large amount of information about the specified ticket. @@ -79,3 +79,39 @@ def update_ticket(self, ticket_id=None, body=None): :param string body: entry to update in the ticket """ return self.ticket.addUpdate({'entry': body}, id=ticket_id) + + def attach_hardware(self, ticket_id=None, hardware_id=None): + """Attach hardware to a ticket. + + :param integer ticket_id: the id of the ticket to attach to + :param integer hardware_id: the id of the hardware to attach + :returns The new ticket attachment + """ + return self.ticket.addAttachedHardware(hardware_id, id=ticket_id) + + def attach_virtual_server(self, ticket_id=None, virtual_id=None): + """Attach a virtual server to a ticket. + + :param integer ticket_id: the id of the ticket to attach to + :param integer virtual_id: the id of the virtual server to attach + :returns The new ticket attachment + """ + return self.ticket.addAttachedVirtualGuest(virtual_id, id=ticket_id) + + def detach_hardware(self, ticket_id=None, hardware_id=None): + """Detach hardware from a ticket. + + :param ticket_id: the id of the ticket to detach from + :param hardware_id: the id of the hardware to detach + :return: Whether the detachment was successful + """ + return self.ticket.removeAttachedHardware(hardware_id, id=ticket_id) + + def detach_virtual_server(self, ticket_id=None, virtual_id=None): + """Detach a virtual server from a ticket. + + :param ticket_id: the id of the ticket to detach from + :param virtual_id: the id of the virtual server to detach + :return: Whether the detachment was successful + """ + return self.ticket.removeAttachedVirtualGuest(virtual_id, id=ticket_id) diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py index 831eca714..25090e078 100644 --- a/SoftLayer/managers/vs.py +++ b/SoftLayer/managers/vs.py @@ -200,6 +200,9 @@ def get_instance(self, instance_id, **kwargs): 'blockDevices', 'blockDeviceTemplateGroup[id, name, globalIdentifier]', 'postInstallScriptUri', + '''softwareComponents[ + passwords[username,password,notes], + softwareLicense[softwareDescription[name]]]''', '''operatingSystem[passwords[username,password], softwareLicense.softwareDescription[ manufacturer,name,version, diff --git a/SoftLayer/testing/__init__.py b/SoftLayer/testing/__init__.py index 1bd6c47ff..279d19488 100644 --- a/SoftLayer/testing/__init__.py +++ b/SoftLayer/testing/__init__.py @@ -146,6 +146,13 @@ def assert_called_with(self, service, method, **props): raise AssertionError('%s::%s was not called with given properties: %s' % (service, method, props)) + def assert_no_fail(self, result): + """Fail when a failing click result has an error""" + if result.exception: + raise result.exception + + self.assertEqual(result.exit_code, 0) + def set_mock(self, service, method): """Set and return mock on the current client.""" return self.mocks.set_mock(service, method) diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 035461784..f39ffc0fe 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -59,13 +59,8 @@ def to_dict(self): This is needed for places where strict type checking is done. """ - new_dict = {} - for key, val in self.items(): - if isinstance(val, NestedDict): - new_dict[key] = val.to_dict() - else: - new_dict[key] = val - return new_dict + return {key: val.to_dict() if isinstance(val, NestedDict) else val + for key, val in self.items()} def query_filter(query): diff --git a/docs/conf.py b/docs/conf.py index cb63c4dad..74ed98e0b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,9 @@ # built documents. # # The short X.Y version. -version = '5.0.1' +version = '5.1.0' # The full version, including alpha/beta/rc tags. -release = '5.0.1' +release = '5.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/fabfile.py b/fabfile.py index 9badba0aa..c864c537e 100644 --- a/fabfile.py +++ b/fabfile.py @@ -36,14 +36,14 @@ def release(version, force=False): clean() - puts(" * Tagging Version %s" % version_str) - force_option = 'f' if force else '' - local("git tag -%sam \"%s\" %s" % (force_option, version_str, version_str)) - local("pip install wheel") puts(" * Uploading to PyPI") upload() + puts(" * Tagging Version %s" % version_str) + force_option = 'f' if force else '' + local("git tag -%sam \"%s\" %s" % (force_option, version_str, version_str)) + puts(" * Pushing Tag to upstream") local("git push upstream %s" % version_str) diff --git a/setup.py b/setup.py index cce102aab..72e36e4cd 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name='SoftLayer', - version='5.0.1', + version='5.1.0', description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='SoftLayer Technologies, Inc.', diff --git a/tests/CLI/core_tests.py b/tests/CLI/core_tests.py index 8acab2120..534fcccca 100644 --- a/tests/CLI/core_tests.py +++ b/tests/CLI/core_tests.py @@ -31,7 +31,7 @@ def test_verbose_max(self): with mock.patch('logging.getLogger') as log_mock: result = self.run_command(['-vvv', 'vs', 'list']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) log_mock().addHandler.assert_called_with(mock.ANY) log_mock().setLevel.assert_called_with(logging.DEBUG) @@ -39,13 +39,13 @@ def test_build_client(self): env = environment.Environment() result = self.run_command(['vs', 'list'], env=env) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertIsNotNone(env.client) def test_diagnostics(self): result = self.run_command(['-v', 'vs', 'list']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertIn('SoftLayer_Account::getVirtualGuests', result.output) self.assertIn('"execution_time"', result.output) self.assertIn('"api_calls"', result.output) diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py new file mode 100644 index 000000000..98f13f445 --- /dev/null +++ b/tests/CLI/modules/block_tests.py @@ -0,0 +1,98 @@ +""" + SoftLayer.tests.CLI.modules.block_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" +from SoftLayer import testing + +import json + + +class BlockTests(testing.TestCase): + + def test_access_list(self): + result = self.run_command(['block', 'access-list', '1234']) + + self.assert_no_fail(result) + self.assertEqual([ + { + 'username': 'joe', + 'name': 'test-server.example.com', + 'type': 'VIRTUAL', + 'host_iqn': 'test-server', + 'password': '12345', + 'private_ip_address': '10.0.0.1', + 'id': 1234, + }, + { + 'username': 'joe', + 'name': 'test-server.example.com', + 'type': 'HARDWARE', + 'host_iqn': 'test-server', + 'password': '12345', + 'private_ip_address': '10.0.0.2', + 'id': 1234, + }, + { + 'username': 'joe', + 'name': '10.0.0.1/24 (backend subnet)', + 'type': 'SUBNET', + 'host_iqn': 'test-server', + 'password': '12345', + 'private_ip_address': None, + 'id': 1234, + }, + { + 'username': 'joe', + 'name': '10.0.0.1 (backend ip)', + 'type': 'IP', + 'host_iqn': 'test-server', + 'password': '12345', + 'private_ip_address': None, + 'id': 1234, + }], + json.loads(result.output),) + + def test_volume_cancel(self): + result = self.run_command([ + '--really', 'block', 'volume-cancel', '1234']) + + self.assert_no_fail(result) + self.assertEqual("", result.output) + self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', + args=(False, True, None)) + + def test_volume_detail(self): + result = self.run_command(['block', 'volume-detail', '1234']) + + self.assert_no_fail(result) + self.assertEqual({ + 'Username': 'username', + 'LUN Id': '2', + 'Endurance Tier': 'Tier 1', + 'IOPs': 1000, + 'Snapshot Capacity (GB)': 10, + 'Snapshot Used (Bytes)': 1024, + 'Capacity (GB)': '20GB', + 'Target IP': '10.1.2.3', + 'Data Center': 'dal05', + 'Type': 'ENDURANCE', + 'ID': 100, + }, json.loads(result.output)) + + def test_volume_list(self): + result = self.run_command(['block', 'volume-list']) + + self.assert_no_fail(result) + self.assertEqual([ + { + 'bytes_used': None, + 'capacity_gb': 20, + 'datacenter': None, + 'id': 100, + 'ip_addr': '10.1.2.3', + 'storage_type': 'ENDURANCE', + 'username': 'username' + }], + json.loads(result.output)) diff --git a/tests/CLI/modules/call_api_tests.py b/tests/CLI/modules/call_api_tests.py index 79d12e4b4..84dd69016 100644 --- a/tests/CLI/modules/call_api_tests.py +++ b/tests/CLI/modules/call_api_tests.py @@ -42,7 +42,7 @@ def test_python_output(self): '-f nested.property=5432', '--output-python']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) # NOTE(kmcdonald): Python 3 no longer inserts 'u' before unicode # string literals but python 2 does. These are stripped out to make # this test pass on both python versions. @@ -72,7 +72,7 @@ def test_options(self): '-f property=1234', '-f nested.property=5432']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), 'test') self.assert_called_with('SoftLayer_Service', 'method', mask='mask[some.mask]', @@ -94,7 +94,7 @@ def test_object(self): result = self.run_command(['call-api', 'Service', 'method']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'string': 'string', 'int': 10, @@ -113,7 +113,7 @@ def test_object_table(self): result = self.run_command(['call-api', 'Service', 'method'], fmt='table') - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) # NOTE(kmcdonald): Order is not guaranteed self.assertIn(":........:........:", result.output) self.assertIn(": name : value :", result.output) @@ -130,7 +130,7 @@ def test_object_nested(self): result = self.run_command(['call-api', 'Service', 'method']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'this': {'is': [{'pretty': 'nested'}]}}) @@ -144,7 +144,7 @@ def test_list(self): result = self.run_command(['call-api', 'Service', 'method']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'string': 'string', 'int': 10, @@ -163,7 +163,7 @@ def test_list_table(self): result = self.run_command(['call-api', 'Service', 'method'], fmt='table') - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, """:......:......:.......:.....:........: : Bool : None : float : int : string : @@ -179,6 +179,6 @@ def test_parameters(self): result = self.run_command(['call-api', 'Service', 'method', 'arg1', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Service', 'method', args=('arg1', '1234')) diff --git a/tests/CLI/modules/cdn_tests.py b/tests/CLI/modules/cdn_tests.py index 14d22128e..d15259ab5 100644 --- a/tests/CLI/modules/cdn_tests.py +++ b/tests/CLI/modules/cdn_tests.py @@ -14,7 +14,7 @@ class CdnTests(testing.TestCase): def test_list_accounts(self): result = self.run_command(['cdn', 'list']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'notes': None, 'created': '2012-06-25T14:05:28-07:00', @@ -30,7 +30,7 @@ def test_list_accounts(self): def test_detail_account(self): result = self.run_command(['cdn', 'detail', '1245']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'notes': None, 'created': '2012-06-25T14:05:28-07:00', @@ -43,20 +43,20 @@ def test_load_content(self): result = self.run_command(['cdn', 'load', '1234', 'http://example.com']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") def test_purge_content(self): result = self.run_command(['cdn', 'purge', '1234', 'http://example.com']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") def test_list_origins(self): result = self.run_command(['cdn', 'origin-list', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [ {'media_type': 'FLASH', 'origin_url': 'http://ams01.objectstorage.softlayer.net:80', @@ -71,12 +71,12 @@ def test_add_origin(self): result = self.run_command(['cdn', 'origin-add', '1234', 'http://example.com']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") def test_remove_origin(self): result = self.run_command(['cdn', 'origin-remove', '1234', 'http://example.com']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") diff --git a/tests/CLI/modules/config_tests.py b/tests/CLI/modules/config_tests.py index db0ed6b8a..a6f0feafd 100644 --- a/tests/CLI/modules/config_tests.py +++ b/tests/CLI/modules/config_tests.py @@ -31,7 +31,7 @@ def set_up(self): def test_show(self): result = self.run_command(['config', 'show']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'Username': 'username', 'API Key': 'api-key', @@ -62,7 +62,7 @@ def test_setup(self, input, getpass, confirm_mock): result = self.run_command(['--config=%s' % config_file.name, 'config', 'setup']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertTrue('Configuration Updated Successfully' in result.output) contents = config_file.read().decode("utf-8") diff --git a/tests/CLI/modules/dns_tests.py b/tests/CLI/modules/dns_tests.py index ca4617c10..d537872d7 100644 --- a/tests/CLI/modules/dns_tests.py +++ b/tests/CLI/modules/dns_tests.py @@ -19,13 +19,13 @@ class DnsTests(testing.TestCase): def test_zone_print(self): result = self.run_command(['dns', 'zone-print', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), "lots of text") def test_create_zone(self): result = self.run_command(['dns', 'zone-create', 'example.com']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") @mock.patch('SoftLayer.CLI.formatting.no_going_back') @@ -33,13 +33,13 @@ def test_delete_zone(self, no_going_back_mock): no_going_back_mock.return_value = True result = self.run_command(['dns', 'zone-delete', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") no_going_back_mock.return_value = False result = self.run_command(['--really', 'dns', 'zone-delete', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") @mock.patch('SoftLayer.CLI.formatting.no_going_back') @@ -53,7 +53,7 @@ def test_delete_zone_abort(self, no_going_back_mock): def test_list_zones(self): result = self.run_command(['dns', 'zone-list']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'serial': 2014030728, 'updated': '2014-03-07T13:52:31-06:00', @@ -63,7 +63,7 @@ def test_list_zones(self): def test_list_records(self): result = self.run_command(['dns', 'record-list', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output)[0], {'record': 'a', 'type': 'CNAME', @@ -75,7 +75,7 @@ def test_add_record(self): result = self.run_command(['dns', 'record-add', '1234', 'hostname', 'A', 'd', '--ttl=100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") @mock.patch('SoftLayer.CLI.formatting.no_going_back') @@ -83,7 +83,7 @@ def test_delete_record(self, no_going_back_mock): no_going_back_mock.return_value = True result = self.run_command(['dns', 'record-remove', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") @mock.patch('SoftLayer.CLI.formatting.no_going_back') diff --git a/tests/CLI/modules/firewall_tests.py b/tests/CLI/modules/firewall_tests.py index 91e208a60..f83022d7e 100644 --- a/tests/CLI/modules/firewall_tests.py +++ b/tests/CLI/modules/firewall_tests.py @@ -14,7 +14,7 @@ class FirewallTests(testing.TestCase): def test_list_firewalls(self): result = self.run_command(['firewall', 'list']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'type': 'VLAN - dedicated', 'server/vlan id': 1, diff --git a/tests/CLI/modules/globalip_tests.py b/tests/CLI/modules/globalip_tests.py index 140f52316..00716c563 100644 --- a/tests/CLI/modules/globalip_tests.py +++ b/tests/CLI/modules/globalip_tests.py @@ -17,7 +17,7 @@ class DnsTests(testing.TestCase): def test_ip_assign(self): result = self.run_command(['globalip', 'assign', '1', '127.0.0.1']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") @mock.patch('SoftLayer.CLI.formatting.no_going_back') @@ -25,14 +25,14 @@ def test_ip_cancel(self, no_going_back_mock): # Test using --really flag result = self.run_command(['--really', 'globalip', 'cancel', '1']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") # Test with confirmation no_going_back_mock.return_value = True result = self.run_command(['globalip', 'cancel', '1']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") # Test with confirmation and responding negatively @@ -45,7 +45,7 @@ def test_ip_cancel(self, no_going_back_mock): def test_ip_list(self): result = self.run_command(['globalip', 'list', '--ip-version=v4']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'assigned': 'Yes', 'id': '200', diff --git a/tests/CLI/modules/nas_tests.py b/tests/CLI/modules/nas_tests.py index 7457f38b3..01e0c8c8a 100644 --- a/tests/CLI/modules/nas_tests.py +++ b/tests/CLI/modules/nas_tests.py @@ -13,7 +13,7 @@ class RWhoisTests(testing.TestCase): def test_list_nas(self): result = self.run_command(['nas', 'list']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'datacenter': 'Dallas', 'server': '127.0.0.1', diff --git a/tests/CLI/modules/object_storage_tests.py b/tests/CLI/modules/object_storage_tests.py index 0cc9eeb79..0f59c847e 100644 --- a/tests/CLI/modules/object_storage_tests.py +++ b/tests/CLI/modules/object_storage_tests.py @@ -14,7 +14,7 @@ class ObjectStorageTests(testing.TestCase): def test_list_accounts(self): result = self.run_command(['object-storage', 'accounts']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'id': 12345, 'name': 'SLOS12345-1'}, {'id': 12346, 'name': 'SLOS12345-2'}]) @@ -31,7 +31,7 @@ def test_list_endpoints(self): result = self.run_command(['object-storage', 'endpoints']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'datacenter': 'dal05', 'private': 'https://dal05/auth/v1.0/', diff --git a/tests/CLI/modules/rwhois_tests.py b/tests/CLI/modules/rwhois_tests.py index e79bdbe9d..6409bf884 100644 --- a/tests/CLI/modules/rwhois_tests.py +++ b/tests/CLI/modules/rwhois_tests.py @@ -34,7 +34,7 @@ def test_edit(self): '--state=TX', '--private']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") self.assert_called_with('SoftLayer_Network_Subnet_Rwhois_Data', @@ -55,7 +55,7 @@ def test_edit(self): def test_edit_public(self): result = self.run_command(['rwhois', 'edit', '--public']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") self.assert_called_with('SoftLayer_Network_Subnet_Rwhois_Data', @@ -77,5 +77,5 @@ def test_show(self): 'Postal Code': 'postalCode', 'State': '-', 'Private Residence': True} - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), expected) diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py index a0802772a..63fcab7ea 100644 --- a/tests/CLI/modules/server_tests.py +++ b/tests/CLI/modules/server_tests.py @@ -22,7 +22,7 @@ class ServerCLITests(testing.TestCase): def test_server_cancel_reasons(self): result = self.run_command(['server', 'cancel-reasons']) output = json.loads(result.output) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(len(output), 10) def test_server_details(self): @@ -54,7 +54,7 @@ def test_server_details(self): {'id': 19082, 'number': 3672, 'type': 'PUBLIC'}] } - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), expected) def test_detail_vs_empty_tag(self): @@ -70,7 +70,7 @@ def test_detail_vs_empty_tag(self): } result = self.run_command(['server', 'detail', '100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual( json.loads(result.output)['tags'], ['example-tag'], @@ -114,7 +114,7 @@ def test_list_servers(self): }, ] - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(expected, json.loads(result.output)) @mock.patch('SoftLayer.CLI.formatting.no_going_back') @@ -126,7 +126,7 @@ def test_server_reload(self, reload_mock, ngb_mock): result = self.run_command(['--really', 'server', 'reload', '12345', '--key=4567']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) reload_mock.assert_called_with(12345, None, [4567]) # Now check to make sure we properly call CLIAbort in the negative case @@ -144,7 +144,7 @@ def test_cancel_server(self, cancel_mock, ngb_mock): result = self.run_command(['--really', 'server', 'cancel', '12345', '--reason=Test', '--comment=Test']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) cancel_mock.assert_called_with(12345, "Test", "Test", False) # Test @@ -172,7 +172,7 @@ def test_server_power_off(self, confirm_mock): def test_server_reboot_default(self): result = self.run_command(['--really', 'server', 'reboot', '12345']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Hardware_Server', 'rebootDefault', identifier=12345) @@ -180,7 +180,7 @@ def test_server_reboot_soft(self): result = self.run_command(['--really', 'server', 'reboot', '12345', '--soft']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Hardware_Server', 'rebootSoft', identifier=12345) @@ -188,7 +188,7 @@ def test_server_reboot_hard(self): result = self.run_command(['--really', 'server', 'reboot', '12345', '--hard']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Hardware_Server', 'rebootHard', identifier=12345) @@ -203,7 +203,7 @@ def test_server_reboot_negative(self, confirm_mock): def test_server_power_on(self): result = self.run_command(['--really', 'server', 'power-on', '12345']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Hardware_Server', 'powerOn', identifier=12345) @@ -211,7 +211,7 @@ def test_server_power_cycle(self): result = self.run_command(['--really', 'server', 'power-cycle', '12345']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Hardware_Server', 'powerCycle', identifier=12345) @@ -250,7 +250,7 @@ def test_create_server_test_flag(self, verify_mock): '--test'], fmt='raw') - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertIn("First Item", result.output) self.assertIn("Second Item", result.output) self.assertIn("Total monthly cost", result.output) @@ -258,7 +258,7 @@ def test_create_server_test_flag(self, verify_mock): def test_create_options(self): result = self.run_command(['server', 'create-options']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) expected = [ [{'datacenter': 'Washington 1', 'value': 'wdc01'}], [{'size': 'Single Xeon 1270, 8GB Ram, 2x1TB SATA disks, Non-RAID', @@ -288,7 +288,7 @@ def test_create_server(self, order_mock): '--key=10', ]) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'id': 98765, 'created': '2013-08-02 15:23:47'}) @@ -320,7 +320,7 @@ def test_create_server_with_export(self, export_mock): '--export=/path/to/test_file.txt'], fmt='raw') - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertIn("Successfully exported options to a template file.", result.output) export_mock.assert_called_with('/path/to/test_file.txt', @@ -358,7 +358,7 @@ def test_edit_server_userdata(self): '--domain=test.sftlyr.ws', '--userdata=My data']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") self.assert_called_with('SoftLayer_Hardware_Server', 'editObject', args=({'domain': 'test.sftlyr.ws', @@ -388,7 +388,7 @@ def test_edit_server_userfile(self): result = self.run_command(['server', 'edit', '1000', '--userfile=%s' % userfile.name]) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") self.assert_called_with('SoftLayer_Hardware_Server', 'setUserMetadata', @@ -400,7 +400,7 @@ def test_update_firmware(self, confirm_mock): confirm_mock.return_value = True result = self.run_command(['server', 'update-firmware', '1000']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, "") self.assert_called_with('SoftLayer_Hardware_Server', 'createFirmwareUpdateTransaction', @@ -417,7 +417,7 @@ def test_edit(self): '--private-speed=100', '100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, '') self.assert_called_with( diff --git a/tests/CLI/modules/sshkey_tests.py b/tests/CLI/modules/sshkey_tests.py index 68b8631fb..fc1ded0e6 100644 --- a/tests/CLI/modules/sshkey_tests.py +++ b/tests/CLI/modules/sshkey_tests.py @@ -23,7 +23,7 @@ def test_add_by_option(self): '--key=%s' % mock_key, '--note=my key']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), "SSH key added: aa:bb:cc:dd") self.assert_called_with('SoftLayer_Security_Ssh_Key', 'createObject', @@ -37,7 +37,7 @@ def test_add_by_file(self): result = self.run_command(['sshkey', 'add', 'key1', '--in-file=%s' % path]) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), "SSH key added: aa:bb:cc:dd") service = self.client['Security_Ssh_Key'] @@ -50,7 +50,7 @@ def test_add_by_file(self): def test_remove_key(self): result = self.run_command(['--really', 'sshkey', 'remove', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Security_Ssh_Key', 'deleteObject', identifier=1234) @@ -65,7 +65,7 @@ def test_edit_key(self): result = self.run_command(['sshkey', 'edit', '1234', '--label=key1', '--note=my key']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Security_Ssh_Key', 'editObject', args=({'notes': 'my key', 'label': 'key1'},), @@ -83,7 +83,7 @@ def test_edit_key_fail(self): def test_list_keys(self): result = self.run_command(['sshkey', 'list']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'notes': '-', 'fingerprint': None, @@ -97,7 +97,7 @@ def test_list_keys(self): def test_print_key(self): result = self.run_command(['sshkey', 'print', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'id': 1234, 'label': 'label', 'notes': 'notes'}) @@ -108,5 +108,5 @@ def test_print_key_file(self): result = self.run_command(['sshkey', 'print', '1234', '--out-file=%s' % sshkey_file.name]) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(mock_key, sshkey_file.read().decode("utf-8")) diff --git a/tests/CLI/modules/summary_tests.py b/tests/CLI/modules/summary_tests.py index 551e3c9fe..f6a57fb73 100644 --- a/tests/CLI/modules/summary_tests.py +++ b/tests/CLI/modules/summary_tests.py @@ -25,5 +25,5 @@ def test_summary(self): } ] - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), expected) diff --git a/tests/CLI/modules/ticket_tests.py b/tests/CLI/modules/ticket_tests.py index 5a579c646..69674c636 100644 --- a/tests/CLI/modules/ticket_tests.py +++ b/tests/CLI/modules/ticket_tests.py @@ -4,9 +4,11 @@ :license: MIT, see LICENSE for more details. """ -from SoftLayer import testing - import json +import mock + +from SoftLayer.CLI import exceptions +from SoftLayer import testing class TicketTests(testing.TestCase): @@ -20,7 +22,7 @@ def test_list(self): 'last_edited': '2013-08-01T14:16:47-07:00', 'status': 'Open', 'title': 'Cloud Instance Cancellation - 08/01/13'}] - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), expected) def test_detail(self): @@ -36,5 +38,132 @@ def test_detail(self): 'update 2': 'By John Smith\nuser says something', 'update 3': 'By emp1 (Employee)\nemployee says something', } - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), expected) + + def test_create(self): + result = self.run_command(['ticket', 'create', '--title=Test', + '--subject-id=1000', + '--body=ticket body']) + + self.assert_no_fail(result) + + args = ({'subjectId': 1000, + 'contents': 'ticket body', + 'assignedUserId': 12345, + 'title': 'Test'}, 'ticket body') + + self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket', + args=args) + + def test_create_and_attach(self): + result = self.run_command(['ticket', 'create', '--title=Test', + '--subject-id=1000', + '--body=ticket body', + '--hardware=234', + '--virtual=567']) + + self.assert_no_fail(result) + + args = ({'subjectId': 1000, + 'contents': 'ticket body', + 'assignedUserId': 12345, + 'title': 'Test'}, 'ticket body') + + self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket', + args=args) + self.assert_called_with('SoftLayer_Ticket', 'addAttachedHardware', + args=(234,), + identifier=100) + self.assert_called_with('SoftLayer_Ticket', 'addAttachedVirtualGuest', + args=(567,), + identifier=100) + + @mock.patch('click.edit') + def test_create_no_body(self, edit_mock): + edit_mock.return_value = 'ticket body' + result = self.run_command(['ticket', 'create', '--title=Test', + '--subject-id=1000']) + self.assert_no_fail(result) + + args = ({'subjectId': 1000, + 'contents': 'ticket body', + 'assignedUserId': 12345, + 'title': 'Test'}, 'ticket body') + + self.assert_called_with('SoftLayer_Ticket', 'createStandardTicket', + args=args) + + def test_subjects(self): + list_expected_ids = [1001, 1002, 1003, 1004, 1005] + result = self.run_command(['ticket', 'subjects']) + + self.assert_no_fail(result) + results = json.loads(result.output) + for result in results: + self.assertIn(result['id'], list_expected_ids) + + def test_attach_no_identifier(self): + result = self.run_command(['ticket', 'attach', '1']) + + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.ArgumentError) + + def test_attach_two_identifiers(self): + result = self.run_command(['ticket', + 'attach', + '1', + '--hardware=100', + '--virtual=100']) + + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.ArgumentError) + + def test_ticket_attach_hardware(self): + result = self.run_command(['ticket', 'attach', '1', '--hardware=100']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Ticket', 'addAttachedHardware', + args=(100,), + identifier=1) + + def test_ticket_attach_virtual_server(self): + result = self.run_command(['ticket', 'attach', '1', '--virtual=100']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Ticket', 'addAttachedVirtualGuest', + args=(100,), + identifier=1) + + def test_detach_no_identifier(self): + result = self.run_command(['ticket', 'detach', '1']) + + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.ArgumentError) + + def test_detach_two_identifiers(self): + result = self.run_command(['ticket', + 'detach', + '1', + '--hardware=100', + '--virtual=100']) + self.assertEqual(result.exit_code, 2) + self.assertIsInstance(result.exception, exceptions.ArgumentError) + + def test_ticket_detach_hardware(self): + result = self.run_command(['ticket', 'detach', '1', '--hardware=100']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Ticket', + 'removeAttachedHardware', + args=(100,), + identifier=1) + + def test_ticket_detach_virtual_server(self): + result = self.run_command(['ticket', 'detach', '1', '--virtual=100']) + + self.assert_no_fail(result) + self.assert_called_with('SoftLayer_Ticket', + 'removeAttachedVirtualGuest', + args=(100,), + identifier=1) diff --git a/tests/CLI/modules/vs_tests.py b/tests/CLI/modules/vs_tests.py index 573de5fe8..53bad7c31 100644 --- a/tests/CLI/modules/vs_tests.py +++ b/tests/CLI/modules/vs_tests.py @@ -17,7 +17,7 @@ class VirtTests(testing.TestCase): def test_list_vs(self): result = self.run_command(['vs', 'list', '--tag=tag']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), [{'datacenter': 'TEST00', 'primary_ip': '172.16.240.2', @@ -36,7 +36,7 @@ def test_detail_vs(self): result = self.run_command(['vs', 'detail', '100', '--passwords', '--price']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'active_transaction': None, 'cores': 2, @@ -61,7 +61,9 @@ def test_detail_vs(self): 'public_ip': '172.16.240.2', 'state': 'RUNNING', 'status': 'ACTIVE', - 'users': [{'password': 'pass', 'username': 'user'}], + 'users': [{'software': 'Ubuntu', + 'password': 'pass', + 'username': 'user'}], 'vlans': [{'type': 'PUBLIC', 'number': 23, 'id': 1}], @@ -80,7 +82,7 @@ def test_detail_vs_empty_tag(self): } result = self.run_command(['vs', 'detail', '100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual( json.loads(result.output)['tags'], ['example-tag'], @@ -89,7 +91,7 @@ def test_detail_vs_empty_tag(self): def test_create_options(self): result = self.run_command(['vs', 'create-options']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'cpus (private)': [], 'cpus (standard)': ['1', '2', '3', '4'], @@ -116,7 +118,7 @@ def test_create(self, confirm_mock): '--tag=dev', '--tag=green']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'guid': '1a2b3c-1701', 'id': 100, @@ -147,7 +149,7 @@ def test_create_with_integer_image_id(self, confirm_mock): '--billing=hourly', '--datacenter=dal05']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(json.loads(result.output), {'guid': '1a2b3c-1701', 'id': 100, @@ -204,7 +206,7 @@ def test_dns_sync_both(self, confirm_mock): result = self.run_command(['vs', 'dns-sync', '100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Dns_Domain', 'getResourceRecords') self.assert_called_with('SoftLayer_Virtual_Guest', 'getReverseDomainRecords') @@ -251,7 +253,7 @@ def test_dns_sync_v6(self, confirm_mock): },) guest.return_value = test_guest result = self.run_command(['vs', 'dns-sync', '--aaaa-record', '100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'createObject', args=createV6args) @@ -269,7 +271,7 @@ def test_dns_sync_v6(self, confirm_mock): getResourceRecords.return_value = [v6Record] editArgs = (v6Record,) result = self.run_command(['vs', 'dns-sync', '--aaaa-record', '100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'editObject', args=editArgs) @@ -295,7 +297,7 @@ def test_dns_sync_edit_a(self, confirm_mock): 'id': 1, 'ttl': 7200}, ) result = self.run_command(['vs', 'dns-sync', '-a', '100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'editObject', args=editArgs) @@ -330,7 +332,7 @@ def test_dns_sync_edit_ptr(self, confirm_mock): editArgs = ({'host': '2', 'data': 'vs-test1.test.sftlyr.ws', 'id': 100, 'ttl': 7200},) result = self.run_command(['vs', 'dns-sync', '--ptr', '100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Dns_Domain_ResourceRecord', 'editObject', args=editArgs) @@ -379,7 +381,7 @@ def test_upgrade(self, confirm_mock): confirm_mock.return_value = True result = self.run_command(['vs', 'upgrade', '100', '--cpu=4', '--memory=2048', '--network=1000']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Product_Order', 'placeOrder') call = self.calls('SoftLayer_Product_Order', 'placeOrder')[0] order_container = call.args[0] @@ -399,7 +401,7 @@ def test_edit(self): '--private-speed=100', '100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual(result.output, '') self.assert_called_with( diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py new file mode 100644 index 000000000..a6b6f866a --- /dev/null +++ b/tests/managers/block_tests.py @@ -0,0 +1,212 @@ +""" + SoftLayer.tests.managers.block_tests + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :license: MIT, see LICENSE for more details. +""" + +import SoftLayer +from SoftLayer import fixtures +from SoftLayer import testing + + +class BlockTests(testing.TestCase): + def set_up(self): + self.block = SoftLayer.BlockStorageManager(self.client) + + def test_cancel_block_volume_immediately(self): + self.block.cancel_block_volume(123, immediate=True) + + self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', + args=(True, True, 'No longer needed'), + identifier=449) + + def test_get_block_volume_details(self): + result = self.block.get_block_volume_details(100) + + self.assertEqual(fixtures.SoftLayer_Network_Storage.getObject, + result) + self.assert_called_with('SoftLayer_Network_Storage', 'getObject', + identifier=100) + + def test_list_block_volumes(self): + result = self.block.list_block_volumes() + + self.assertEqual(fixtures.SoftLayer_Account.getIscsiNetworkStorage, + result) + self.assert_called_with('SoftLayer_Account', 'getIscsiNetworkStorage') + + def test_get_block_volume_access_list(self): + result = self.block.get_block_volume_access_list(100) + + self.assertEqual(fixtures.SoftLayer_Network_Storage.getObject, + result) + self.assert_called_with('SoftLayer_Network_Storage', 'getObject', + identifier=100) + + def test_get_block_volume_snapshot_list(self): + result = self.block.get_block_volume_snapshot_list(100) + + self.assertEqual(fixtures.SoftLayer_Network_Storage.getSnapshots, + result) + self.assert_called_with('SoftLayer_Network_Storage', 'getSnapshots', + identifier=100) + + def test_delete_snapshot(self): + result = self.block.delete_snapshot(100) + + self.assertEqual(fixtures.SoftLayer_Network_Storage.deleteObject, + result) + self.assert_called_with('SoftLayer_Network_Storage', 'deleteObject', + identifier=100) + + def test_order_block_volume_no_package(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [] + + self.assertRaises( + ValueError, + self.block.order_block_volume, + "performance_storage_iscsi", "dal05", 100, "LINUX", iops=100, + ) + + def test_order_block_volume_too_many_packages(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [{}, {}] + + self.assertRaises( + ValueError, + self.block.order_block_volume, + "performance_storage_iscsi", "dal05", 100, "LINUX", iops=100, + ) + + def test_order_block_volume_performance(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [{ + 'id': 1, + 'name': 'Performance', + 'items': [{ + 'capacity': '1', + 'prices': [{ + 'id': 1, + 'locationGroupId': '', + 'categories': [{ + 'categoryCode': 'performance_storage_iscsi', + }], + }], + }, { + 'capacity': '100', + 'prices': [{ + 'id': 2, + 'locationGroupId': '', + 'categories': [{ + 'categoryCode': 'performance_storage_space', + }], + }], + }, { + 'capacity': '100', + 'prices': [{ + 'id': 3, + 'locationGroupId': '', + 'categories': [{ + 'categoryCode': 'performance_storage_iops', + }], + 'capacityRestrictionMinimum': '100', + 'capacityRestrictionMaximum': '100', + }], + }], + }] + + result = self.block.order_block_volume( + "performance_storage_iscsi", "dal05", 100, "LINUX", iops=100) + + self.assertEqual( + result, + { + 'orderDate': '2013-08-01 15:23:45', + 'orderId': 1234, + 'prices': [{ + 'hourlyRecurringFee': '2', + 'id': 1, + 'item': {'description': 'this is a thing', 'id': 1}, + 'laborFee': '2', + 'oneTimeFee': '2', + 'oneTimeFeeTax': '.1', + 'quantity': 1, + 'recurringFee': '2', + 'recurringFeeTax': '.1', + 'setupFee': '1'}], + }, + ) + + def test_order_block_volume_endurance(self): + mock = self.set_mock('SoftLayer_Product_Package', 'getAllObjects') + mock.return_value = [{ + 'id': 1, + 'name': 'Performance', + 'items': [{ + 'capacity': '1', + 'prices': [{ + 'id': 1, + 'locationGroupId': '', + 'categories': [{ + 'categoryCode': 'storage_block', + }], + }], + }, { + 'capacity': '1', + 'prices': [{ + 'id': 2, + 'locationGroupId': '', + 'categories': [{ + 'categoryCode': 'storage_service_enterprise', + }], + }], + }, { + 'capacity': '100', + 'prices': [{ + 'id': 3, + 'locationGroupId': '', + 'categories': [{ + 'categoryCode': 'performance_storage_space', + }], + 'capacityRestrictionMinimum': '100', + 'capacityRestrictionMaximum': '100', + }], + }, { + 'capacity': '100', + 'attributes': [{ + 'value': '100', + }], + 'prices': [{ + 'id': 4, + 'locationGroupId': '', + 'categories': [{ + 'categoryCode': 'storage_tier_level', + }], + }], + }], + }] + + result = self.block.order_block_volume( + "storage_service_enterprise", "dal05", 100, "LINUX", + tier_level=0.25) + + self.assertEqual( + result, + { + 'orderDate': '2013-08-01 15:23:45', + 'orderId': 1234, + 'prices': [{ + 'hourlyRecurringFee': '2', + 'id': 1, + 'item': {'description': 'this is a thing', 'id': 1}, + 'laborFee': '2', + 'oneTimeFee': '2', + 'oneTimeFeeTax': '.1', + 'quantity': 1, + 'recurringFee': '2', + 'recurringFeeTax': '.1', + 'setupFee': '1'}], + }, + ) diff --git a/tests/managers/ticket_tests.py b/tests/managers/ticket_tests.py index aa75c9826..2ebbfced7 100644 --- a/tests/managers/ticket_tests.py +++ b/tests/managers/ticket_tests.py @@ -77,3 +77,28 @@ def test_update_ticket(self): self.assert_called_with('SoftLayer_Ticket', 'addUpdate', args=({'entry': 'Update1'},), identifier=100) + + def test_attach_hardware(self): + self.ticket.attach_hardware(100, 123) + self.assert_called_with('SoftLayer_Ticket', 'addAttachedHardware', + args=(123,), + identifier=100) + + def test_attach_virtual_server(self): + self.ticket.attach_virtual_server(100, 123) + self.assert_called_with('SoftLayer_Ticket', 'addAttachedVirtualGuest', + args=(123,), + identifier=100) + + def test_detach_hardware(self): + self.ticket.detach_hardware(100, 123) + self.assert_called_with('SoftLayer_Ticket', 'removeAttachedHardware', + args=(123,), + identifier=100) + + def test_detach_virtual_server(self): + self.ticket.detach_virtual_server(100, 123) + self.assert_called_with('SoftLayer_Ticket', + 'removeAttachedVirtualGuest', + args=(123,), + identifier=100) diff --git a/tox.ini b/tox.ini index edf21817b..41a70f0eb 100644 --- a/tox.ini +++ b/tox.ini @@ -2,15 +2,12 @@ envlist = py27,py33,py34,py35,pypy,analysis,coverage [testenv] -setenv = - LANG=C.UTF-8 - LC_ALL=C.UTF-8 deps = -r{toxinidir}/tools/test-requirements.txt -commands = py.test tests +commands = py.test {posargs:tests} [testenv:coverage] basepython = python2.7 -commands = py.test tests \ +commands = py.test {posargs:tests} \ --cov=SoftLayer \ --cov-fail-under=77 \ --cov-report=html \