From 4f5bf4fef938fa8754c4daa6f598578862183698 Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 27 Mar 2016 10:38:40 +0200 Subject: [PATCH 01/16] Simplify with a dict comprehension --- SoftLayer/utils.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 035461784..61b72610a 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): From ac953a67a1d90326559c9e2f9418eead68c2a648 Mon Sep 17 00:00:00 2001 From: cclauss Date: Sun, 27 Mar 2016 13:01:24 +0200 Subject: [PATCH 02/16] PEP8: E128 continuation line under-indented for visual indent --- SoftLayer/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py index 61b72610a..f39ffc0fe 100644 --- a/SoftLayer/utils.py +++ b/SoftLayer/utils.py @@ -60,7 +60,7 @@ def to_dict(self): This is needed for places where strict type checking is done. """ return {key: val.to_dict() if isinstance(val, NestedDict) else val - for key, val in self.items()} + for key, val in self.items()} def query_filter(query): From e54601d8826fa05653942aefaeacd9ce9506b2fa Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 30 Mar 2016 16:24:07 -0500 Subject: [PATCH 03/16] Improve release script --- fabfile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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) From ffcb72d1c706f89f2b9e6047104a528eadae0dd9 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Thu, 31 Mar 2016 09:37:37 -0500 Subject: [PATCH 04/16] Fixes vlan detail if access to subnets is restricted --- SoftLayer/CLI/vlan/detail.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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'], From 6d8787ac06748ade5fe4d05f312d78a5b9427edb Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 1 Apr 2016 11:50:44 -0500 Subject: [PATCH 05/16] Re-Add hardware, subnets and virtualGuest mask to vlan detail --- SoftLayer/managers/network.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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. From 59d3614e284f156037adcaf2bced2eb07bbb4553 Mon Sep 17 00:00:00 2001 From: Cameron Porter Date: Mon, 4 Apr 2016 00:06:22 -0500 Subject: [PATCH 06/16] Add ability to attach/detach devices to tickets. Add attach arguments for hardware and virtual to ticket create CLI. Add new ticket attach and detach CLI commands. Add methods to ticket manager to attach and detach devices. --- SoftLayer/CLI/routes.py | 2 + SoftLayer/CLI/ticket/attach.py | 42 ++++++++ SoftLayer/CLI/ticket/create.py | 36 +++++-- SoftLayer/CLI/ticket/detach.py | 42 ++++++++ SoftLayer/fixtures/SoftLayer_Ticket.py | 17 ++++ SoftLayer/managers/ticket.py | 40 +++++++- tests/CLI/modules/ticket_tests.py | 133 ++++++++++++++++++++++++- tests/managers/ticket_tests.py | 25 +++++ 8 files changed, 327 insertions(+), 10 deletions(-) create mode 100644 SoftLayer/CLI/ticket/attach.py create mode 100644 SoftLayer/CLI/ticket/detach.py diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 0d93f2ec7..26378fb4b 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -188,6 +188,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/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/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/tests/CLI/modules/ticket_tests.py b/tests/CLI/modules/ticket_tests.py index 5a579c646..6f54e5cce 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): @@ -38,3 +40,130 @@ def test_detail(self): } self.assertEqual(result.exit_code, 0) 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.assertEqual(result.exit_code, 0) + + 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.assertEqual(result.exit_code, 0) + + 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.assertEqual(result.exit_code, 0) + + 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.assertEqual(result.exit_code, 0) + 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.assertEqual(result.exit_code, 0) + 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.assertEqual(result.exit_code, 0) + 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.assertEqual(result.exit_code, 0) + 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.assertEqual(result.exit_code, 0) + self.assert_called_with('SoftLayer_Ticket', + 'removeAttachedVirtualGuest', + args=(100,), + identifier=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) From 6c63d743686bf41371450ed0ec849864045c87ad Mon Sep 17 00:00:00 2001 From: cclauss Date: Mon, 11 Apr 2016 22:42:10 +0200 Subject: [PATCH 07/16] Add Python 3.5 and later this year Python 3.6. (#703) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From e35107a2a79b6bfd6a5e04e4b887298133912ad1 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 22 Apr 2016 12:51:22 -0500 Subject: [PATCH 08/16] slcli virtual list now lists users/passwords for all known software (#719) --- SoftLayer/CLI/virt/detail.py | 16 +++++++++++++--- SoftLayer/fixtures/SoftLayer_Virtual_Guest.py | 5 +++++ SoftLayer/managers/vs.py | 3 +++ tests/CLI/modules/vs_tests.py | 4 +++- 4 files changed, 24 insertions(+), 4 deletions(-) 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/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/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/tests/CLI/modules/vs_tests.py b/tests/CLI/modules/vs_tests.py index 573de5fe8..4eb31578a 100644 --- a/tests/CLI/modules/vs_tests.py +++ b/tests/CLI/modules/vs_tests.py @@ -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}], From f6b0fd735f4d21fa1ade8c71afd75859511f6038 Mon Sep 17 00:00:00 2001 From: Tomy Durden Date: Wed, 25 Nov 2015 12:02:51 -0600 Subject: [PATCH 09/16] Initial addition of softlayer-python block storage * Created SoftLayer.managers.block.BlockStorageManager * Created SoftLayer.managers.block.BlockStorageManager#list_block_volumes * Created SoftLayer.managers.block.BlockStorageManager#get_block_volume_details * Created SoftLayer.managers.block.BlockStorageManager#get_block_volume_access_list * Created SoftLayer.managers.block.BlockStorageManager#get_block_volume_snapshot_list * Created SoftLayer.managers.block.BlockStorageManager#order_block_volume * Created SoftLayer.managers.block.BlockStorageManager#_get_package * Created SoftLayer.managers.block.BlockStorageManager#_get_item_prices_performance * Created SoftLayer.managers.block.BlockStorageManager#_get_item_prices_endurance * Created SoftLayer.managers.block.BlockStorageManager#_get_location_id * Created block.volume-list route and CLI module * Created block.volume-detail route and CLI module * Created block.volume-order route and CLI module * Created block.access-list route and CLI module * Created block.snapshot-list route and CLI module --- SoftLayer/CLI/block/__init__.py | 1 + SoftLayer/CLI/block/access/__init__.py | 1 + SoftLayer/CLI/block/access/list.py | 93 ++++++++ SoftLayer/CLI/block/detail.py | 54 +++++ SoftLayer/CLI/block/list.py | 63 ++++++ SoftLayer/CLI/block/order.py | 73 +++++++ SoftLayer/CLI/block/snapshot/__init__.py | 1 + SoftLayer/CLI/block/snapshot/list.py | 56 +++++ SoftLayer/CLI/routes.py | 7 + SoftLayer/managers/__init__.py | 2 + SoftLayer/managers/block.py | 263 +++++++++++++++++++++++ 11 files changed, 614 insertions(+) create mode 100644 SoftLayer/CLI/block/__init__.py create mode 100644 SoftLayer/CLI/block/access/__init__.py create mode 100644 SoftLayer/CLI/block/access/list.py create mode 100644 SoftLayer/CLI/block/detail.py create mode 100644 SoftLayer/CLI/block/list.py create mode 100644 SoftLayer/CLI/block/order.py create mode 100644 SoftLayer/CLI/block/snapshot/__init__.py create mode 100644 SoftLayer/CLI/block/snapshot/list.py create mode 100644 SoftLayer/managers/block.py 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/__init__.py b/SoftLayer/CLI/block/access/__init__.py new file mode 100644 index 000000000..050b3938d --- /dev/null +++ b/SoftLayer/CLI/block/access/__init__.py @@ -0,0 +1 @@ +"""Block Storage Access Control.""" diff --git a/SoftLayer/CLI/block/access/list.py b/SoftLayer/CLI/block/access/list.py new file mode 100644 index 000000000..5871623bb --- /dev/null +++ b/SoftLayer/CLI/block/access/list.py @@ -0,0 +1,93 @@ +"""List hosts with access to volume.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + +COLUMNS = [ + column_helper.Column('id', ('id',)), + column_helper.Column('hostname', ('hostname',)), + column_helper.Column('type', ('type',)), + column_helper.Column('primaryBackendIpAddress', ('primaryBackendIpAddress',)), +] + +DEFAULT_COLUMNS = [ + 'id', + 'hostname', + 'type', + 'primaryBackendIpAddress', +] + + +@click.command() +@click.argument('volume_id') +@click.option('--sortby', help='Column to sort by', default='hostname') +@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 block storage.""" + 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 + + if access_list: + if 'allowedVirtualGuests' in access_list.keys(): + for host in access_list['allowedVirtualGuests']: + host['type'] = 'VIRTUAL' + host['hostname'] = "{0}.{1}".format( + host['hostname'], + host['domain']) + table.add_row([value or formatting.blank() + for value in columns.row(host)]) + + if 'allowedHardware' in access_list.keys(): + for host in access_list['allowedHardware']: + host['type'] = 'HARDWARE' + host['hostname'] = "{0}.{1}".format( + host['hostname'], + host['domain']) + table.add_row([value or formatting.blank() + for value in columns.row(host)]) + + if 'allowedSubnets' in access_list.keys(): + for host in access_list['allowedSubnets']: + host['type'] = 'SUBNET' + if 'note' in host.keys(): + host['hostname'] = "{0}/{1} ({3})".format( + host['networkIdentifier'], + host['cidr'], + host['note'], + ) + else: + host['hostname'] = "{0}/{1}".format( + host['networkIdentifier'], + host['cidr'] + ) + table.add_row([value or formatting.blank() + for value in columns.row(host)]) + + if 'allowedIpAddresses' in access_list.keys(): + for host in access_list['allowedIpAddresses']: + host['type'] = 'IP' + if 'note' in host.keys(): + host['hostname'] = "{0} ({1})".format( + host['ipAddress'], + host['note'] + ) + else: + host['hostname'] = host['ipAddress'] + table.add_row([value or formatting.blank() + for value in columns.row(host)]) + + env.fout(table) + else: + click.echo("No authorized hosts found.") diff --git a/SoftLayer/CLI/block/detail.py b/SoftLayer/CLI/block/detail.py new file mode 100644 index 000000000..66b97e1ed --- /dev/null +++ b/SoftLayer/CLI/block/detail.py @@ -0,0 +1,54 @@ +"""Display details for a specified volume.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting +from SoftLayer import utils + +import click + + +@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' + + table.add_row( + ['ID', block_volume['id']]) + table.add_row( + ['Username', block_volume['username']]) + table.add_row( + ['Type', block_volume['storageType']['keyName'].split('_').pop(0)]) + table.add_row( + ['Capacity (GB)', "%iGB" % block_volume['capacityGb']]) + + if block_volume['storageType']['keyName'].split('_').pop(0) == 'PERFORMANCE': + table.add_row( + ['IOPs', block_volume['iops']]) + if block_volume['storageType']['keyName'].split('_').pop(0) == 'ENDURANCE': + table.add_row( + ['Endurance Tier', block_volume['storageTierLevel']['description']]) + + table.add_row( + ['Data Center', block_volume['serviceResource']['datacenter']['name']]) + table.add_row( + ['Bytes Used', block_volume['bytesUsed']]) + table.add_row( + ['IP', block_volume['serviceResourceBackendIpAddress']]) + + if block_volume['snapshotCapacityGb']: + table.add_row( + ['Snapshot Reserved (GB)', block_volume['snapshotCapacityGb']]) + table.add_row( + ['Snapshot Used (Byes)', block_volume['snapshotSizeBytes']]) + + env.fout(table) diff --git a/SoftLayer/CLI/block/list.py b/SoftLayer/CLI/block/list.py new file mode 100644 index 000000000..80c669d6b --- /dev/null +++ b/SoftLayer/CLI/block/list.py @@ -0,0 +1,63 @@ +"""List block storage volumes.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + +COLUMNS = [ + column_helper.Column('id', ('id',)), + column_helper.Column('username', ('username',)), + column_helper.Column('datacenter', + ('serviceResource', 'datacenter', 'name')), + column_helper.Column('storageType', ('storageType', 'keyName')), + column_helper.Column('capacityGb', ('capacityGb',)), + column_helper.Column('bytesUsed', ('bytesUsed',)), + column_helper.Column('ipAddr', ('serviceResourceBackendIpAddress',)), +] + +DEFAULT_COLUMNS = [ + 'id', + 'username', + 'datacenter', + 'storageType', + 'capacityGb', + 'bytesUsed', + 'ipAddr' +] + + +@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: + block_volume['storageType']['keyName'] = block_volume['storageType'][ + 'keyName'].split('_').pop(0) + 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..ac2247334 --- /dev/null +++ b/SoftLayer/CLI/block/order.py @@ -0,0 +1,73 @@ +"""Order a block storage volume.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import environment +from SoftLayer.CLI import exceptions +import click + + +@click.command() +@click.option('--storage_type', + help='Type of storage volume', + type=click.Choice(['performance', 'endurance']), + required=True) +@click.option('--size', + help='Size of storage volume', + required=True) +@click.option('--iops', + help='Performance Storage IOPs') +@click.option('--tier', + help='Endurance Storage Tier (IOP per GB)', + type=click.Choice(['0.25', '2', '4'])) +@click.option('--os', + help='Operating System', + type=click.Choice([ + 'HYPER_V', + 'LINUX', + 'VMWARE', + 'WINDOWS_2008', + 'WINDOWS_GPT', + 'WINDOWS', + 'XEN']), + required=True) +@click.option('--location', + help='Size of storage volume', + required=True) +@environment.pass_env +def cli(env, storage_type, size, iops, tier, os, location): + """Order a block storage volume.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + + if storage_type == 'performance': + if iops is None: + raise exceptions.CLIAbort('Option --iops required with performance') + + order = block_manager.order_block_volume( + storage_type='performance_storage_iscsi', + location=location, + size=size, + iops=iops, + os_type=os + ) + + if storage_type == 'endurance': + if tier is None: + raise exceptions.CLIAbort('Option --tier required with performance') + + order = block_manager.order_block_volume( + storage_type='storage_service_enterprise', + location=location, + size=size, + tier_level=tier, + os_type=os + ) + + if 'placedOrder' in order.keys(): + print "Order #{0} placed successfully!".format( + order['placedOrder']['id']) + for item in order['placedOrder']['items']: + print " > %s" % item['description'] + else: + print "Order could not be placed! Please verify your options " \ + "and try again." diff --git a/SoftLayer/CLI/block/snapshot/__init__.py b/SoftLayer/CLI/block/snapshot/__init__.py new file mode 100644 index 000000000..4b84f0814 --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/__init__.py @@ -0,0 +1 @@ +"""Block Storage Snapshots.""" diff --git a/SoftLayer/CLI/block/snapshot/list.py b/SoftLayer/CLI/block/snapshot/list.py new file mode 100644 index 000000000..d7551987e --- /dev/null +++ b/SoftLayer/CLI/block/snapshot/list.py @@ -0,0 +1,56 @@ +"""List block storage snapshots.""" +# :license: MIT, see LICENSE for more details. + +import SoftLayer +from SoftLayer.CLI import columns as column_helper +from SoftLayer.CLI import environment +from SoftLayer.CLI import formatting + +import click + +COLUMNS = [ + column_helper.Column('id', ('id',)), + column_helper.Column('name', ('name',)), + column_helper.Column('snapshotCreationTimestamp', + ('snapshotCreationTimestamp',)), + column_helper.Column('snapshotSizeBytes', ('snapshotSizeBytes',)), +] + +DEFAULT_COLUMNS = [ + 'id', + 'name', + 'snapshotCreationTimestamp', + 'snapshotSizeBytes' +] + + +@click.command() +@click.argument('volume_id') +@click.option('--sortby', help='Column to sort by', + default='snapshotCreationTimestamp') +@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.""" + block_manager = SoftLayer.BlockStorageManager(env.client) + volume = block_manager.get_block_volume_snapshot_list(volume_id=volume_id) + + table = formatting.Table(columns.columns) + table.sortby = sortby + + if volume and 'snapshots' in volume.keys(): + for snapshot in volume['snapshots']: + + if 'notes' in snapshot.keys(): + snapshot['name'] = snapshot['notes'] + + table.add_row([value or formatting.blank() + for value in columns.row(snapshot)]) + + env.fout(table) + else: + click.echo("No snapshots found.") diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 26378fb4b..a3fdd8e9b 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -56,6 +56,13 @@ ('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-order', 'SoftLayer.CLI.block.order:cli'), + ('block:snapshot-list', 'SoftLayer.CLI.block.snapshot.list: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'), 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..e64f22b42 --- /dev/null +++ b/SoftLayer/managers/block.py @@ -0,0 +1,263 @@ +""" + SoftLayer.block + ~~~~~~~~~~~~~~~ + Block Storage Manager + + :license: MIT, see LICENSE for more details. +""" +from SoftLayer import exceptions +from SoftLayer import utils + + +class BlockStorageManager(utils.IdentifierMixin, object): + """Manages Block Storage volumes.""" + + def __init__(self, client): + self.configuration = {} + self.client = client + self.account = client['Account'] + self.product_package = self.client['Product_Package'] + self.block_svc = self.client['Network_Storage'] + self.block_os_types = self.client['Network_Storage_Iscsi_OS_Type'] + self.product_order = self.client['Product_Order'] + + 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'] = "mask[%s]" % ','.join(items) + + call = 'getNetworkStorage' + + _filter = utils.NestedDict(kwargs.get('filter') or {}) + + _filter['networkStorage']['storageType']['keyName'] = ( + utils.query_filter('*=BLOCK_STORAGE')) + if storage_type: + _filter['networkStorage']['storageType']['keyName'] = ( + utils.query_filter('%s_BLOCK_STORAGE' % storage_type.upper())) + + if datacenter: + _filter['networkStorage']['serviceResource']['datacenter'][ + 'name'] = ( + utils.query_filter(datacenter)) + + if username: + _filter['networkStorage']['username'] = ( + utils.query_filter(username)) + + kwargs['filter'] = _filter.to_dict() + func = getattr(self.account, call) + return func(**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', + 'bytesUsed', + 'storageType.keyName', + 'serviceResource.datacenter[name]', + 'serviceResourceBackendIpAddress', + 'storageTierLevel', + 'iops' + ] + kwargs['mask'] = "mask[%s]" % ','.join(items) + return self.block_svc.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', + 'allowedHardware', + 'allowedSubnets', + 'allowedIpAddresses' + ] + kwargs['mask'] = "mask[%s]" % ','.join(items) + return self.block_svc.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', + 'snapshots.notes', + 'snapshots.snapshotSizeBytes', + 'snapshots.storageType.keyName', + 'snapshots.snapshotCreationTimestamp', + 'snapshots[hourlySchedule,dailySchedule,weeklySchedule]', + ] + kwargs['mask'] = "mask[%s]" % ','.join(items) + return self.block_svc.getObject(id=volume_id, **kwargs) + + def order_block_volume(self, storage_type, location, size, os_type, + iops=None, tier_level=None, **kwargs): + + package = self._get_package(storage_type) + if package['name'] == 'Performance': + complexType = 'SoftLayer_Container_Product_Order_Network_PerformanceStorage_Iscsi' + prices = self._get_item_prices_performance(package, size, iops) + if package['name'] == 'Endurance': + complexType = 'SoftLayer_Container_Product_Order_Network_Storage_Enterprise' + prices = self._get_item_prices_endurance(package, size, tier_level) + + location_id = self._get_location_id(location) + + order = { + 'complexType': complexType, + 'packageId': package['id'], + 'osFormatType': {'keyName': os_type}, + 'prices': prices, + 'quantity': 1, + 'location': location_id, + } + + return self.product_order.placeOrder(order) + + def _get_package(self, category_code, **kwargs): + """ + Returns a product packaged based on type of storage. + :param category_code: Category code of product package. + :param kwargs: + :return: Returns a packaged based on type of storage. + """ + if 'mask' not in kwargs: + items = [ + 'id', + 'name', + 'items', + 'items[prices[categories],attributes]' + ] + kwargs['mask'] = "mask[%s]" % ','.join(items) + + _filter = utils.NestedDict(kwargs.get('filter') or {}) + _filter['categories']['categoryCode'] = ( + utils.query_filter(category_code)) + _filter['statusCode'] = (utils.query_filter('ACTIVE')) + kwargs['filter'] = _filter.to_dict() + + func = getattr(self.product_package, 'getAllObjects') + return func(**kwargs).pop() + + @staticmethod + def _get_item_prices_performance(package, size, iops, **kwargs): + """ + Returns a collection of prices for a performance storage order. + :param package: Package object + :param size: Size of volume in GB + :param iops: IOPs of volume + :param kwargs: + :return: Returns a collection of prices for a performance storage order. + """ + prices = [] + for item in package['items']: + for price in item['prices']: + for category in price['categories']: + if price['locationGroupId'] == '' \ + and (category[ + 'categoryCode'] == 'performance_storage_iscsi' + or (item['capacity'] == size + and category[ + 'categoryCode'] == 'performance_storage_space' + ) + or (item['capacity'] == iops + and category[ + 'categoryCode'] == 'performance_storage_iops' + and (price[ + 'capacityRestrictionMinimum'] <= size <= + price[ + 'capacityRestrictionMaximum']) + )): + prices.append(price) + return prices + + @staticmethod + def _get_item_prices_endurance(package, size, tier_level, **kwargs): + """ + Returns a collection of prices for a endurance storage order. + :param package: Package object + :param size: Size of volume in GB + :param iops: IOPs of volume + :param kwargs: + :return: Returns a collection of prices for a endurance storage order. + """ + + tiers = { + '0.25': '100', + '2': '200', + '4': '300' + } + + prices = [] + for item in package['items']: + for price in item['prices']: + for category in price['categories']: + if price['locationGroupId'] == '' \ + and (category[ + 'categoryCode'] == 'storage_service_enterprise' + or category['categoryCode'] == 'storage_block' + or (category[ + 'categoryCode'] == 'performance_storage_space' + and price[ + 'capacityRestrictionMinimum'] == tiers.get( + tier_level) and item[ + 'capacity'] == size + ) + or (category[ + 'categoryCode'] == 'storage_tier_level' + and item['attributes'][0][ + 'value'] == tiers.get(tier_level) + ) + ): + prices.append(price) + return prices + + 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.') From 4eac4db165eddb80e5325a7919262c6d1854091f Mon Sep 17 00:00:00 2001 From: Violet Rodriguez Date: Fri, 4 Dec 2015 10:32:50 -0600 Subject: [PATCH 10/16] Modify block-storage manager, detail, order, access-list to include more information Add volume-cancel option to performance/endurance block storage Add support for deleting snapshots Fix block detail view for snapshot capacity --- SoftLayer/CLI/block/access/list.py | 33 ++- SoftLayer/CLI/block/cancel.py | 28 +++ SoftLayer/CLI/block/detail.py | 20 +- SoftLayer/CLI/block/list.py | 10 +- SoftLayer/CLI/block/order.py | 56 +++-- SoftLayer/CLI/block/snapshot/delete.py | 15 ++ SoftLayer/CLI/block/snapshot/list.py | 2 +- SoftLayer/CLI/routes.py | 2 + SoftLayer/fixtures/SoftLayer_Account.py | 19 ++ .../fixtures/SoftLayer_Network_Storage.py | 20 ++ SoftLayer/managers/block.py | 232 ++++++++++++------ tests/managers/block_tests.py | 61 +++++ 12 files changed, 380 insertions(+), 118 deletions(-) create mode 100644 SoftLayer/CLI/block/cancel.py create mode 100644 SoftLayer/CLI/block/snapshot/delete.py create mode 100644 SoftLayer/fixtures/SoftLayer_Network_Storage.py create mode 100644 tests/managers/block_tests.py diff --git a/SoftLayer/CLI/block/access/list.py b/SoftLayer/CLI/block/access/list.py index 5871623bb..4450f9ab6 100644 --- a/SoftLayer/CLI/block/access/list.py +++ b/SoftLayer/CLI/block/access/list.py @@ -1,18 +1,35 @@ """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 -import click COLUMNS = [ - column_helper.Column('id', ('id',)), - column_helper.Column('hostname', ('hostname',)), - column_helper.Column('type', ('type',)), - column_helper.Column('primaryBackendIpAddress', ('primaryBackendIpAddress',)), + column_helper.Column( + 'id', + ('id',)), + column_helper.Column( + 'hostname', + ('hostname',)), + column_helper.Column( + 'type', + ('type',)), + column_helper.Column( + 'primaryBackendIpAddress', + ('primaryBackendIpAddress',)), + column_helper.Column( + 'hostIqn', + ('allowedHost', 'name',)), + column_helper.Column( + 'username', + ('allowedHost', 'credential', 'username',)), + column_helper.Column( + 'password', + ('allowedHost', 'credential', 'password',)), ] DEFAULT_COLUMNS = [ @@ -20,6 +37,9 @@ 'hostname', 'type', 'primaryBackendIpAddress', + 'hostIqn', + 'username', + 'password', ] @@ -35,7 +55,8 @@ def cli(env, columns, sortby, volume_id): """List block storage.""" block_manager = SoftLayer.BlockStorageManager(env.client) - access_list = block_manager.get_block_volume_access_list(volume_id=volume_id) + access_list = block_manager.get_block_volume_access_list( + volume_id=volume_id) table = formatting.Table(columns.columns) table.sortby = sortby 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 index 66b97e1ed..fb338ec67 100644 --- a/SoftLayer/CLI/block/detail.py +++ b/SoftLayer/CLI/block/detail.py @@ -1,13 +1,12 @@ """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 -import click - @click.command() @click.argument('volume_id') @@ -30,25 +29,28 @@ def cli(env, volume_id): ['Type', block_volume['storageType']['keyName'].split('_').pop(0)]) table.add_row( ['Capacity (GB)', "%iGB" % block_volume['capacityGb']]) + table.add_row( + ['LUN Id', "%s" % block_volume['lunId']]) - if block_volume['storageType']['keyName'].split('_').pop(0) == 'PERFORMANCE': + if block_volume['storageType']['keyName'].split('_').pop(0)\ + == 'PERFORMANCE': table.add_row( ['IOPs', block_volume['iops']]) if block_volume['storageType']['keyName'].split('_').pop(0) == 'ENDURANCE': table.add_row( - ['Endurance Tier', block_volume['storageTierLevel']['description']]) + ['Endurance Tier', + block_volume['storageTierLevel']['description']]) table.add_row( ['Data Center', block_volume['serviceResource']['datacenter']['name']]) table.add_row( - ['Bytes Used', block_volume['bytesUsed']]) - table.add_row( - ['IP', block_volume['serviceResourceBackendIpAddress']]) + ['Target IP', block_volume['serviceResourceBackendIpAddress']]) if block_volume['snapshotCapacityGb']: table.add_row( - ['Snapshot Reserved (GB)', block_volume['snapshotCapacityGb']]) + ['Snapshot Capacity (GB)', block_volume['snapshotCapacityGb']]) table.add_row( - ['Snapshot Used (Byes)', block_volume['snapshotSizeBytes']]) + ['Snapshot Used (Bytes)', + block_volume['parentVolume']['snapshotSizeBytes']]) env.fout(table) diff --git a/SoftLayer/CLI/block/list.py b/SoftLayer/CLI/block/list.py index 80c669d6b..e6f6464bc 100644 --- a/SoftLayer/CLI/block/list.py +++ b/SoftLayer/CLI/block/list.py @@ -1,12 +1,12 @@ """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 -import click COLUMNS = [ column_helper.Column('id', ('id',)), @@ -33,7 +33,7 @@ @click.command() @click.option('--username', '-u', help='Volume username') @click.option('--datacenter', '-d', help='Datacenter shortname') -@click.option('--storage_type', +@click.option('--storage-type', help='Type of storage volume', type=click.Choice(['performance', 'endurance'])) @click.option('--sortby', help='Column to sort by', default='username') @@ -47,9 +47,9 @@ 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()) + username=username, + storage_type=storage_type, + mask=columns.mask()) table = formatting.Table(columns.columns) table.sortby = sortby diff --git a/SoftLayer/CLI/block/order.py b/SoftLayer/CLI/block/order.py index ac2247334..44f8185d8 100644 --- a/SoftLayer/CLI/block/order.py +++ b/SoftLayer/CLI/block/order.py @@ -1,26 +1,34 @@ """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 -import click -@click.command() -@click.option('--storage_type', +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', - help='Size of storage volume', + type=int, + help='Size of storage volume in GB', required=True) @click.option('--iops', - help='Performance Storage 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)', + help='Endurance Storage Tier (IOP per GB)' + + ' [required for storage-type endurance]', type=click.Choice(['0.25', '2', '4'])) -@click.option('--os', +@click.option('--os-type', help='Operating System', type=click.Choice([ 'HYPER_V', @@ -32,42 +40,54 @@ 'XEN']), required=True) @click.option('--location', - help='Size of storage volume', + help='Datacenter short name (e.g.: dal09)', required=True) @environment.pass_env -def cli(env, storage_type, size, iops, tier, os, location): +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') + 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' + ) order = block_manager.order_block_volume( storage_type='performance_storage_iscsi', location=location, size=size, iops=iops, - os_type=os + os_type=os_type ) if storage_type == 'endurance': if tier is None: - raise exceptions.CLIAbort('Option --tier required with performance') + raise exceptions.CLIAbort( + 'Option --tier required with Endurance in IOPS/GB [0.25,2,4]') order = block_manager.order_block_volume( storage_type='storage_service_enterprise', location=location, size=size, tier_level=tier, - os_type=os + os_type=os_type ) if 'placedOrder' in order.keys(): - print "Order #{0} placed successfully!".format( - order['placedOrder']['id']) + click.echo("Order #{0} placed successfully!".format( + order['placedOrder']['id'])) for item in order['placedOrder']['items']: - print " > %s" % item['description'] + click.echo(" > %s" % item['description']) else: - print "Order could not be placed! Please verify your options " \ - "and try again." + 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 index d7551987e..d8a74c4c9 100644 --- a/SoftLayer/CLI/block/snapshot/list.py +++ b/SoftLayer/CLI/block/snapshot/list.py @@ -1,12 +1,12 @@ """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 -import click COLUMNS = [ column_helper.Column('id', ('id',)), diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index a3fdd8e9b..5f2fc6eef 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -59,8 +59,10 @@ ('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'), diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index a1fde3c6a..8bf4c333e 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -509,3 +509,22 @@ 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' +}] diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage.py b/SoftLayer/fixtures/SoftLayer_Network_Storage.py new file mode 100644 index 000000000..8b8995a29 --- /dev/null +++ b/SoftLayer/fixtures/SoftLayer_Network_Storage.py @@ -0,0 +1,20 @@ +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, + 'serviceResource': {'datacenter': {'id': 449500}}, + 'serviceResourceBackendIpAddress': '10.1.2.3', + 'serviceResourceName': 'Storage Type 01 Aggregate staaspar0101_pc01', + 'username': 'username' +} + +deleteObject = True diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py index e64f22b42..f21ac7459 100644 --- a/SoftLayer/managers/block.py +++ b/SoftLayer/managers/block.py @@ -22,9 +22,9 @@ def __init__(self, client): self.product_order = self.client['Product_Order'] def list_block_volumes(self, datacenter=None, username=None, - storage_type=None, **kwargs): - """ - Returns a list of block volumes. + 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 @@ -42,32 +42,31 @@ def list_block_volumes(self, datacenter=None, username=None, ] kwargs['mask'] = "mask[%s]" % ','.join(items) - call = 'getNetworkStorage' - _filter = utils.NestedDict(kwargs.get('filter') or {}) - _filter['networkStorage']['storageType']['keyName'] = ( - utils.query_filter('*=BLOCK_STORAGE')) + _filter['iscsiNetworkStorage']['serviceResource']['type']['type'] = \ + (utils.query_filter('!~ ISCSI')) + + _filter['iscsiNetworkStorage']['storageType']['keyName'] = ( + utils.query_filter('*BLOCK_STORAGE')) if storage_type: - _filter['networkStorage']['storageType']['keyName'] = ( + _filter['iscsiNetworkStorage']['storageType']['keyName'] = ( utils.query_filter('%s_BLOCK_STORAGE' % storage_type.upper())) if datacenter: - _filter['networkStorage']['serviceResource']['datacenter'][ - 'name'] = ( - utils.query_filter(datacenter)) + _filter['iscsiNetworkStorage']['serviceResource']['datacenter'][ + 'name'] = (utils.query_filter(datacenter)) if username: - _filter['networkStorage']['username'] = ( - utils.query_filter(username)) + _filter['iscsiNetworkStorage']['username'] = \ + (utils.query_filter(username)) kwargs['filter'] = _filter.to_dict() - func = getattr(self.account, call) - return func(**kwargs) + return self.client.call('Account', 'getIscsiNetworkStorage', **kwargs) def get_block_volume_details(self, volume_id, **kwargs): - """ - Returns details about the specified volume. + """Returns details about the specified volume. + :param volume_id: ID of volume. :param kwargs: :return: Returns details about the specified volume. @@ -79,19 +78,21 @@ def get_block_volume_details(self, volume_id, **kwargs): 'username', 'password', 'capacityGb', - 'bytesUsed', + 'snapshotCapacityGb', + 'parentVolume.snapshotSizeBytes', 'storageType.keyName', 'serviceResource.datacenter[name]', 'serviceResourceBackendIpAddress', 'storageTierLevel', - 'iops' + 'iops', + 'lunId', ] kwargs['mask'] = "mask[%s]" % ','.join(items) return self.block_svc.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. + """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. @@ -99,17 +100,17 @@ def get_block_volume_access_list(self, volume_id, **kwargs): if 'mask' not in kwargs: items = [ 'id', - 'allowedVirtualGuests', - 'allowedHardware', - 'allowedSubnets', - 'allowedIpAddresses' + 'allowedVirtualGuests[allowedHost[credential]]', + 'allowedHardware[allowedHost[credential]]', + 'allowedSubnets[allowedHost[credential]]', + 'allowedIpAddresses[allowedHost[credential]]', ] kwargs['mask'] = "mask[%s]" % ','.join(items) return self.block_svc.getObject(id=volume_id, **kwargs) def get_block_volume_snapshot_list(self, volume_id, **kwargs): - """ - Returns a list of snapshots for the specified volume. + """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. @@ -126,21 +127,44 @@ def get_block_volume_snapshot_list(self, volume_id, **kwargs): kwargs['mask'] = "mask[%s]" % ','.join(items) return self.block_svc.getObject(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.block_svc.deleteObject(id=snapshot_id) + def order_block_volume(self, storage_type, location, size, os_type, - iops=None, tier_level=None, **kwargs): + iops=None, tier_level=None): + """Places an order for a block volume. + + :param storage_type: "Performance" or "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 package['name'] == 'Performance': - complexType = 'SoftLayer_Container_Product_Order_Network_PerformanceStorage_Iscsi' + complex_type = base_type_name + 'PerformanceStorage_Iscsi' prices = self._get_item_prices_performance(package, size, iops) - if package['name'] == 'Endurance': - complexType = 'SoftLayer_Container_Product_Order_Network_Storage_Enterprise' + elif package['name'] == 'Endurance': + complex_type = base_type_name + 'Storage_Enterprise' prices = self._get_item_prices_endurance(package, size, tier_level) - - location_id = self._get_location_id(location) + else: + raise exceptions.SoftLayerError( + "storage_type must be either Performance or Endurance") order = { - 'complexType': complexType, + 'complexType': complex_type, 'packageId': package['id'], 'osFormatType': {'keyName': os_type}, 'prices': prices, @@ -151,8 +175,8 @@ def order_block_volume(self, storage_type, location, size, os_type, return self.product_order.placeOrder(order) def _get_package(self, category_code, **kwargs): - """ - Returns a product packaged based on type of storage. + """Returns a product packaged based on type of storage. + :param category_code: Category code of product package. :param kwargs: :return: Returns a packaged based on type of storage. @@ -176,41 +200,53 @@ def _get_package(self, category_code, **kwargs): return func(**kwargs).pop() @staticmethod - def _get_item_prices_performance(package, size, iops, **kwargs): - """ - Returns a collection of prices for a performance storage order. + def _get_item_prices_performance(package, size, iops): + """Returns a collection of prices for performance storage. + :param package: Package object :param size: Size of volume in GB :param iops: IOPs of volume :param kwargs: - :return: Returns a collection of prices for a performance storage order. + :return: Returns a collection of prices for performance storage. """ + parent_category_code = 'performance_storage_iscsi' + space_category_code = 'performance_storage_space' + iops_category_code = 'performance_storage_iops' + + found_space_price = False + found_iops_price = False + prices = [] for item in package['items']: for price in item['prices']: for category in price['categories']: - if price['locationGroupId'] == '' \ - and (category[ - 'categoryCode'] == 'performance_storage_iscsi' - or (item['capacity'] == size - and category[ - 'categoryCode'] == 'performance_storage_space' - ) - or (item['capacity'] == iops - and category[ - 'categoryCode'] == 'performance_storage_iops' - and (price[ - 'capacityRestrictionMinimum'] <= size <= - price[ - 'capacityRestrictionMaximum']) - )): - prices.append(price) + if price['locationGroupId'] == '': + # Find the parent-level price object. + if category['categoryCode'] == parent_category_code: + prices.append(price) + # Find the valid space price object. + elif (category['categoryCode'] == space_category_code + and item['capacity'] == size): + prices.append(price) + found_space_price = True + # Find the valid iops price object. + elif (category['categoryCode'] == iops_category_code + and item['capacity'] == iops + and (price['capacityRestrictionMinimum'] <= size + <= price['capacityRestrictionMaximum'])): + prices.append(price) + found_iops_price = True + + if found_space_price is False or found_iops_price is False: + raise ValueError( + "No prices found for the requested size and iops.") + return prices @staticmethod - def _get_item_prices_endurance(package, size, tier_level, **kwargs): - """ - Returns a collection of prices for a endurance storage order. + def _get_item_prices_endurance(package, size, tier_level): + """Returns a collection of prices for a endurance storage order. + :param package: Package object :param size: Size of volume in GB :param iops: IOPs of volume @@ -224,33 +260,50 @@ def _get_item_prices_endurance(package, size, tier_level, **kwargs): '4': '300' } + endurance_parent_category_codes = [ + 'storage_service_enterprise', + 'storage_block', + ] + + space_category_code = 'performance_storage_space' + tier_category_code = 'storage_tier_level' + + found_space_price = False + found_iops_price = False + prices = [] for item in package['items']: for price in item['prices']: for category in price['categories']: - if price['locationGroupId'] == '' \ - and (category[ - 'categoryCode'] == 'storage_service_enterprise' - or category['categoryCode'] == 'storage_block' - or (category[ - 'categoryCode'] == 'performance_storage_space' - and price[ - 'capacityRestrictionMinimum'] == tiers.get( - tier_level) and item[ - 'capacity'] == size - ) - or (category[ - 'categoryCode'] == 'storage_tier_level' - and item['attributes'][0][ - 'value'] == tiers.get(tier_level) - ) - ): - prices.append(price) + # Only collect prices from valid location groups. + if price['locationGroupId'] == '': + # Find the parent-level price object. + if any(category['categoryCode'] in s + for s in endurance_parent_category_codes): + prices.append(price) + # Find the valid space price object. + elif (category['categoryCode'] == space_category_code + and price['capacityRestrictionMinimum'] == + tiers.get(tier_level) + and item['capacity'] == size): + prices.append(price) + found_space_price = True + # Find the valid tier price object. + elif (category['categoryCode'] == tier_category_code + and item['attributes'][0]['value'] == + tiers.get(tier_level)): + prices.append(price) + found_iops_price = True + + if found_space_price is False or found_iops_price is False: + raise ValueError( + "No prices found for the requested size and tier.") + return prices def _get_location_id(self, location): - """ - Returns location id + """Returns location id + :param location: Datacenter short name :return: Returns location id """ @@ -261,3 +314,24 @@ def _get_location_id(self, 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) diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py new file mode 100644 index 000000000..951028699 --- /dev/null +++ b/tests/managers/block_tests.py @@ -0,0 +1,61 @@ +""" + 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_access_list(100) + + self.assertEqual(fixtures.SoftLayer_Network_Storage.getObject, + result) + self.assert_called_with('SoftLayer_Network_Storage', 'getObject', + 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) From 653b0da59dab506f5625c090fefd94403e7f4c80 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Fri, 22 Apr 2016 18:12:29 -0500 Subject: [PATCH 11/16] Improves code quality of block storage ordering code --- SoftLayer/CLI/block/order.py | 40 +++--- SoftLayer/managers/block.py | 268 +++++++++++++++++++++-------------- 2 files changed, 186 insertions(+), 122 deletions(-) diff --git a/SoftLayer/CLI/block/order.py b/SoftLayer/CLI/block/order.py index 44f8185d8..db9073782 100644 --- a/SoftLayer/CLI/block/order.py +++ b/SoftLayer/CLI/block/order.py @@ -22,11 +22,11 @@ @click.option('--iops', type=int, help='Performance Storage IOPs,' - + ' between 100 and 6000 in multiples of 100' - + ' [required for storage-type performance]') + ' 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]', + ' [required for storage-type endurance]', type=click.Choice(['0.25', '2', '4'])) @click.option('--os-type', help='Operating System', @@ -62,26 +62,32 @@ def cli(env, storage_type, size, iops, tier, os_type, location): 'Option --iops must be a multiple of 100' ) - order = block_manager.order_block_volume( - storage_type='performance_storage_iscsi', - location=location, - size=size, - iops=iops, - os_type=os_type - ) + 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]') - order = block_manager.order_block_volume( - storage_type='storage_service_enterprise', - location=location, - size=size, - tier_level=tier, - os_type=os_type - ) + try: + order = block_manager.order_block_volume( + storage_type='storage_service_enterprise', + location=location, + size=size, + tier_level=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( diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py index f21ac7459..2c51e0286 100644 --- a/SoftLayer/managers/block.py +++ b/SoftLayer/managers/block.py @@ -9,6 +9,13 @@ from SoftLayer import utils +ENDURANCE_TIERS = { + '0.25': '100', + '2': '200', + '4': '300' +} + + class BlockStorageManager(utils.IdentifierMixin, object): """Manages Block Storage volumes.""" @@ -149,16 +156,27 @@ def order_block_volume(self, storage_type, location, size, os_type, 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)") + 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 package['name'] == 'Performance': complex_type = base_type_name + 'PerformanceStorage_Iscsi' - prices = self._get_item_prices_performance(package, size, iops) + prices = [ + _find_performance_block_price(package), + _find_performance_space_price(package, iops), + _find_performance_iops_price(package, size, iops), + ] elif package['name'] == 'Endurance': complex_type = base_type_name + 'Storage_Enterprise' - prices = self._get_item_prices_endurance(package, size, tier_level) + 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") @@ -199,108 +217,6 @@ def _get_package(self, category_code, **kwargs): func = getattr(self.product_package, 'getAllObjects') return func(**kwargs).pop() - @staticmethod - def _get_item_prices_performance(package, size, iops): - """Returns a collection of prices for performance storage. - - :param package: Package object - :param size: Size of volume in GB - :param iops: IOPs of volume - :param kwargs: - :return: Returns a collection of prices for performance storage. - """ - parent_category_code = 'performance_storage_iscsi' - space_category_code = 'performance_storage_space' - iops_category_code = 'performance_storage_iops' - - found_space_price = False - found_iops_price = False - - prices = [] - for item in package['items']: - for price in item['prices']: - for category in price['categories']: - if price['locationGroupId'] == '': - # Find the parent-level price object. - if category['categoryCode'] == parent_category_code: - prices.append(price) - # Find the valid space price object. - elif (category['categoryCode'] == space_category_code - and item['capacity'] == size): - prices.append(price) - found_space_price = True - # Find the valid iops price object. - elif (category['categoryCode'] == iops_category_code - and item['capacity'] == iops - and (price['capacityRestrictionMinimum'] <= size - <= price['capacityRestrictionMaximum'])): - prices.append(price) - found_iops_price = True - - if found_space_price is False or found_iops_price is False: - raise ValueError( - "No prices found for the requested size and iops.") - - return prices - - @staticmethod - def _get_item_prices_endurance(package, size, tier_level): - """Returns a collection of prices for a endurance storage order. - - :param package: Package object - :param size: Size of volume in GB - :param iops: IOPs of volume - :param kwargs: - :return: Returns a collection of prices for a endurance storage order. - """ - - tiers = { - '0.25': '100', - '2': '200', - '4': '300' - } - - endurance_parent_category_codes = [ - 'storage_service_enterprise', - 'storage_block', - ] - - space_category_code = 'performance_storage_space' - tier_category_code = 'storage_tier_level' - - found_space_price = False - found_iops_price = False - - prices = [] - for item in package['items']: - for price in item['prices']: - for category in price['categories']: - # Only collect prices from valid location groups. - if price['locationGroupId'] == '': - # Find the parent-level price object. - if any(category['categoryCode'] in s - for s in endurance_parent_category_codes): - prices.append(price) - # Find the valid space price object. - elif (category['categoryCode'] == space_category_code - and price['capacityRestrictionMinimum'] == - tiers.get(tier_level) - and item['capacity'] == size): - prices.append(price) - found_space_price = True - # Find the valid tier price object. - elif (category['categoryCode'] == tier_category_code - and item['attributes'][0]['value'] == - tiers.get(tier_level)): - prices.append(price) - found_iops_price = True - - if found_space_price is False or found_iops_price is False: - raise ValueError( - "No prices found for the requested size and tier.") - - return prices - def _get_location_id(self, location): """Returns location id @@ -335,3 +251,145 @@ def cancel_block_volume(self, volume_id, 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 price['capacityRestrictionMinimum'] != level: + 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['attributes']: + if 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 + ) From 47a55ef7aecb12ea55e7764c9783799966811a44 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Mon, 25 Apr 2016 16:23:18 -0500 Subject: [PATCH 12/16] Adds block storage ordering tests --- SoftLayer/CLI/block/order.py | 2 +- SoftLayer/managers/block.py | 89 +++++++++++--------- tests/managers/block_tests.py | 153 +++++++++++++++++++++++++++++++++- tox.ini | 7 +- 4 files changed, 203 insertions(+), 48 deletions(-) diff --git a/SoftLayer/CLI/block/order.py b/SoftLayer/CLI/block/order.py index db9073782..f1840078c 100644 --- a/SoftLayer/CLI/block/order.py +++ b/SoftLayer/CLI/block/order.py @@ -83,7 +83,7 @@ def cli(env, storage_type, size, iops, tier, os_type, location): storage_type='storage_service_enterprise', location=location, size=size, - tier_level=tier, + tier_level=float(tier), os_type=os_type ) except ValueError as ex: diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py index 2c51e0286..2c6134823 100644 --- a/SoftLayer/managers/block.py +++ b/SoftLayer/managers/block.py @@ -10,9 +10,9 @@ ENDURANCE_TIERS = { - '0.25': '100', - '2': '200', - '4': '300' + 0.25: 100, + 2: 200, + 4: 300, } @@ -22,11 +22,6 @@ class BlockStorageManager(utils.IdentifierMixin, object): def __init__(self, client): self.configuration = {} self.client = client - self.account = client['Account'] - self.product_package = self.client['Product_Package'] - self.block_svc = self.client['Network_Storage'] - self.block_os_types = self.client['Network_Storage_Iscsi_OS_Type'] - self.product_order = self.client['Product_Order'] def list_block_volumes(self, datacenter=None, username=None, storage_type=None, **kwargs): @@ -95,7 +90,8 @@ def get_block_volume_details(self, volume_id, **kwargs): 'lunId', ] kwargs['mask'] = "mask[%s]" % ','.join(items) - return self.block_svc.getObject(id=volume_id, **kwargs) + 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. @@ -113,7 +109,8 @@ def get_block_volume_access_list(self, volume_id, **kwargs): 'allowedIpAddresses[allowedHost[credential]]', ] kwargs['mask'] = "mask[%s]" % ','.join(items) - return self.block_svc.getObject(id=volume_id, **kwargs) + 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. @@ -123,29 +120,34 @@ def get_block_volume_snapshot_list(self, volume_id, **kwargs): :return: Returns a list of snapshots for the specified volume. """ if 'mask' not in kwargs: - items = [ - 'snapshots.id', - 'snapshots.notes', - 'snapshots.snapshotSizeBytes', - 'snapshots.storageType.keyName', - 'snapshots.snapshotCreationTimestamp', - 'snapshots[hourlySchedule,dailySchedule,weeklySchedule]', - ] + items = '''snapshots[ + id, + notes, + snapshotSizeBytes, + storageType[keyName], + snapshotCreationTimestamp + hourlySchedule, + dailySchedule, + weeklySchedule +]''' kwargs['mask'] = "mask[%s]" % ','.join(items) - return self.block_svc.getObject(id=volume_id, **kwargs) + return self.client.call('Network_Storage', 'getObject', + 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.block_svc.deleteObject(id=snapshot_id) + 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" or "Endurance" + :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 @@ -162,14 +164,14 @@ def order_block_volume(self, storage_type, location, size, os_type, base_type_name = 'SoftLayer_Container_Product_Order_Network_' package = self._get_package(storage_type) - if package['name'] == 'Performance': + 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 package['name'] == 'Endurance': + elif storage_type == 'storage_service_enterprise': complex_type = base_type_name + 'Storage_Enterprise' prices = [ _find_endurance_block_price(package), @@ -190,32 +192,34 @@ def order_block_volume(self, storage_type, location, size, os_type, 'location': location_id, } - return self.product_order.placeOrder(order) + return self.client.call('Product_Order', 'placeOrder', order) - def _get_package(self, category_code, **kwargs): + def _get_package(self, category_code): """Returns a product packaged based on type of storage. :param category_code: Category code of product package. - :param kwargs: :return: Returns a packaged based on type of storage. """ - if 'mask' not in kwargs: - items = [ - 'id', - 'name', - 'items', - 'items[prices[categories],attributes]' - ] - kwargs['mask'] = "mask[%s]" % ','.join(items) - _filter = utils.NestedDict(kwargs.get('filter') or {}) + _filter = utils.NestedDict({}) _filter['categories']['categoryCode'] = ( utils.query_filter(category_code)) _filter['statusCode'] = (utils.query_filter('ACTIVE')) - kwargs['filter'] = _filter.to_dict() - func = getattr(self.product_package, 'getAllObjects') - return func(**kwargs).pop() + 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 @@ -297,7 +301,10 @@ def _find_endurance_space_price(package, size, tier_level): continue level = ENDURANCE_TIERS.get(tier_level) - if price['capacityRestrictionMinimum'] != level: + if level < int(price['capacityRestrictionMinimum']): + continue + + if level > int(price['capacityRestrictionMaximum']): continue return price @@ -307,8 +314,8 @@ def _find_endurance_space_price(package, size, tier_level): def _find_endurance_tier_price(package, tier_level): for item in package['items']: - for attribute in item['attributes']: - if attribute['value'] != ENDURANCE_TIERS.get(tier_level): + for attribute in item.get('attributes', []): + if int(attribute['value']) == ENDURANCE_TIERS.get(tier_level): break else: continue diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py index 951028699..a2fb7585a 100644 --- a/tests/managers/block_tests.py +++ b/tests/managers/block_tests.py @@ -45,7 +45,7 @@ def test_get_block_volume_access_list(self): identifier=100) def test_get_block_volume_snapshot_list(self): - result = self.block.get_block_volume_access_list(100) + result = self.block.get_block_volume_snapshot_list(100) self.assertEqual(fixtures.SoftLayer_Network_Storage.getObject, result) @@ -59,3 +59,154 @@ def test_delete_snapshot(self): 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/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 \ From 231f840961b11a0f57f30a8fced0588c8c3f1d7c Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Tue, 26 Apr 2016 10:24:47 -0500 Subject: [PATCH 13/16] Adds tests for block access-list, volume-cancel, volume-detail, volume-list --- SoftLayer/CLI/block/access/__init__.py | 1 - SoftLayer/CLI/block/access/list.py | 114 ---------------- SoftLayer/CLI/block/access_list.py | 123 ++++++++++++++++++ SoftLayer/CLI/block/detail.py | 61 ++++----- SoftLayer/CLI/block/list.py | 21 +-- SoftLayer/CLI/routes.py | 2 +- SoftLayer/fixtures/SoftLayer_Account.py | 3 +- .../fixtures/SoftLayer_Network_Storage.py | 49 ++++++- tests/CLI/modules/block_tests.py | 98 ++++++++++++++ 9 files changed, 315 insertions(+), 157 deletions(-) delete mode 100644 SoftLayer/CLI/block/access/__init__.py delete mode 100644 SoftLayer/CLI/block/access/list.py create mode 100644 SoftLayer/CLI/block/access_list.py create mode 100644 tests/CLI/modules/block_tests.py diff --git a/SoftLayer/CLI/block/access/__init__.py b/SoftLayer/CLI/block/access/__init__.py deleted file mode 100644 index 050b3938d..000000000 --- a/SoftLayer/CLI/block/access/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Block Storage Access Control.""" diff --git a/SoftLayer/CLI/block/access/list.py b/SoftLayer/CLI/block/access/list.py deleted file mode 100644 index 4450f9ab6..000000000 --- a/SoftLayer/CLI/block/access/list.py +++ /dev/null @@ -1,114 +0,0 @@ -"""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 - - -COLUMNS = [ - column_helper.Column( - 'id', - ('id',)), - column_helper.Column( - 'hostname', - ('hostname',)), - column_helper.Column( - 'type', - ('type',)), - column_helper.Column( - 'primaryBackendIpAddress', - ('primaryBackendIpAddress',)), - column_helper.Column( - 'hostIqn', - ('allowedHost', 'name',)), - column_helper.Column( - 'username', - ('allowedHost', 'credential', 'username',)), - column_helper.Column( - 'password', - ('allowedHost', 'credential', 'password',)), -] - -DEFAULT_COLUMNS = [ - 'id', - 'hostname', - 'type', - 'primaryBackendIpAddress', - 'hostIqn', - 'username', - 'password', -] - - -@click.command() -@click.argument('volume_id') -@click.option('--sortby', help='Column to sort by', default='hostname') -@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 block storage.""" - 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 - - if access_list: - if 'allowedVirtualGuests' in access_list.keys(): - for host in access_list['allowedVirtualGuests']: - host['type'] = 'VIRTUAL' - host['hostname'] = "{0}.{1}".format( - host['hostname'], - host['domain']) - table.add_row([value or formatting.blank() - for value in columns.row(host)]) - - if 'allowedHardware' in access_list.keys(): - for host in access_list['allowedHardware']: - host['type'] = 'HARDWARE' - host['hostname'] = "{0}.{1}".format( - host['hostname'], - host['domain']) - table.add_row([value or formatting.blank() - for value in columns.row(host)]) - - if 'allowedSubnets' in access_list.keys(): - for host in access_list['allowedSubnets']: - host['type'] = 'SUBNET' - if 'note' in host.keys(): - host['hostname'] = "{0}/{1} ({3})".format( - host['networkIdentifier'], - host['cidr'], - host['note'], - ) - else: - host['hostname'] = "{0}/{1}".format( - host['networkIdentifier'], - host['cidr'] - ) - table.add_row([value or formatting.blank() - for value in columns.row(host)]) - - if 'allowedIpAddresses' in access_list.keys(): - for host in access_list['allowedIpAddresses']: - host['type'] = 'IP' - if 'note' in host.keys(): - host['hostname'] = "{0} ({1})".format( - host['ipAddress'], - host['note'] - ) - else: - host['hostname'] = host['ipAddress'] - table.add_row([value or formatting.blank() - for value in columns.row(host)]) - - env.fout(table) - else: - click.echo("No authorized hosts found.") diff --git a/SoftLayer/CLI/block/access_list.py b/SoftLayer/CLI/block/access_list.py new file mode 100644 index 000000000..58de7e625 --- /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( + 'primaryBackendIpAddress', + ('primaryBackendIpAddress',), + """ +allowedVirtualGuests.primaryBackendIpAddress +allowedHardware.primaryBackendIpAddress +allowedSubnets.primaryBackendIpAddress +allowedIpAddresses.primaryBackendIpAddress +"""), + column_helper.Column( + 'hostIqn', + ('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', + 'primaryBackendIpAddress', + 'hostIqn', + '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 block storage.""" + 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/detail.py b/SoftLayer/CLI/block/detail.py index fb338ec67..c98736ed9 100644 --- a/SoftLayer/CLI/block/detail.py +++ b/SoftLayer/CLI/block/detail.py @@ -21,36 +21,39 @@ def cli(env, volume_id): table.align['Name'] = 'r' table.align['Value'] = 'l' - table.add_row( - ['ID', block_volume['id']]) - table.add_row( - ['Username', block_volume['username']]) - table.add_row( - ['Type', block_volume['storageType']['keyName'].split('_').pop(0)]) - table.add_row( - ['Capacity (GB)', "%iGB" % block_volume['capacityGb']]) - table.add_row( - ['LUN Id', "%s" % block_volume['lunId']]) - - if block_volume['storageType']['keyName'].split('_').pop(0)\ - == 'PERFORMANCE': - table.add_row( - ['IOPs', block_volume['iops']]) - if block_volume['storageType']['keyName'].split('_').pop(0) == 'ENDURANCE': - 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']]) + 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']]) + 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 index e6f6464bc..069983a74 100644 --- a/SoftLayer/CLI/block/list.py +++ b/SoftLayer/CLI/block/list.py @@ -9,14 +9,19 @@ COLUMNS = [ - column_helper.Column('id', ('id',)), - column_helper.Column('username', ('username',)), + column_helper.Column('id', ('id',), mask="id"), + column_helper.Column('username', ('username',), mask="username"), column_helper.Column('datacenter', - ('serviceResource', 'datacenter', 'name')), - column_helper.Column('storageType', ('storageType', 'keyName')), - column_helper.Column('capacityGb', ('capacityGb',)), - column_helper.Column('bytesUsed', ('bytesUsed',)), - column_helper.Column('ipAddr', ('serviceResourceBackendIpAddress',)), + ('serviceResource', 'datacenter', 'name'), + mask="serviceResource.datacenter.name"), + column_helper.Column( + 'storageType', + lambda b: b['storageType']['keyName'].split('_').pop(0), + mask="storageType.keyName"), + column_helper.Column('capacityGb', ('capacityGb',), mask="capacityGb"), + column_helper.Column('bytesUsed', ('bytesUsed',), mask="bytesUsed"), + column_helper.Column('ipAddr', ('serviceResourceBackendIpAddress',), + mask="serviceResourceBackendIpAddress"), ] DEFAULT_COLUMNS = [ @@ -55,8 +60,6 @@ def cli(env, sortby, columns, datacenter, username, storage_type): table.sortby = sortby for block_volume in block_volumes: - block_volume['storageType']['keyName'] = block_volume['storageType'][ - 'keyName'].split('_').pop(0) table.add_row([value or formatting.blank() for value in columns.row(block_volume)]) diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py index 5f2fc6eef..dfbd76805 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -63,7 +63,7 @@ ('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'), + ('block:access-list', 'SoftLayer.CLI.block.access_list:cli'), ('firewall', 'SoftLayer.CLI.firewall'), ('firewall:add', 'SoftLayer.CLI.firewall.add:cli'), diff --git a/SoftLayer/fixtures/SoftLayer_Account.py b/SoftLayer/fixtures/SoftLayer_Account.py index 8bf4c333e..b03c26d00 100644 --- a/SoftLayer/fixtures/SoftLayer_Account.py +++ b/SoftLayer/fixtures/SoftLayer_Account.py @@ -526,5 +526,6 @@ 'serviceResource': {'datacenter': {'id': 449500}}, 'serviceResourceBackendIpAddress': '10.1.2.3', 'serviceResourceName': 'Storage Type 01 Aggregate staaspar0101_pc01', - 'username': 'username' + 'username': 'username', + 'storageType': {'keyName': 'ENDURANCE_STORAGE'}, }] diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage.py b/SoftLayer/fixtures/SoftLayer_Network_Storage.py index 8b8995a29..4b5559338 100644 --- a/SoftLayer/fixtures/SoftLayer_Network_Storage.py +++ b/SoftLayer/fixtures/SoftLayer_Network_Storage.py @@ -11,10 +11,55 @@ 'notes': """{'status': 'available'}""", 'password': '', 'serviceProviderId': 1, - 'serviceResource': {'datacenter': {'id': 449500}}, + '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' + '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'}, + }, + }], } deleteObject = True diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py new file mode 100644 index 000000000..d115d4fa5 --- /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.assertEqual(result.exit_code, 0) + self.assertEqual([ + { + 'username': 'joe', + 'name': 'test-server.example.com', + 'type': 'VIRTUAL', + 'hostIqn': 'test-server', + 'password': '12345', + 'primaryBackendIpAddress': '10.0.0.1', + 'id': 1234, + }, + { + 'username': 'joe', + 'name': 'test-server.example.com', + 'type': 'HARDWARE', + 'hostIqn': 'test-server', + 'password': '12345', + 'primaryBackendIpAddress': '10.0.0.2', + 'id': 1234, + }, + { + 'username': 'joe', + 'name': '10.0.0.1/24 (backend subnet)', + 'type': 'SUBNET', + 'hostIqn': 'test-server', + 'password': '12345', + 'primaryBackendIpAddress': None, + 'id': 1234, + }, + { + 'username': 'joe', + 'name': '10.0.0.1 (backend ip)', + 'type': 'IP', + 'hostIqn': 'test-server', + 'password': '12345', + 'primaryBackendIpAddress': None, + 'id': 1234, + }], + json.loads(result.output),) + + def test_volume_cancel(self): + result = self.run_command([ + '--really', 'block', 'volume-cancel', '1234']) + + self.assertEqual(result.exit_code, 0) + 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.assertEqual(result.exit_code, 0) + 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.assertEqual(result.exit_code, 0) + self.assertEqual([ + { + 'bytesUsed': None, + 'capacityGb': 20, + 'datacenter': None, + 'id': 100, + 'ipAddr': '10.1.2.3', + 'storageType': 'ENDURANCE', + 'username': 'username' + }], + json.loads(result.output)) From 2bf9320737195641918d2ce6593b0f837efae4d6 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 4 May 2016 15:49:26 -0500 Subject: [PATCH 14/16] Address review concerns --- SoftLayer/CLI/block/access_list.py | 10 ++-- SoftLayer/CLI/block/list.py | 16 ++--- SoftLayer/CLI/block/snapshot/__init__.py | 1 - SoftLayer/CLI/block/snapshot/list.py | 56 ------------------ .../delete.py => snapshot_delete.py} | 0 SoftLayer/CLI/block/snapshot_list.py | 58 +++++++++++++++++++ SoftLayer/CLI/routes.py | 4 +- .../fixtures/SoftLayer_Network_Storage.py | 2 +- SoftLayer/managers/block.py | 11 ++-- tests/CLI/modules/block_tests.py | 24 ++++---- tests/managers/block_tests.py | 4 +- 11 files changed, 94 insertions(+), 92 deletions(-) delete mode 100644 SoftLayer/CLI/block/snapshot/__init__.py delete mode 100644 SoftLayer/CLI/block/snapshot/list.py rename SoftLayer/CLI/block/{snapshot/delete.py => snapshot_delete.py} (100%) create mode 100644 SoftLayer/CLI/block/snapshot_list.py diff --git a/SoftLayer/CLI/block/access_list.py b/SoftLayer/CLI/block/access_list.py index 58de7e625..4b45a54b7 100644 --- a/SoftLayer/CLI/block/access_list.py +++ b/SoftLayer/CLI/block/access_list.py @@ -45,7 +45,7 @@ def _format_name(obj): """), column_helper.Column('type', ('type',)), column_helper.Column( - 'primaryBackendIpAddress', + 'private_ip_address', ('primaryBackendIpAddress',), """ allowedVirtualGuests.primaryBackendIpAddress @@ -54,7 +54,7 @@ def _format_name(obj): allowedIpAddresses.primaryBackendIpAddress """), column_helper.Column( - 'hostIqn', + 'host_iqn', ('allowedHost', 'name',), """ allowedVirtualGuests.allowedHost.name @@ -87,8 +87,8 @@ def _format_name(obj): 'id', 'name', 'type', - 'primaryBackendIpAddress', - 'hostIqn', + 'private_ip_address', + 'host_iqn', 'username', 'password', ] @@ -104,7 +104,7 @@ def _format_name(obj): default=','.join(DEFAULT_COLUMNS)) @environment.pass_env def cli(env, columns, sortby, volume_id): - """List block storage.""" + """List ACLs.""" block_manager = SoftLayer.BlockStorageManager(env.client) access_list = block_manager.get_block_volume_access_list( volume_id=volume_id) diff --git a/SoftLayer/CLI/block/list.py b/SoftLayer/CLI/block/list.py index 069983a74..40edcb7c8 100644 --- a/SoftLayer/CLI/block/list.py +++ b/SoftLayer/CLI/block/list.py @@ -15,12 +15,12 @@ ('serviceResource', 'datacenter', 'name'), mask="serviceResource.datacenter.name"), column_helper.Column( - 'storageType', + 'storage_type', lambda b: b['storageType']['keyName'].split('_').pop(0), mask="storageType.keyName"), - column_helper.Column('capacityGb', ('capacityGb',), mask="capacityGb"), - column_helper.Column('bytesUsed', ('bytesUsed',), mask="bytesUsed"), - column_helper.Column('ipAddr', ('serviceResourceBackendIpAddress',), + column_helper.Column('capacity_gb', ('capacityGb',), mask="capacityGb"), + column_helper.Column('bytes_used', ('bytesUsed',), mask="bytesUsed"), + column_helper.Column('ip_addr', ('serviceResourceBackendIpAddress',), mask="serviceResourceBackendIpAddress"), ] @@ -28,10 +28,10 @@ 'id', 'username', 'datacenter', - 'storageType', - 'capacityGb', - 'bytesUsed', - 'ipAddr' + 'storage_type', + 'capacity_gb', + 'bytes_used', + 'ip_addr' ] diff --git a/SoftLayer/CLI/block/snapshot/__init__.py b/SoftLayer/CLI/block/snapshot/__init__.py deleted file mode 100644 index 4b84f0814..000000000 --- a/SoftLayer/CLI/block/snapshot/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Block Storage Snapshots.""" diff --git a/SoftLayer/CLI/block/snapshot/list.py b/SoftLayer/CLI/block/snapshot/list.py deleted file mode 100644 index d8a74c4c9..000000000 --- a/SoftLayer/CLI/block/snapshot/list.py +++ /dev/null @@ -1,56 +0,0 @@ -"""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', ('id',)), - column_helper.Column('name', ('name',)), - column_helper.Column('snapshotCreationTimestamp', - ('snapshotCreationTimestamp',)), - column_helper.Column('snapshotSizeBytes', ('snapshotSizeBytes',)), -] - -DEFAULT_COLUMNS = [ - 'id', - 'name', - 'snapshotCreationTimestamp', - 'snapshotSizeBytes' -] - - -@click.command() -@click.argument('volume_id') -@click.option('--sortby', help='Column to sort by', - default='snapshotCreationTimestamp') -@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.""" - block_manager = SoftLayer.BlockStorageManager(env.client) - volume = block_manager.get_block_volume_snapshot_list(volume_id=volume_id) - - table = formatting.Table(columns.columns) - table.sortby = sortby - - if volume and 'snapshots' in volume.keys(): - for snapshot in volume['snapshots']: - - if 'notes' in snapshot.keys(): - snapshot['name'] = snapshot['notes'] - - table.add_row([value or formatting.blank() - for value in columns.row(snapshot)]) - - env.fout(table) - else: - click.echo("No snapshots found.") diff --git a/SoftLayer/CLI/block/snapshot/delete.py b/SoftLayer/CLI/block/snapshot_delete.py similarity index 100% rename from SoftLayer/CLI/block/snapshot/delete.py rename to SoftLayer/CLI/block/snapshot_delete.py 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 dfbd76805..a9aec3c95 100644 --- a/SoftLayer/CLI/routes.py +++ b/SoftLayer/CLI/routes.py @@ -61,8 +61,8 @@ ('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: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'), diff --git a/SoftLayer/fixtures/SoftLayer_Network_Storage.py b/SoftLayer/fixtures/SoftLayer_Network_Storage.py index 4b5559338..0257b1678 100644 --- a/SoftLayer/fixtures/SoftLayer_Network_Storage.py +++ b/SoftLayer/fixtures/SoftLayer_Network_Storage.py @@ -61,5 +61,5 @@ }, }], } - +getSnapshots = [] deleteObject = True diff --git a/SoftLayer/managers/block.py b/SoftLayer/managers/block.py index 2c6134823..906496ee2 100644 --- a/SoftLayer/managers/block.py +++ b/SoftLayer/managers/block.py @@ -42,7 +42,7 @@ def list_block_volumes(self, datacenter=None, username=None, 'serviceResource.datacenter[name]', 'serviceResourceBackendIpAddress' ] - kwargs['mask'] = "mask[%s]" % ','.join(items) + kwargs['mask'] = ','.join(items) _filter = utils.NestedDict(kwargs.get('filter') or {}) @@ -89,7 +89,7 @@ def get_block_volume_details(self, volume_id, **kwargs): 'iops', 'lunId', ] - kwargs['mask'] = "mask[%s]" % ','.join(items) + kwargs['mask'] = ','.join(items) return self.client.call('Network_Storage', 'getObject', id=volume_id, **kwargs) @@ -108,7 +108,7 @@ def get_block_volume_access_list(self, volume_id, **kwargs): 'allowedSubnets[allowedHost[credential]]', 'allowedIpAddresses[allowedHost[credential]]', ] - kwargs['mask'] = "mask[%s]" % ','.join(items) + kwargs['mask'] = ','.join(items) return self.client.call('Network_Storage', 'getObject', id=volume_id, **kwargs) @@ -130,8 +130,9 @@ def get_block_volume_snapshot_list(self, volume_id, **kwargs): dailySchedule, weeklySchedule ]''' - kwargs['mask'] = "mask[%s]" % ','.join(items) - return self.client.call('Network_Storage', 'getObject', + kwargs['mask'] = ','.join(items) + + return self.client.call('Network_Storage', 'getSnapshots', id=volume_id, **kwargs) def delete_snapshot(self, snapshot_id): diff --git a/tests/CLI/modules/block_tests.py b/tests/CLI/modules/block_tests.py index d115d4fa5..49368e143 100644 --- a/tests/CLI/modules/block_tests.py +++ b/tests/CLI/modules/block_tests.py @@ -20,36 +20,36 @@ def test_access_list(self): 'username': 'joe', 'name': 'test-server.example.com', 'type': 'VIRTUAL', - 'hostIqn': 'test-server', + 'host_iqn': 'test-server', 'password': '12345', - 'primaryBackendIpAddress': '10.0.0.1', + 'private_ip_address': '10.0.0.1', 'id': 1234, }, { 'username': 'joe', 'name': 'test-server.example.com', 'type': 'HARDWARE', - 'hostIqn': 'test-server', + 'host_iqn': 'test-server', 'password': '12345', - 'primaryBackendIpAddress': '10.0.0.2', + 'private_ip_address': '10.0.0.2', 'id': 1234, }, { 'username': 'joe', 'name': '10.0.0.1/24 (backend subnet)', 'type': 'SUBNET', - 'hostIqn': 'test-server', + 'host_iqn': 'test-server', 'password': '12345', - 'primaryBackendIpAddress': None, + 'private_ip_address': None, 'id': 1234, }, { 'username': 'joe', 'name': '10.0.0.1 (backend ip)', 'type': 'IP', - 'hostIqn': 'test-server', + 'host_iqn': 'test-server', 'password': '12345', - 'primaryBackendIpAddress': None, + 'private_ip_address': None, 'id': 1234, }], json.loads(result.output),) @@ -87,12 +87,12 @@ def test_volume_list(self): self.assertEqual(result.exit_code, 0) self.assertEqual([ { - 'bytesUsed': None, - 'capacityGb': 20, + 'bytes_used': None, + 'capacity_gb': 20, 'datacenter': None, 'id': 100, - 'ipAddr': '10.1.2.3', - 'storageType': 'ENDURANCE', + 'ip_addr': '10.1.2.3', + 'storage_type': 'ENDURANCE', 'username': 'username' }], json.loads(result.output)) diff --git a/tests/managers/block_tests.py b/tests/managers/block_tests.py index a2fb7585a..a6b6f866a 100644 --- a/tests/managers/block_tests.py +++ b/tests/managers/block_tests.py @@ -47,9 +47,9 @@ def test_get_block_volume_access_list(self): def test_get_block_volume_snapshot_list(self): result = self.block.get_block_volume_snapshot_list(100) - self.assertEqual(fixtures.SoftLayer_Network_Storage.getObject, + self.assertEqual(fixtures.SoftLayer_Network_Storage.getSnapshots, result) - self.assert_called_with('SoftLayer_Network_Storage', 'getObject', + self.assert_called_with('SoftLayer_Network_Storage', 'getSnapshots', identifier=100) def test_delete_snapshot(self): From b832bfa3c6a99c6bc2ee417e1cfcb0d2b941a528 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 4 May 2016 16:09:13 -0500 Subject: [PATCH 15/16] Version bump to 5.1.0 --- CHANGELOG | 10 ++++++++++ SoftLayer/consts.py | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) 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/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/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/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.', From def654a3bc8cff0c535d37e2b0e8b010f3eab145 Mon Sep 17 00:00:00 2001 From: Kevin McDonald Date: Wed, 11 May 2016 22:37:38 -0500 Subject: [PATCH 16/16] Give better assertion errors when CLI commands fail in tests (#725) --- SoftLayer/testing/__init__.py | 7 +++++ tests/CLI/core_tests.py | 6 ++-- tests/CLI/modules/block_tests.py | 8 ++--- tests/CLI/modules/call_api_tests.py | 16 +++++----- tests/CLI/modules/cdn_tests.py | 14 ++++----- tests/CLI/modules/config_tests.py | 4 +-- tests/CLI/modules/dns_tests.py | 16 +++++----- tests/CLI/modules/firewall_tests.py | 2 +- tests/CLI/modules/globalip_tests.py | 8 ++--- tests/CLI/modules/nas_tests.py | 2 +- tests/CLI/modules/object_storage_tests.py | 4 +-- tests/CLI/modules/rwhois_tests.py | 6 ++-- tests/CLI/modules/server_tests.py | 38 +++++++++++------------ tests/CLI/modules/sshkey_tests.py | 14 ++++----- tests/CLI/modules/summary_tests.py | 2 +- tests/CLI/modules/ticket_tests.py | 20 ++++++------ tests/CLI/modules/vs_tests.py | 26 ++++++++-------- 17 files changed, 100 insertions(+), 93 deletions(-) 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/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 index 49368e143..98f13f445 100644 --- a/tests/CLI/modules/block_tests.py +++ b/tests/CLI/modules/block_tests.py @@ -14,7 +14,7 @@ class BlockTests(testing.TestCase): def test_access_list(self): result = self.run_command(['block', 'access-list', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual([ { 'username': 'joe', @@ -58,7 +58,7 @@ def test_volume_cancel(self): result = self.run_command([ '--really', 'block', 'volume-cancel', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual("", result.output) self.assert_called_with('SoftLayer_Billing_Item', 'cancelItem', args=(False, True, None)) @@ -66,7 +66,7 @@ def test_volume_cancel(self): def test_volume_detail(self): result = self.run_command(['block', 'volume-detail', '1234']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual({ 'Username': 'username', 'LUN Id': '2', @@ -84,7 +84,7 @@ def test_volume_detail(self): def test_volume_list(self): result = self.run_command(['block', 'volume-list']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assertEqual([ { 'bytes_used': None, 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 6f54e5cce..69674c636 100644 --- a/tests/CLI/modules/ticket_tests.py +++ b/tests/CLI/modules/ticket_tests.py @@ -22,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): @@ -38,7 +38,7 @@ 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): @@ -46,7 +46,7 @@ def test_create(self): '--subject-id=1000', '--body=ticket body']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) args = ({'subjectId': 1000, 'contents': 'ticket body', @@ -63,7 +63,7 @@ def test_create_and_attach(self): '--hardware=234', '--virtual=567']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) args = ({'subjectId': 1000, 'contents': 'ticket body', @@ -84,7 +84,7 @@ 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.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) args = ({'subjectId': 1000, 'contents': 'ticket body', @@ -98,7 +98,7 @@ def test_subjects(self): list_expected_ids = [1001, 1002, 1003, 1004, 1005] result = self.run_command(['ticket', 'subjects']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) results = json.loads(result.output) for result in results: self.assertIn(result['id'], list_expected_ids) @@ -122,7 +122,7 @@ def test_attach_two_identifiers(self): def test_ticket_attach_hardware(self): result = self.run_command(['ticket', 'attach', '1', '--hardware=100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Ticket', 'addAttachedHardware', args=(100,), identifier=1) @@ -130,7 +130,7 @@ def test_ticket_attach_hardware(self): def test_ticket_attach_virtual_server(self): result = self.run_command(['ticket', 'attach', '1', '--virtual=100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Ticket', 'addAttachedVirtualGuest', args=(100,), identifier=1) @@ -153,7 +153,7 @@ def test_detach_two_identifiers(self): def test_ticket_detach_hardware(self): result = self.run_command(['ticket', 'detach', '1', '--hardware=100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Ticket', 'removeAttachedHardware', args=(100,), @@ -162,7 +162,7 @@ def test_ticket_detach_hardware(self): def test_ticket_detach_virtual_server(self): result = self.run_command(['ticket', 'detach', '1', '--virtual=100']) - self.assertEqual(result.exit_code, 0) + self.assert_no_fail(result) self.assert_called_with('SoftLayer_Ticket', 'removeAttachedVirtualGuest', args=(100,), diff --git a/tests/CLI/modules/vs_tests.py b/tests/CLI/modules/vs_tests.py index 4eb31578a..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, @@ -82,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'], @@ -91,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'], @@ -118,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, @@ -149,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, @@ -206,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') @@ -253,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) @@ -271,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) @@ -297,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) @@ -332,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) @@ -381,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] @@ -401,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(